consul/agent/structs/acl_templated_policy.go

339 lines
12 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"bytes"
_ "embed"
"fmt"
"hash"
"hash/fnv"
"html/template"
"github.com/hashicorp/go-multierror"
"github.com/xeipuuv/gojsonschema"
"golang.org/x/exp/slices"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib/stringslice"
)
//go:embed acltemplatedpolicy/schemas/node.json
var ACLTemplatedPolicyNodeSchema string
//go:embed acltemplatedpolicy/schemas/service.json
var ACLTemplatedPolicyServiceSchema string
//go:embed acltemplatedpolicy/schemas/workload-identity.json
var ACLTemplatedPolicyWorkloadIdentitySchema string
//go:embed acltemplatedpolicy/schemas/api-gateway.json
var ACLTemplatedPolicyAPIGatewaySchema string
type ACLTemplatedPolicies []*ACLTemplatedPolicy
const (
ACLTemplatedPolicyServiceID = "00000000-0000-0000-0000-000000000003"
ACLTemplatedPolicyNodeID = "00000000-0000-0000-0000-000000000004"
ACLTemplatedPolicyDNSID = "00000000-0000-0000-0000-000000000005"
ACLTemplatedPolicyNomadServerID = "00000000-0000-0000-0000-000000000006"
ACLTemplatedPolicyWorkloadIdentityID = "00000000-0000-0000-0000-000000000007"
ACLTemplatedPolicyAPIGatewayID = "00000000-0000-0000-0000-000000000008"
ACLTemplatedPolicyNomadClientID = "00000000-0000-0000-0000-000000000009"
ACLTemplatedPolicyServiceDescription = "Gives the token or role permissions to register a service and discover services in the Consul catalog. It also gives the specified service's sidecar proxy the permission to discover and route traffic to other services."
ACLTemplatedPolicyNodeDescription = "Gives the token or role permissions for a register an agent/node into the catalog. A node is typically a consul agent but can also be a physical server, cloud instance or a container."
ACLTemplatedPolicyDNSDescription = "Gives the token or role permissions for the Consul DNS to query services in the network."
ACLTemplatedPolicyNomadServerDescription = "Gives the token or role permissions required for integration with a nomad server."
ACLTemplatedPolicyWorkloadIdentityDescription = "Gives the token or role permissions for a specific workload identity."
ACLTemplatedPolicyAPIGatewayDescription = "Gives the token or role permissions for a Consul api gateway"
ACLTemplatedPolicyNomadClientDescription = "Gives the token or role permissions required for integration with a nomad client."
ACLTemplatedPolicyNoRequiredVariablesSchema = "" // catch-all schema for all templated policy that don't require a schema
)
// ACLTemplatedPolicyBase contains basic information about builtin templated policies
// template name, id, template code and schema
type ACLTemplatedPolicyBase struct {
TemplateName string
TemplateID string
Schema string
Template string
Description string
}
var (
// Note: when adding a new builtin template, ensure you update `command/acl/templatedpolicy/formatter.go`
// to handle the new templates required variables and schema.
aclTemplatedPoliciesList = map[string]*ACLTemplatedPolicyBase{
api.ACLTemplatedPolicyServiceName: {
TemplateID: ACLTemplatedPolicyServiceID,
TemplateName: api.ACLTemplatedPolicyServiceName,
Schema: ACLTemplatedPolicyServiceSchema,
Template: ACLTemplatedPolicyService,
Description: ACLTemplatedPolicyServiceDescription,
},
api.ACLTemplatedPolicyNodeName: {
TemplateID: ACLTemplatedPolicyNodeID,
TemplateName: api.ACLTemplatedPolicyNodeName,
Schema: ACLTemplatedPolicyNodeSchema,
Template: ACLTemplatedPolicyNode,
Description: ACLTemplatedPolicyNodeDescription,
},
api.ACLTemplatedPolicyDNSName: {
TemplateID: ACLTemplatedPolicyDNSID,
TemplateName: api.ACLTemplatedPolicyDNSName,
Schema: ACLTemplatedPolicyNoRequiredVariablesSchema,
Template: ACLTemplatedPolicyDNS,
Description: ACLTemplatedPolicyDNSDescription,
},
api.ACLTemplatedPolicyNomadServerName: {
TemplateID: ACLTemplatedPolicyNomadServerID,
TemplateName: api.ACLTemplatedPolicyNomadServerName,
Schema: ACLTemplatedPolicyNoRequiredVariablesSchema,
Template: ACLTemplatedPolicyNomadServer,
Description: ACLTemplatedPolicyNomadServerDescription,
},
api.ACLTemplatedPolicyWorkloadIdentityName: {
TemplateID: ACLTemplatedPolicyWorkloadIdentityID,
TemplateName: api.ACLTemplatedPolicyWorkloadIdentityName,
Schema: ACLTemplatedPolicyWorkloadIdentitySchema,
Template: ACLTemplatedPolicyWorkloadIdentity,
Description: ACLTemplatedPolicyWorkloadIdentityDescription,
},
api.ACLTemplatedPolicyAPIGatewayName: {
TemplateID: ACLTemplatedPolicyAPIGatewayID,
TemplateName: api.ACLTemplatedPolicyAPIGatewayName,
Schema: ACLTemplatedPolicyAPIGatewaySchema,
Template: ACLTemplatedPolicyAPIGateway,
Description: ACLTemplatedPolicyAPIGatewayDescription,
},
api.ACLTemplatedPolicyNomadClientName: {
TemplateID: ACLTemplatedPolicyNomadClientID,
TemplateName: api.ACLTemplatedPolicyNomadClientName,
Schema: ACLTemplatedPolicyNoRequiredVariablesSchema,
Template: ACLTemplatedPolicyNomadClient,
Description: ACLTemplatedPolicyNomadClientDescription,
},
}
)
// ACLTemplatedPolicy represents a template used to generate a `synthetic` policy
// given some input variables.
type ACLTemplatedPolicy struct {
// TemplateID are hidden from all displays and should not be exposed to the users.
TemplateID string `json:",omitempty"`
// TemplateName is used for display purposes mostly and should not be used for policy rendering.
TemplateName string `json:",omitempty"`
// TemplateVariables are input variables required to render templated policies.
TemplateVariables *ACLTemplatedPolicyVariables `json:",omitempty"`
// Datacenters that the synthetic policy will be valid within.
// - No wildcards allowed
// - If empty then the synthetic policy is valid within all datacenters
//
// This is kept for legacy reasons to enable us to replace Node/Service Identities by templated policies.
//
// Only valid for global tokens. It is an error to specify this for local tokens.
Datacenters []string `json:",omitempty"`
}
// ACLTemplatedPolicyVariables are input variables required to render templated policies.
type ACLTemplatedPolicyVariables struct {
Name string `json:"name,omitempty"`
}
func (tp *ACLTemplatedPolicy) Clone() *ACLTemplatedPolicy {
tp2 := *tp
tp2.TemplateVariables = nil
if tp.TemplateVariables != nil {
tp2.TemplateVariables = tp.TemplateVariables.Clone()
}
tp2.Datacenters = stringslice.CloneStringSlice(tp.Datacenters)
return &tp2
}
func (tp *ACLTemplatedPolicy) AddToHash(h hash.Hash) {
h.Write([]byte(tp.TemplateID))
h.Write([]byte(tp.TemplateName))
if tp.TemplateVariables != nil {
tp.TemplateVariables.AddToHash(h)
}
for _, dc := range tp.Datacenters {
h.Write([]byte(dc))
}
}
func (tv *ACLTemplatedPolicyVariables) AddToHash(h hash.Hash) {
h.Write([]byte(tv.Name))
}
func (tv *ACLTemplatedPolicyVariables) Clone() *ACLTemplatedPolicyVariables {
tv2 := *tv
return &tv2
}
// validates templated policy variables against schema.
func (tp *ACLTemplatedPolicy) ValidateTemplatedPolicy(schema string) error {
if schema == "" {
return nil
}
loader := gojsonschema.NewStringLoader(schema)
dataloader := gojsonschema.NewGoLoader(tp.TemplateVariables)
res, err := gojsonschema.Validate(loader, dataloader)
if err != nil {
return fmt.Errorf("failed to load json schema for validation %w", err)
}
// validate service and node identity names
if tp.TemplateVariables != nil {
if tp.TemplateName == api.ACLTemplatedPolicyServiceName && !acl.IsValidServiceIdentityName(tp.TemplateVariables.Name) {
return fmt.Errorf("service identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", tp.TemplateVariables.Name)
}
if tp.TemplateName == api.ACLTemplatedPolicyNodeName && !acl.IsValidNodeIdentityName(tp.TemplateVariables.Name) {
return fmt.Errorf("node identity %q has an invalid name. Only lowercase alphanumeric characters, '-' and '_' are allowed", tp.TemplateVariables.Name)
}
}
if res.Valid() {
return nil
}
var merr *multierror.Error
for _, resultError := range res.Errors() {
merr = multierror.Append(merr, fmt.Errorf(resultError.Description()))
}
return merr.ErrorOrNil()
}
func (tp *ACLTemplatedPolicy) EstimateSize() int {
size := len(tp.TemplateName) + len(tp.TemplateID) + tp.TemplateVariables.EstimateSize()
for _, dc := range tp.Datacenters {
size += len(dc)
}
return size
}
func (tv *ACLTemplatedPolicyVariables) EstimateSize() int {
return len(tv.Name)
}
// SyntheticPolicy generates a policy based on templated policies' ID and variables
//
// Given that we validate this string name before persisting, we do not
// have to escape it before doing the following interpolation.
func (tp *ACLTemplatedPolicy) SyntheticPolicy(entMeta *acl.EnterpriseMeta) (*ACLPolicy, error) {
rules, err := tp.aclTemplatedPolicyRules(entMeta)
if err != nil {
return nil, err
}
hasher := fnv.New128a()
hashID := fmt.Sprintf("%x", hasher.Sum([]byte(rules)))
policy := &ACLPolicy{
Rules: rules,
ID: hashID,
Name: fmt.Sprintf("synthetic-policy-%s", hashID),
Datacenters: tp.Datacenters,
Description: fmt.Sprintf("synthetic policy generated from templated policy: %s", tp.TemplateName),
}
policy.EnterpriseMeta.Merge(entMeta)
policy.SetHash(true)
return policy, nil
}
func (tp *ACLTemplatedPolicy) aclTemplatedPolicyRules(entMeta *acl.EnterpriseMeta) (string, error) {
if entMeta == nil {
entMeta = DefaultEnterpriseMetaInDefaultPartition()
}
entMeta.Normalize()
tpl := template.New(tp.TemplateName)
tmplCode, ok := aclTemplatedPoliciesList[tp.TemplateName]
if !ok {
return "", fmt.Errorf("acl templated policy does not exist: %s", tp.TemplateName)
}
parsedTpl, err := tpl.Parse(tmplCode.Template)
if err != nil {
return "", fmt.Errorf("an error occured when parsing template structs: %w", err)
}
var buf bytes.Buffer
err = parsedTpl.Execute(&buf, struct {
*ACLTemplatedPolicyVariables
Namespace string
Partition string
}{
Namespace: entMeta.NamespaceOrDefault(),
Partition: entMeta.PartitionOrDefault(),
ACLTemplatedPolicyVariables: tp.TemplateVariables,
})
if err != nil {
return "", fmt.Errorf("an error occured when executing on templated policy variables: %w", err)
}
return buf.String(), nil
}
// Deduplicate returns a new list of templated policies without duplicates.
// compares values of template variables to ensure no duplicates
func (tps ACLTemplatedPolicies) Deduplicate() ACLTemplatedPolicies {
list := make(map[string][]ACLTemplatedPolicyVariables)
var out ACLTemplatedPolicies
for _, tp := range tps {
// checks if template name already in the unique list
_, found := list[tp.TemplateName]
if !found {
list[tp.TemplateName] = make([]ACLTemplatedPolicyVariables, 0)
}
templateSchema := aclTemplatedPoliciesList[tp.TemplateName].Schema
// if schema is empty, template does not require variables
if templateSchema == "" {
if !found {
out = append(out, tp)
}
continue
}
if !slices.Contains(list[tp.TemplateName], *tp.TemplateVariables) {
list[tp.TemplateName] = append(list[tp.TemplateName], *tp.TemplateVariables)
out = append(out, tp)
}
}
return out
}
func GetACLTemplatedPolicyBase(templateName string) (*ACLTemplatedPolicyBase, bool) {
if orig, found := aclTemplatedPoliciesList[templateName]; found {
copy := *orig
return &copy, found
}
return nil, false
}
// GetACLTemplatedPolicyList returns a copy of the list of templated policies
func GetACLTemplatedPolicyList() map[string]*ACLTemplatedPolicyBase {
m := make(map[string]*ACLTemplatedPolicyBase, len(aclTemplatedPoliciesList))
for k, v := range aclTemplatedPoliciesList {
m[k] = v
}
return m
}