internal/pkg/policy/secret.go (205 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 policy
import (
"context"
"regexp"
"strconv"
"strings"
"github.com/elastic/fleet-server/v7/internal/pkg/bulk"
"github.com/elastic/fleet-server/v7/internal/pkg/model"
"github.com/elastic/fleet-server/v7/internal/pkg/smap"
)
var (
secretRegex = regexp.MustCompile(`\$co\.elastic\.secret{([^}]*)}`)
)
// read secret values that belong to the agent policy's secret references, returns secrets as id:value map
func getSecretValues(ctx context.Context, secretRefs []model.SecretReferencesItems, bulker bulk.Bulk) (map[string]string, error) {
if len(secretRefs) == 0 {
return nil, nil
}
ids := make([]string, 0, len(secretRefs))
for _, ref := range secretRefs {
ids = append(ids, ref.ID)
}
results, err := bulker.ReadSecrets(ctx, ids)
if err != nil {
return nil, err
}
return results, nil
}
// read inputs and secret_references from agent policy
// replace values of secret refs in inputs and input streams properties
func getPolicyInputsWithSecrets(ctx context.Context, data *model.PolicyData, bulker bulk.Bulk) ([]map[string]interface{}, []string, error) {
if len(data.Inputs) == 0 {
return nil, nil, nil
}
if len(data.SecretReferences) == 0 {
return data.Inputs, nil, nil
}
secretValues, err := getSecretValues(ctx, data.SecretReferences, bulker)
if err != nil {
return nil, nil, err
}
result := make([]map[string]interface{}, 0)
keys := make([]string, 0)
for i, input := range data.Inputs {
newInput, ks := replaceMapRef(input, secretValues)
for _, key := range ks {
keys = append(keys, "inputs."+strconv.Itoa(i)+"."+key)
}
result = append(result, newInput)
}
data.SecretReferences = nil
return result, keys, nil
}
// replaceMapRef replaces all nested secret values in the passed input and returns the resulting input along with a list of keys where inputs have been replaced.
func replaceMapRef(input map[string]any, secrets map[string]string) (map[string]any, []string) {
keys := make([]string, 0)
result := make(map[string]any, len(input))
var r any
for k, v := range input {
switch value := v.(type) {
case string:
ref, replaced := replaceStringRef(value, secrets)
if replaced {
keys = append(keys, k)
}
r = ref
case map[string]any:
ref, ks := replaceMapRef(value, secrets)
for _, key := range ks {
keys = append(keys, k+"."+key)
}
r = ref
case []any:
ref, ks := replaceSliceRef(value, secrets)
for _, key := range ks {
keys = append(keys, k+"."+key)
}
r = ref
default:
r = v
}
result[k] = r
}
return result, keys
}
// replaceSliceRef replaces all nested secrets within the passed slice and returns the resulting slice along with a list of keys that indicate where values have been replaced.
func replaceSliceRef(arr []any, secrets map[string]string) ([]any, []string) {
keys := make([]string, 0)
result := make([]any, len(arr))
var r any
for i, v := range arr {
switch value := v.(type) {
case string:
ref, replaced := replaceStringRef(value, secrets)
if replaced {
keys = append(keys, strconv.Itoa(i))
}
r = ref
case map[string]any:
ref, ks := replaceMapRef(value, secrets)
for _, key := range ks {
keys = append(keys, strconv.Itoa(i)+"."+key)
}
r = ref
case []any:
ref, ks := replaceSliceRef(value, secrets)
for _, key := range ks {
keys = append(keys, strconv.Itoa(i)+"."+key)
}
r = ref
default:
r = v
}
result[i] = r
}
return result, keys
}
type OutputSecret struct {
Path []string
ID string
}
func getSecretIDAndPath(secret smap.Map) ([]OutputSecret, error) {
outputSecrets := make([]OutputSecret, 0)
secretID := secret.GetString("id")
if secretID != "" {
outputSecrets = append(outputSecrets, OutputSecret{
Path: make([]string, 0),
ID: secretID,
})
return outputSecrets, nil
}
for secretKey := range secret {
newOutputSecrets, err := getSecretIDAndPath(secret.GetMap(secretKey))
if err != nil {
return nil, err
}
for _, secret := range newOutputSecrets {
path := append([]string{secretKey}, secret.Path...)
outputSecrets = append(outputSecrets, OutputSecret{
Path: path,
ID: secret.ID,
})
}
}
return outputSecrets, nil
}
func setSecretPath(output smap.Map, secretValue string, secretPaths []string) error {
// Break the recursion
if len(secretPaths) == 1 {
output[secretPaths[0]] = secretValue
return nil
}
path, secretPaths := secretPaths[0], secretPaths[1:]
if output.GetMap(path) == nil {
output[path] = make(map[string]interface{})
}
return setSecretPath(output.GetMap(path), secretValue, secretPaths)
}
// Read secret from output and mutate output with secret value
func ProcessOutputSecret(ctx context.Context, output smap.Map, bulker bulk.Bulk) ([]string, error) {
secrets := output.GetMap(FieldOutputSecrets)
delete(output, FieldOutputSecrets)
secretReferences := make([]model.SecretReferencesItems, 0)
outputSecrets, err := getSecretIDAndPath(secrets)
keys := make([]string, 0, len(outputSecrets))
if err != nil {
return nil, err
}
for _, secret := range outputSecrets {
secretReferences = append(secretReferences, model.SecretReferencesItems{
ID: secret.ID,
})
}
if len(secretReferences) == 0 {
return nil, nil
}
secretValues, err := getSecretValues(ctx, secretReferences, bulker)
if err != nil {
return nil, err
}
for _, secret := range outputSecrets {
var key string
for _, p := range secret.Path {
if key == "" {
key = p
continue
}
key = key + "." + p
}
keys = append(keys, key)
err = setSecretPath(output, secretValues[secret.ID], secret.Path)
if err != nil {
return nil, err
}
}
return keys, nil
}
// replaceStringRef replaces values matching a secret ref regex, e.g. $co.elastic.secret{<secret ref>} -> <secret value>
// and does this for multiple matches
// returns the resulting string value, and if any replacements were made
func replaceStringRef(ref string, secretValues map[string]string) (string, bool) {
hasReplaced := false
matches := secretRegex.FindStringSubmatch(ref)
for len(matches) > 1 {
secretRef := matches[1]
if val, ok := secretValues[secretRef]; ok {
hasReplaced = true
ref = strings.Replace(ref, matches[0], val, 1)
matches = secretRegex.FindStringSubmatch(ref)
continue
}
break
}
return ref, hasReplaced
}