mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
12 KiB
338 lines
12 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package structs |
|
|
|
import ( |
|
"bytes" |
|
_ "embed" |
|
"fmt" |
|
"hash" |
|
"hash/fnv" |
|
"text/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 ©, 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 |
|
}
|
|
|