// 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
}