Skip to content

Commit

Permalink
Implement support for API Key metadata (#195)
Browse files Browse the repository at this point in the history
* Implement support for API Key metadata

* Adjust apikey.Create to make the metadata functional options

* Address code review feedback

* Make metadata properties json omitempty

* Additional changes to the metatadata format

(cherry picked from commit 82ea1e7)
  • Loading branch information
aleksmaus authored and mergify-bot committed Apr 13, 2021
1 parent c5e7d9a commit 124b6c5
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 8 deletions.
6 changes: 4 additions & 2 deletions cmd/fleet/handleEnroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,14 @@ func createFleetAgent(ctx context.Context, bulker bulk.Bulk, id string, agent mo
}

func generateAccessApiKey(ctx context.Context, client *elasticsearch.Client, agentId string) (*apikey.ApiKey, error) {
return apikey.Create(ctx, client, agentId, "", []byte(kFleetAccessRolesJSON))
return apikey.Create(ctx, client, agentId, "", []byte(kFleetAccessRolesJSON),
apikey.NewMetadata(agentId, apikey.TypeAccess))
}

func generateOutputApiKey(ctx context.Context, client *elasticsearch.Client, agentId, outputName string, roles []byte) (*apikey.ApiKey, error) {
name := fmt.Sprintf("%s:%s", agentId, outputName)
return apikey.Create(ctx, client, name, "", roles)
return apikey.Create(ctx, client, name, "", roles,
apikey.NewMetadata(agentId, apikey.TypeOutput))
}

func (et *EnrollerT) fetchEnrollmentKeyRecord(ctx context.Context, id string) (*model.EnrollmentApiKey, error) {
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/apikey/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (
ErrMalformedHeader = errors.New("malformed authorization header")
ErrMalformedToken = errors.New("malformed token")
ErrInvalidToken = errors.New("token not valid utf8")
ErrApiKeyNotFound = errors.New("api key not found")
)

var AuthKey = http.CanonicalHeaderKey("Authorization")
Expand Down
79 changes: 79 additions & 0 deletions internal/pkg/apikey/apikey_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

// +build integration

package apikey

import (
"context"
"errors"
"testing"

ftesting "github.com/elastic/fleet-server/v7/internal/pkg/testing"

"github.com/gofrs/uuid"
"github.com/google/go-cmp/cmp"
)

const testFleetRoles = `
{
"fleet-apikey-access": {
"cluster": [],
"applications": [{
"application": ".fleet",
"privileges": ["no-privileges"],
"resources": ["*"]
}]
}
}
`

func TestCreateApiKeyWithMetadata(t *testing.T) {
ctx, cn := context.WithCancel(context.Background())
defer cn()

bulker := ftesting.SetupBulk(ctx, t)

// Create the key
agentId := uuid.Must(uuid.NewV4()).String()
name := uuid.Must(uuid.NewV4()).String()
akey, err := Create(ctx, bulker.Client(), name, "", []byte(testFleetRoles),
NewMetadata(agentId, TypeAccess))
if err != nil {
t.Fatal(err)
}

// Get the key and verify that metadata was saved correctly
aKeyMeta, err := Get(ctx, bulker.Client(), akey.Id)
if err != nil {
t.Fatal(err)
}

diff := cmp.Diff(ManagedByFleetServer, aKeyMeta.Metadata.ManagedBy)
if diff != "" {
t.Error(diff)
}

diff = cmp.Diff(true, aKeyMeta.Metadata.Managed)
if diff != "" {
t.Error(diff)
}

diff = cmp.Diff(agentId, aKeyMeta.Metadata.AgentId)
if diff != "" {
t.Error(diff)
}

diff = cmp.Diff(TypeAccess.String(), aKeyMeta.Metadata.Type)
if diff != "" {
t.Error(diff)
}

// Try to get the key that doesn't exists, expect ErrApiKeyNotFound
aKeyMeta, err = Get(ctx, bulker.Client(), "0000000000000")
if !errors.Is(err, ErrApiKeyNotFound) {
t.Errorf("Unexpected error type: %v", err)
}
}
5 changes: 4 additions & 1 deletion internal/pkg/apikey/apikey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

// +build !integration

package apikey

import (
"encoding/base64"
"github.com/stretchr/testify/assert"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMonitorLeadership(t *testing.T) {
Expand Down
11 changes: 6 additions & 5 deletions internal/pkg/apikey/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ import (
"github.com/elastic/go-elasticsearch/v8/esapi"
)

func Create(ctx context.Context, client *elasticsearch.Client, name, ttl string, roles []byte) (*ApiKey, error) {

func Create(ctx context.Context, client *elasticsearch.Client, name, ttl string, roles []byte, meta interface{}) (*ApiKey, error) {
payload := struct {
Name string `json:"name,omitempty"`
Expiration string `json:"expiration,omitempty"`
Roles json.RawMessage `json:"role_descriptors,omitempty"`
Metadata interface{} `json:"metadata"`
}{
name,
ttl,
roles,
Name: name,
Expiration: ttl,
Roles: roles,
Metadata: meta,
}

body, err := json.Marshal(&payload)
Expand Down
66 changes: 66 additions & 0 deletions internal/pkg/apikey/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package apikey

import (
"context"
"encoding/json"
"fmt"

"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
)

type ApiKeyMetadata struct {
Id string
Metadata Metadata
}

func Get(ctx context.Context, client *elasticsearch.Client, id string) (apiKey ApiKeyMetadata, err error) {

opts := []func(*esapi.SecurityGetAPIKeyRequest){
client.Security.GetAPIKey.WithContext(ctx),
client.Security.GetAPIKey.WithID(id),
}

res, err := client.Security.GetAPIKey(
opts...,
)

if err != nil {
return
}

defer res.Body.Close()

if res.IsError() {
return apiKey, fmt.Errorf("fail GetAPIKey: %s, %w", res.String(), ErrApiKeyNotFound)
}

type APIKeyResponse struct {
Id string `json:"id"`
Metadata Metadata `json:"metadata"`
}
type GetAPIKeyResponse struct {
ApiKeys []APIKeyResponse `json:"api_keys"`
}

var resp GetAPIKeyResponse
d := json.NewDecoder(res.Body)
if err = d.Decode(&resp); err != nil {
return
}

if len(resp.ApiKeys) == 0 {
return apiKey, ErrApiKeyNotFound
}

first := resp.ApiKeys[0]

return ApiKeyMetadata{
Id: first.Id,
Metadata: first.Metadata,
}, nil
}
34 changes: 34 additions & 0 deletions internal/pkg/apikey/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package apikey

const ManagedByFleetServer = "fleet-server"

type Type int

const (
TypeAccess Type = iota
TypeOutput
)

func (t Type) String() string {
return []string{"access", "output"}[t]
}

type Metadata struct {
AgentId string `json:"agent_id,omitempty"`
Managed bool `json:"managed,omitempty"`
ManagedBy string `json:"managed_by,omitempty"`
Type string `json:"type,omitempty"`
}

func NewMetadata(agentId string, typ Type) Metadata {
return Metadata{
AgentId: agentId,
Managed: true,
ManagedBy: ManagedByFleetServer,
Type: typ.String(),
}
}

0 comments on commit 124b6c5

Please sign in to comment.