internal/pkg/apikey/apikey.go (112 lines of code) (raw):

// 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 handles operations dealing with elasticsearch's API keys package apikey import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "strings" "unicode/utf8" "github.com/elastic/go-elasticsearch/v8" "github.com/elastic/go-elasticsearch/v8/esapi" ) const ( authPrefix = "ApiKey " ) var ( ErrNoAuthHeader = errors.New("no authorization header") 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") // APIKeyMetadata tracks Metadata associated with an APIKey. type APIKeyMetadata struct { ID string Metadata Metadata RoleDescriptors json.RawMessage } // Read gathers APIKeyMetadata from Elasticsearch using the given client. func Read(ctx context.Context, client *elasticsearch.Client, id string, withOwner bool) (*APIKeyMetadata, error) { opts := []func(*esapi.SecurityGetAPIKeyRequest){ client.Security.GetAPIKey.WithContext(ctx), client.Security.GetAPIKey.WithID(id), } if withOwner { opts = append(opts, client.Security.GetAPIKey.WithOwner(true)) } res, err := client.Security.GetAPIKey( opts..., ) if err != nil { return nil, fmt.Errorf("request to elasticsearch failed: %w", err) } defer res.Body.Close() if res.IsError() { return nil, fmt.Errorf("%s: %w", res.String(), ErrAPIKeyNotFound) } type APIKeyResponse struct { ID string `json:"id"` Metadata Metadata `json:"metadata"` RoleDescriptors json.RawMessage `json:"role_descriptors"` } type GetAPIKeyResponse struct { APIKeys []APIKeyResponse `json:"api_keys"` } var resp GetAPIKeyResponse d := json.NewDecoder(res.Body) if err = d.Decode(&resp); err != nil { return nil, fmt.Errorf( "could not decode elasticsearch GetAPIKeyResponse: %w", err) } if len(resp.APIKeys) == 0 { return nil, ErrAPIKeyNotFound } first := resp.APIKeys[0] return &APIKeyMetadata{ ID: first.ID, Metadata: first.Metadata, RoleDescriptors: first.RoleDescriptors, }, nil } // APIKey is used to represent an Elasticsearch API Key. type APIKey struct { ID string Key string } // NewAPIKeyFromToken generates an APIKey from the given b64 encoded token. func NewAPIKeyFromToken(token string) (*APIKey, error) { d, err := base64.StdEncoding.DecodeString(token) if err != nil { return nil, fmt.Errorf("%w: %w", ErrInvalidToken, err) } if !utf8.Valid(d) { return nil, ErrInvalidToken } s := strings.Split(string(d), ":") if len(s) != 2 { return nil, ErrMalformedToken } // interpret id:key apiKey := APIKey{ ID: s[0], Key: s[1], } return &apiKey, nil } // Token returns the b64 encoded token of the APIKey. func (k APIKey) Token() string { s := fmt.Sprintf("%s:%s", k.ID, k.Key) return base64.StdEncoding.EncodeToString([]byte(s)) } // Agent provides a string consisting of "ID:Key" func (k APIKey) Agent() string { return fmt.Sprintf("%s:%s", k.ID, k.Key) } // ExtractAPIKey gathers to APIKey associated with the request. func ExtractAPIKey(r *http.Request) (*APIKey, error) { s, ok := r.Header[AuthKey] if !ok { return nil, ErrNoAuthHeader } if len(s) != 1 || !strings.HasPrefix(s[0], authPrefix) { return nil, ErrMalformedHeader } apiKeyStr := s[0][len(authPrefix):] apiKeyStr = strings.TrimSpace(apiKeyStr) return NewAPIKeyFromToken(apiKeyStr) }