internal/pkg/policy/parsed_policy.go (160 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"
"encoding/json"
"errors"
"fmt"
"go.elastic.co/apm/v2"
"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"
)
const (
FieldOutputs = "outputs"
FieldOutputType = "type"
FieldOutputSecrets = "secrets"
FieldOutputFleetServer = "fleet_server"
FieldOutputServiceToken = "service_token"
FieldOutputPermissions = "output_permissions"
)
var (
ErrOutputsNotFound = errors.New("outputs not found")
ErrDefaultOutputNotFound = errors.New("default output not found")
ErrMultipleDefaultOutputsFound = errors.New("multiple default outputs found")
ErrInvalidPermissionsFormat = errors.New("invalid permissions format")
)
type RoleT struct {
Raw []byte
Sha2 string
}
type RoleMapT map[string]RoleT
type ParsedPolicyDefaults struct {
Name string
}
type ParsedPolicy struct {
Policy model.Policy
Roles RoleMapT
Outputs map[string]Output
Default ParsedPolicyDefaults
Inputs []map[string]interface{}
SecretKeys []string
Links apm.SpanLink
}
func NewParsedPolicy(ctx context.Context, bulker bulk.Bulk, p model.Policy) (*ParsedPolicy, error) {
var err error
secretKeys := make([]string, 0)
// Interpret the output permissions if available
var roles map[string]RoleT
if roles, err = parsePerms(p.Data.OutputPermissions); err != nil {
return nil, err
}
policyOutputs, err := constructPolicyOutputs(p.Data.Outputs, roles)
if err != nil {
return nil, err
}
for name, policyOutput := range p.Data.Outputs {
ks, err := ProcessOutputSecret(ctx, policyOutput, bulker)
if err != nil {
return nil, err
}
for _, key := range ks {
secretKeys = append(secretKeys, "outputs."+name+"."+key)
}
}
defaultName, err := findDefaultOutputName(p.Data.Outputs)
if err != nil {
return nil, err
}
policyInputs, keys, err := getPolicyInputsWithSecrets(ctx, p.Data, bulker)
if err != nil {
return nil, err
}
secretKeys = append(secretKeys, keys...)
// We are cool and the gang
pp := &ParsedPolicy{
Policy: p,
Roles: roles,
Outputs: policyOutputs,
Default: ParsedPolicyDefaults{
Name: defaultName,
},
Inputs: policyInputs,
SecretKeys: secretKeys,
}
if trace := apm.TransactionFromContext(ctx); trace != nil {
// Pass current transaction link (should be a monitor transaction) to caller (likely a client request).
tCtx := trace.TraceContext()
pp.Links = apm.SpanLink{
Trace: tCtx.Trace,
Span: tCtx.Span,
}
}
return pp, nil
}
func constructPolicyOutputs(outputs map[string]map[string]interface{}, roles map[string]RoleT) (map[string]Output, error) {
result := make(map[string]Output, len(outputs))
for k, v := range outputs {
typeStr, ok := v["type"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid output type: %+v", v)
}
p := Output{
Name: k,
Type: typeStr,
}
if role, ok := roles[k]; ok {
p.Role = &role
}
result[k] = p
}
return result, nil
}
func parsePerms(permsRaw json.RawMessage) (RoleMapT, error) {
permMap, err := smap.Parse(permsRaw)
if err != nil {
return nil, err
}
// iterate across the keys
m := make(RoleMapT, len(permMap))
for k := range permMap {
v := permMap.GetMap(k)
if v != nil {
var r RoleT
// Stable hash on permissions payload
// permission hash created here
if r.Sha2, err = v.Hash(); err != nil {
return nil, err
}
// Re-marshal, the payload for each section
if r.Raw, err = json.Marshal(v); err != nil {
return nil, err
}
m[k] = r
}
}
return m, nil
}
// findDefaultName returns the name of the 1st output with the "elasticsearch" type or falls back to behaviour that relies on deprecated fields.
//
// Previous fleet-server and elastic-agent released had a default output which was removed Sept 2021.
func findDefaultOutputName(outputs map[string]map[string]interface{}) (string, error) {
// iterate across the keys finding the defaults
var defaults []string
var ESdefaults []string
for k, v := range outputs {
if v != nil {
typeStr, ok := v["type"].(string)
if ok && typeStr == OutputTypeElasticsearch {
ESdefaults = append(ESdefaults, k)
continue
}
fleetServer, ok := v[FieldOutputFleetServer]
if !ok {
defaults = append(defaults, k)
continue
}
fsMap, ok := fleetServer.(map[string]interface{})
if ok {
str, ok := fsMap[FieldOutputServiceToken].(string)
if ok && str == "" {
defaults = append(defaults, k)
continue
}
}
}
}
// Prefer ES outputs over other types
defaults = append(ESdefaults, defaults...)
// Note: When updating this logic to support multiple outputs, this logic
// should change to not be order dependent.
if len(defaults) > 0 {
return defaults[0], nil
}
return "", ErrDefaultOutputNotFound
}