From 136ce8146b1122ca207da3024c368a85c5c46f37 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 13 Apr 2021 21:07:33 +0000 Subject: [PATCH] Implement support for API Key metadata (#195) (#232) * 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 82ea1e798ac682929ebcc65b390d6d556a803c86) Co-authored-by: Aleksandr Maus <aleksandr.maus@elastic.co> --- cmd/fleet/handleEnroll.go | 6 +- internal/pkg/apikey/apikey.go | 1 + .../pkg/apikey/apikey_integration_test.go | 79 +++++++++++++++++++ internal/pkg/apikey/apikey_test.go | 5 +- internal/pkg/apikey/create.go | 11 +-- internal/pkg/apikey/get.go | 66 ++++++++++++++++ internal/pkg/apikey/metadata.go | 34 ++++++++ 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 internal/pkg/apikey/apikey_integration_test.go create mode 100644 internal/pkg/apikey/get.go create mode 100644 internal/pkg/apikey/metadata.go diff --git a/cmd/fleet/handleEnroll.go b/cmd/fleet/handleEnroll.go index e15ceb26d..55ff4c2d1 100644 --- a/cmd/fleet/handleEnroll.go +++ b/cmd/fleet/handleEnroll.go @@ -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) { diff --git a/internal/pkg/apikey/apikey.go b/internal/pkg/apikey/apikey.go index afc02564b..9230cabea 100644 --- a/internal/pkg/apikey/apikey.go +++ b/internal/pkg/apikey/apikey.go @@ -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") diff --git a/internal/pkg/apikey/apikey_integration_test.go b/internal/pkg/apikey/apikey_integration_test.go new file mode 100644 index 000000000..f629f535c --- /dev/null +++ b/internal/pkg/apikey/apikey_integration_test.go @@ -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) + } +} diff --git a/internal/pkg/apikey/apikey_test.go b/internal/pkg/apikey/apikey_test.go index efd6d0fb6..12ca70613 100644 --- a/internal/pkg/apikey/apikey_test.go +++ b/internal/pkg/apikey/apikey_test.go @@ -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) { diff --git a/internal/pkg/apikey/create.go b/internal/pkg/apikey/create.go index 35d4b66b2..52f9c512f 100644 --- a/internal/pkg/apikey/create.go +++ b/internal/pkg/apikey/create.go @@ -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) diff --git a/internal/pkg/apikey/get.go b/internal/pkg/apikey/get.go new file mode 100644 index 000000000..7230dcd95 --- /dev/null +++ b/internal/pkg/apikey/get.go @@ -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 +} diff --git a/internal/pkg/apikey/metadata.go b/internal/pkg/apikey/metadata.go new file mode 100644 index 000000000..5e347ecb8 --- /dev/null +++ b/internal/pkg/apikey/metadata.go @@ -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(), + } +}