internal/kibana/enrollmenttokens.go (88 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 kibana
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
)
type EnrollmentToken struct {
Active bool `json:"active"`
APIKey string `json:"api_key"`
ID string `json:"id"`
Name string `json:"name"`
PolicyID string `json:"policy_id"`
}
// GetEnrollmentTokenForPolicyID returns an active enrollment token for a given policy ID.
// It obtains the token by returning one of the list of active tokens for this policy, or
// requesting one if there are none.
func (c *Client) GetEnrollmentTokenForPolicyID(ctx context.Context, policyID string) (string, error) {
kuery := fmt.Sprintf("active:true and policy_id:%s", policyID)
tokens, err := c.getEnrollmentTokens(ctx, kuery)
if err != nil {
return "", err
}
if len(tokens) == 0 {
token, err := c.requestEnrollmentToken(ctx, policyID)
if err != nil {
return "", fmt.Errorf("no active enrollment token found for policy %s and failed to request one: %w", policyID, err)
}
if !token.Active {
return "", fmt.Errorf("requested token %s is not active, this should not happen", token.ID)
}
return token.APIKey, nil
}
// API sorts tokens by creation date in descending order, so the first one is
// the newest, return it.
return tokens[0].APIKey, nil
}
func (c *Client) getEnrollmentTokens(ctx context.Context, kuery string) ([]EnrollmentToken, error) {
var tokens []EnrollmentToken
var resp struct {
List []EnrollmentToken `json:"list"`
Items []EnrollmentToken `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"perPage`
}
for {
values := make(url.Values)
values.Set("page", strconv.Itoa(resp.Page+1))
values.Set("kuery", kuery)
resource := fmt.Sprintf("%s/enrollment_api_keys?%s", FleetAPI, values.Encode())
statusCode, respBody, err := c.get(ctx, resource)
if err != nil {
return nil, fmt.Errorf("could not get enrollment tokens (query: %q): %w", values.Encode(), err)
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get enrollment tokens (query: %q; API status code = %d; response body = %s", values.Encode(), statusCode, respBody)
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not decode response to get enrollment tokens: %w", err)
}
// Tokens are listed twice, at least on some versions, get only one copy of them.
if len(resp.List) > 0 {
tokens = append(tokens, resp.List...)
} else if len(resp.Items) > 0 {
tokens = append(tokens, resp.Items...)
}
if resp.Page*resp.PerPage >= resp.Total {
break
}
}
return tokens, nil
}
func (c *Client) requestEnrollmentToken(ctx context.Context, policyID string) (*EnrollmentToken, error) {
reqBody := fmt.Sprintf(`{"policy_id":"%s"}`, policyID)
resource := fmt.Sprintf("%s/enrollment_api_keys", FleetAPI)
statusCode, respBody, err := c.post(ctx, resource, []byte(reqBody))
if err != nil {
return nil, fmt.Errorf("could not request enrollment token: %w", err)
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not request enrollment token (API status code = %d; response body = %s", statusCode, respBody)
}
var resp struct {
Item EnrollmentToken `json:"item"`
Action string `json:"action"`
}
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not decode response to request for enrollment token: %w", err)
}
return &resp.Item, nil
}