[CC-5719] Add support for builtin global-read-only policy (#18319)

* [CC-5719] Add support for builtin global-read-only policy

* Add changelog

* Add read-only to docs

* Fix some minor issues.

* Change from ReplaceAll to Sprintf

* Change IsValidPolicy name to return an error instead of bool

* Fix PolicyList test

* Fix other tests

* Apply suggestions from code review

Co-authored-by: Paul Glass <pglass@hashicorp.com>

* Fix state store test for policy list.

* Fix naming issues

* Update acl/validation.go

Co-authored-by: Chris Thain <32781396+cthain@users.noreply.github.com>

* Update agent/consul/acl_endpoint.go

---------

Co-authored-by: Paul Glass <pglass@hashicorp.com>
Co-authored-by: Chris Thain <32781396+cthain@users.noreply.github.com>
This commit is contained in:
Jeremy Jacobson 2023-08-01 10:12:14 -07:00 committed by GitHub
parent bb6fc63823
commit 6424ef6a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 319 additions and 152 deletions

6
.changelog/18319.txt Normal file
View File

@ -0,0 +1,6 @@
```release-note:improvement
acl: added builtin ACL policy that provides global read-only access (builtin/global-read-only)
```
```release-note:improvement
acl: allow for a single slash character in policy names
```

View File

@ -12,6 +12,8 @@ const (
AnonymousTokenID = "00000000-0000-0000-0000-000000000002" AnonymousTokenID = "00000000-0000-0000-0000-000000000002"
AnonymousTokenAlias = "anonymous token" AnonymousTokenAlias = "anonymous token"
AnonymousTokenSecret = "anonymous" AnonymousTokenSecret = "anonymous"
ReservedBuiltinPrefix = "builtin/"
) )
// Config encapsulates all of the generic configuration parameters used for // Config encapsulates all of the generic configuration parameters used for

View File

@ -3,17 +3,22 @@
package acl package acl
import "regexp" import (
"fmt"
"regexp"
"strings"
)
const ( const (
ServiceIdentityNameMaxLength = 256 ServiceIdentityNameMaxLength = 256
NodeIdentityNameMaxLength = 256 NodeIdentityNameMaxLength = 256
PolicyNameMaxLength = 128
) )
var ( var (
validServiceIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`) validServiceIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
validNodeIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`) validNodeIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`) validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]+\/?[A-Za-z0-9\-_]*$`)
validRoleName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,256}$`) validRoleName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,256}$`)
validAuthMethodName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`) validAuthMethodName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
) )
@ -40,10 +45,21 @@ func IsValidNodeIdentityName(name string) bool {
return validNodeIdentityName.MatchString(name) return validNodeIdentityName.MatchString(name)
} }
// IsValidPolicyName returns true if the provided name can be used as an // ValidatePolicyName returns nil if the provided name can be used as an
// ACLPolicy Name. // ACLPolicy Name otherwise a useful error is returned.
func IsValidPolicyName(name string) bool { func ValidatePolicyName(name string) error {
return validPolicyName.MatchString(name) if len(name) < 1 || len(name) > PolicyNameMaxLength {
return fmt.Errorf("Invalid Policy: invalid Name. Length must be greater than 0 and less than %d", PolicyNameMaxLength)
}
if strings.HasPrefix(name, "/") || strings.HasPrefix(name, ReservedBuiltinPrefix) {
return fmt.Errorf("Invalid Policy: invalid Name. Names cannot be prefixed with '/' or '%s'", ReservedBuiltinPrefix)
}
if !validPolicyName.MatchString(name) {
return fmt.Errorf("Invalid Policy: invalid Name. Only alphanumeric characters, a single '/', '-' and '_' are allowed")
}
return nil
} }
// IsValidRoleName returns true if the provided name can be used as an // IsValidRoleName returns true if the provided name can be used as an

78
acl/validation_test.go Normal file
View File

@ -0,0 +1,78 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package acl
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_ValidatePolicyName(t *testing.T) {
for _, tc := range []struct {
description string
name string
valid bool
}{
{
description: "valid policy",
name: "this-is-valid",
valid: true,
},
{
description: "empty policy",
name: "",
valid: false,
},
{
description: "with slash",
name: "policy/with-slash",
valid: true,
},
{
description: "leading slash",
name: "/no-leading-slash",
valid: false,
},
{
description: "too many slashes",
name: "too/many/slashes",
valid: false,
},
{
description: "no double-slash",
name: "no//double-slash",
valid: false,
},
{
description: "builtin prefix",
name: "builtin/prefix-cannot-be-used",
valid: false,
},
{
description: "long",
name: "this-policy-name-is-very-very-long-but-it-is-okay-because-it-is-the-max-length-that-we-allow-here-in-a-policy-name-which-is-good",
valid: true,
},
{
description: "too long",
name: "this-is-a-policy-that-has-one-character-too-many-it-is-way-too-long-for-a-policy-we-do-not-want-a-policy-of-this-length-because-1",
valid: false,
},
{
description: "invalid start character",
name: "!foo",
valid: false,
},
{
description: "invalid character",
name: "this%is%bad",
valid: false,
},
} {
t.Run(tc.description, func(t *testing.T) {
require.Equal(t, tc.valid, ValidatePolicyName(tc.name) == nil)
})
}
}

View File

@ -438,8 +438,8 @@ func TestACL_HTTP(t *testing.T) {
policies, ok := raw.(structs.ACLPolicyListStubs) policies, ok := raw.(structs.ACLPolicyListStubs)
require.True(t, ok) require.True(t, ok)
// 2 we just created + global management // 2 we just created + builtin policies
require.Len(t, policies, 3) require.Len(t, policies, 2+len(structs.ACLBuiltinPolicies))
for policyID, expected := range policyMap { for policyID, expected := range policyMap {
found := false found := false

View File

@ -869,8 +869,8 @@ func (a *ACL) PolicySet(args *structs.ACLPolicySetRequest, reply *structs.ACLPol
return fmt.Errorf("Invalid Policy: no Name is set") return fmt.Errorf("Invalid Policy: no Name is set")
} }
if !acl.IsValidPolicyName(policy.Name) { if err := acl.ValidatePolicyName(policy.Name); err != nil {
return fmt.Errorf("Invalid Policy: invalid Name. Only alphanumeric characters, '-' and '_' are allowed") return err
} }
var idMatch *structs.ACLPolicy var idMatch *structs.ACLPolicy
@ -915,13 +915,13 @@ func (a *ACL) PolicySet(args *structs.ACLPolicySetRequest, reply *structs.ACLPol
return fmt.Errorf("Invalid Policy: A policy with name %q already exists", policy.Name) return fmt.Errorf("Invalid Policy: A policy with name %q already exists", policy.Name)
} }
if policy.ID == structs.ACLPolicyGlobalManagementID { if builtinPolicy, ok := structs.ACLBuiltinPolicies[policy.ID]; ok {
if policy.Datacenters != nil || len(policy.Datacenters) > 0 { if policy.Datacenters != nil || len(policy.Datacenters) > 0 {
return fmt.Errorf("Changing the Datacenters of the builtin global-management policy is not permitted") return fmt.Errorf("Changing the Datacenters of the %s policy is not permitted", builtinPolicy.Name)
} }
if policy.Rules != idMatch.Rules { if policy.Rules != idMatch.Rules {
return fmt.Errorf("Changing the Rules for the builtin global-management policy is not permitted") return fmt.Errorf("Changing the Rules for the builtin %s policy is not permitted", builtinPolicy.Name)
} }
} }
} }
@ -999,8 +999,8 @@ func (a *ACL) PolicyDelete(args *structs.ACLPolicyDeleteRequest, reply *string)
return fmt.Errorf("policy does not exist: %w", acl.ErrNotFound) return fmt.Errorf("policy does not exist: %w", acl.ErrNotFound)
} }
if policy.ID == structs.ACLPolicyGlobalManagementID { if builtinPolicy, ok := structs.ACLBuiltinPolicies[policy.ID]; ok {
return fmt.Errorf("Delete operation not permitted on the builtin global-management policy") return fmt.Errorf("Delete operation not permitted on the builtin %s policy", builtinPolicy.Name)
} }
req := structs.ACLPolicyBatchDeleteRequest{ req := structs.ACLPolicyBatchDeleteRequest{

View File

@ -2183,7 +2183,7 @@ func TestACLEndpoint_PolicySet_CustomID(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestACLEndpoint_PolicySet_globalManagement(t *testing.T) { func TestACLEndpoint_PolicySet_builtins(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")
} }
@ -2195,13 +2195,16 @@ func TestACLEndpoint_PolicySet_globalManagement(t *testing.T) {
aclEp := ACL{srv: srv} aclEp := ACL{srv: srv}
for _, builtinPolicy := range structs.ACLBuiltinPolicies {
name := fmt.Sprintf("foobar-%s", builtinPolicy.Name) // This is required to get past validation
// Can't change the rules // Can't change the rules
{ {
req := structs.ACLPolicySetRequest{ req := structs.ACLPolicySetRequest{
Datacenter: "dc1", Datacenter: "dc1",
Policy: structs.ACLPolicy{ Policy: structs.ACLPolicy{
ID: structs.ACLPolicyGlobalManagementID, ID: builtinPolicy.ID,
Name: "foobar", // This is required to get past validation Name: name,
Rules: "service \"\" { policy = \"write\" }", Rules: "service \"\" { policy = \"write\" }",
}, },
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken}, WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
@ -2209,7 +2212,7 @@ func TestACLEndpoint_PolicySet_globalManagement(t *testing.T) {
resp := structs.ACLPolicy{} resp := structs.ACLPolicy{}
err := aclEp.PolicySet(&req, &resp) err := aclEp.PolicySet(&req, &resp)
require.EqualError(t, err, "Changing the Rules for the builtin global-management policy is not permitted") require.EqualError(t, err, fmt.Sprintf("Changing the Rules for the builtin %s policy is not permitted", builtinPolicy.Name))
} }
// Can rename it // Can rename it
@ -2217,9 +2220,9 @@ func TestACLEndpoint_PolicySet_globalManagement(t *testing.T) {
req := structs.ACLPolicySetRequest{ req := structs.ACLPolicySetRequest{
Datacenter: "dc1", Datacenter: "dc1",
Policy: structs.ACLPolicy{ Policy: structs.ACLPolicy{
ID: structs.ACLPolicyGlobalManagementID, ID: builtinPolicy.ID,
Name: "foobar", Name: name,
Rules: structs.ACLPolicyGlobalManagement, Rules: builtinPolicy.Rules,
}, },
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken}, WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
} }
@ -2229,13 +2232,13 @@ func TestACLEndpoint_PolicySet_globalManagement(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Get the policy again // Get the policy again
policyResp, err := retrieveTestPolicy(codec, TestDefaultInitialManagementToken, "dc1", structs.ACLPolicyGlobalManagementID) policyResp, err := retrieveTestPolicy(codec, TestDefaultInitialManagementToken, "dc1", builtinPolicy.ID)
require.NoError(t, err) require.NoError(t, err)
policy := policyResp.Policy policy := policyResp.Policy
require.Equal(t, policy.ID, structs.ACLPolicyGlobalManagementID) require.Equal(t, policy.ID, builtinPolicy.ID)
require.Equal(t, policy.Name, "foobar") require.Equal(t, policy.Name, name)
}
} }
} }
@ -2271,7 +2274,7 @@ func TestACLEndpoint_PolicyDelete(t *testing.T) {
require.Nil(t, tokenResp.Policy) require.Nil(t, tokenResp.Policy)
} }
func TestACLEndpoint_PolicyDelete_globalManagement(t *testing.T) { func TestACLEndpoint_PolicyDelete_builtins(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")
} }
@ -2282,16 +2285,17 @@ func TestACLEndpoint_PolicyDelete_globalManagement(t *testing.T) {
waitForLeaderEstablishment(t, srv) waitForLeaderEstablishment(t, srv)
aclEp := ACL{srv: srv} aclEp := ACL{srv: srv}
for _, builtinPolicy := range structs.ACLBuiltinPolicies {
req := structs.ACLPolicyDeleteRequest{ req := structs.ACLPolicyDeleteRequest{
Datacenter: "dc1", Datacenter: "dc1",
PolicyID: structs.ACLPolicyGlobalManagementID, PolicyID: builtinPolicy.ID,
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken}, WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
} }
var resp string var resp string
err := aclEp.PolicyDelete(&req, &resp) err := aclEp.PolicyDelete(&req, &resp)
require.EqualError(t, err, fmt.Sprintf("Delete operation not permitted on the builtin %s policy", builtinPolicy.Name))
require.EqualError(t, err, "Delete operation not permitted on the builtin global-management policy") }
} }
func TestACLEndpoint_PolicyList(t *testing.T) { func TestACLEndpoint_PolicyList(t *testing.T) {
@ -2324,6 +2328,7 @@ func TestACLEndpoint_PolicyList(t *testing.T) {
policies := []string{ policies := []string{
structs.ACLPolicyGlobalManagementID, structs.ACLPolicyGlobalManagementID,
structs.ACLPolicyGlobalReadOnlyID,
p1.ID, p1.ID,
p2.ID, p2.ID,
} }

View File

@ -244,7 +244,7 @@ func (w *TokenWriter) Delete(secretID string, fromLogout bool) error {
func validateTokenID(id string) error { func validateTokenID(id string) error {
if structs.ACLIDReserved(id) { if structs.ACLIDReserved(id) {
return fmt.Errorf("UUIDs with the prefix %q are reserved", structs.ACLReservedPrefix) return fmt.Errorf("UUIDs with the prefix %q are reserved", structs.ACLReservedIDPrefix)
} }
if _, err := uuid.ParseUUID(id); err != nil { if _, err := uuid.ParseUUID(id); err != nil {
return errors.New("not a valid UUID") return errors.New("not a valid UUID")

View File

@ -41,7 +41,7 @@ func TestTokenWriter_Create_Validation(t *testing.T) {
errorContains: "not a valid UUID", errorContains: "not a valid UUID",
}, },
"AccessorID is reserved": { "AccessorID is reserved": {
token: structs.ACLToken{AccessorID: structs.ACLReservedPrefix + generateID(t)}, token: structs.ACLToken{AccessorID: structs.ACLReservedIDPrefix + generateID(t)},
errorContains: "reserved", errorContains: "reserved",
}, },
"AccessorID already in use (as AccessorID)": { "AccessorID already in use (as AccessorID)": {
@ -57,7 +57,7 @@ func TestTokenWriter_Create_Validation(t *testing.T) {
errorContains: "not a valid UUID", errorContains: "not a valid UUID",
}, },
"SecretID is reserved": { "SecretID is reserved": {
token: structs.ACLToken{SecretID: structs.ACLReservedPrefix + generateID(t)}, token: structs.ACLToken{SecretID: structs.ACLReservedIDPrefix + generateID(t)},
errorContains: "reserved", errorContains: "reserved",
}, },
"SecretID already in use (as AccessorID)": { "SecretID already in use (as AccessorID)": {

View File

@ -108,7 +108,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
ID: structs.ACLPolicyGlobalManagementID, ID: structs.ACLPolicyGlobalManagementID,
Name: "global-management", Name: "global-management",
Description: "Builtin Policy that grants unlimited access", Description: "Builtin Policy that grants unlimited access",
Rules: structs.ACLPolicyGlobalManagement, Rules: structs.ACLPolicyGlobalManagementRules,
} }
policy.SetHash(true) policy.SetHash(true)
require.NoError(t, fsm.state.ACLPolicySet(1, policy)) require.NoError(t, fsm.state.ACLPolicySet(1, policy))

View File

@ -420,34 +420,11 @@ func (s *Server) initializeACLs(ctx context.Context) error {
if s.InPrimaryDatacenter() { if s.InPrimaryDatacenter() {
s.logger.Info("initializing acls") s.logger.Info("initializing acls")
// Create/Upgrade the builtin global-management policy // Create/Upgrade the builtin policies
_, policy, err := s.fsm.State().ACLPolicyGetByID(nil, structs.ACLPolicyGlobalManagementID, structs.DefaultEnterpriseMetaInDefaultPartition()) for _, policy := range structs.ACLBuiltinPolicies {
if err != nil { if err := s.writeBuiltinACLPolicy(policy); err != nil {
return fmt.Errorf("failed to get the builtin global-management policy") return err
} }
if policy == nil || policy.Rules != structs.ACLPolicyGlobalManagement {
newPolicy := structs.ACLPolicy{
ID: structs.ACLPolicyGlobalManagementID,
Name: "global-management",
Description: "Builtin Policy that grants unlimited access",
Rules: structs.ACLPolicyGlobalManagement,
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
}
if policy != nil {
newPolicy.Name = policy.Name
newPolicy.Description = policy.Description
}
newPolicy.SetHash(true)
req := structs.ACLPolicyBatchSetRequest{
Policies: structs.ACLPolicies{&newPolicy},
}
_, err := s.raftApply(structs.ACLPolicySetRequestType, &req)
if err != nil {
return fmt.Errorf("failed to create global-management policy: %v", err)
}
s.logger.Info("Created ACL 'global-management' policy")
} }
// Check for configured initial management token. // Check for configured initial management token.
@ -492,6 +469,36 @@ func (s *Server) initializeACLs(ctx context.Context) error {
return nil return nil
} }
// writeBuiltinACLPolicy writes the given built-in policy to Raft if the policy
// is not found or if the policy rules have been changed. The name and
// description of a built-in policy are user-editable and must be preserved
// during updates. This function must only be called in a primary datacenter.
func (s *Server) writeBuiltinACLPolicy(newPolicy structs.ACLPolicy) error {
_, policy, err := s.fsm.State().ACLPolicyGetByID(nil, newPolicy.ID, structs.DefaultEnterpriseMetaInDefaultPartition())
if err != nil {
return fmt.Errorf("failed to get the builtin %s policy", newPolicy.Name)
}
if policy == nil || policy.Rules != newPolicy.Rules {
if policy != nil {
newPolicy.Name = policy.Name
newPolicy.Description = policy.Description
}
newPolicy.EnterpriseMeta = *structs.DefaultEnterpriseMetaInDefaultPartition()
newPolicy.SetHash(true)
req := structs.ACLPolicyBatchSetRequest{
Policies: structs.ACLPolicies{&newPolicy},
}
_, err := s.raftApply(structs.ACLPolicySetRequestType, &req)
if err != nil {
return fmt.Errorf("failed to create %s policy: %v", newPolicy.Name, err)
}
s.logger.Info(fmt.Sprintf("Created ACL '%s' policy", newPolicy.Name))
}
return nil
}
func (s *Server) initializeManagementToken(name, secretID string) error { func (s *Server) initializeManagementToken(name, secretID string) error {
state := s.fsm.State() state := s.fsm.State()
if _, err := uuid.ParseUUID(secretID); err != nil { if _, err := uuid.ParseUUID(secretID); err != nil {

View File

@ -1307,9 +1307,12 @@ func TestLeader_ACL_Initialization(t *testing.T) {
_, s1 := testServerWithConfig(t, conf) _, s1 := testServerWithConfig(t, conf)
testrpc.WaitForTestAgent(t, s1.RPC, "dc1") testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
_, policy, err := s1.fsm.State().ACLPolicyGetByID(nil, structs.ACLPolicyGlobalManagementID, nil) // check that the builtin policies were created
for _, builtinPolicy := range structs.ACLBuiltinPolicies {
_, policy, err := s1.fsm.State().ACLPolicyGetByID(nil, builtinPolicy.ID, nil)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, policy) require.NotNil(t, policy)
}
if tt.initialManagement != "" { if tt.initialManagement != "" {
_, initialManagement, err := s1.fsm.State().ACLTokenGetBySecret(nil, tt.initialManagement, nil) _, initialManagement, err := s1.fsm.State().ACLTokenGetBySecret(nil, tt.initialManagement, nil)
@ -1439,15 +1442,17 @@ func TestLeader_ACLUpgrade_IsStickyEvenIfSerfTagsRegress(t *testing.T) {
waitForLeaderEstablishment(t, s2) waitForLeaderEstablishment(t, s2)
waitForNewACLReplication(t, s2, structs.ACLReplicatePolicies, 1, 0, 0) waitForNewACLReplication(t, s2, structs.ACLReplicatePolicies, 1, 0, 0)
// Everybody has the management policy. // Everybody has the builtin policies.
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
_, policy1, err := s1.fsm.State().ACLPolicyGetByID(nil, structs.ACLPolicyGlobalManagementID, structs.DefaultEnterpriseMetaInDefaultPartition()) for _, builtinPolicy := range structs.ACLBuiltinPolicies {
_, policy1, err := s1.fsm.State().ACLPolicyGetByID(nil, builtinPolicy.ID, structs.DefaultEnterpriseMetaInDefaultPartition())
require.NoError(r, err) require.NoError(r, err)
require.NotNil(r, policy1) require.NotNil(r, policy1)
_, policy2, err := s2.fsm.State().ACLPolicyGetByID(nil, structs.ACLPolicyGlobalManagementID, structs.DefaultEnterpriseMetaInDefaultPartition()) _, policy2, err := s2.fsm.State().ACLPolicyGetByID(nil, builtinPolicy.ID, structs.DefaultEnterpriseMetaInDefaultPartition())
require.NoError(r, err) require.NoError(r, err)
require.NotNil(r, policy2) require.NotNil(r, policy2)
}
}) })
// Shutdown s1 and s2. // Shutdown s1 and s2.

View File

@ -884,18 +884,18 @@ func aclPolicySetTxn(tx WriteTxn, idx uint64, policy *structs.ACLPolicy) error {
} }
if existing != nil { if existing != nil {
if policy.ID == structs.ACLPolicyGlobalManagementID { if builtinPolicy, ok := structs.ACLBuiltinPolicies[policy.ID]; ok {
// Only the name and description are modifiable // Only the name and description are modifiable
// Here we specifically check that the rules on the global management policy // Here we specifically check that the rules on the builtin policy
// are identical to the correct policy rules within the binary. This is opposed // are identical to the correct policy rules within the binary. This is opposed
// to checking against the current rules to allow us to update the rules during // to checking against the current rules to allow us to update the rules during
// upgrades. // upgrades.
if policy.Rules != structs.ACLPolicyGlobalManagement { if policy.Rules != builtinPolicy.Rules {
return fmt.Errorf("Changing the Rules for the builtin global-management policy is not permitted") return fmt.Errorf("Changing the Rules for the builtin %s policy is not permitted", builtinPolicy.Name)
} }
if policy.Datacenters != nil && len(policy.Datacenters) != 0 { if policy.Datacenters != nil && len(policy.Datacenters) != 0 {
return fmt.Errorf("Changing the Datacenters of the builtin global-management policy is not permitted") return fmt.Errorf("Changing the Datacenters of the builtin %s policy is not permitted", builtinPolicy.Name)
} }
} }
} }
@ -1062,8 +1062,8 @@ func aclPolicyDeleteTxn(tx WriteTxn, idx uint64, value string, fn aclPolicyGetFn
policy := rawPolicy.(*structs.ACLPolicy) policy := rawPolicy.(*structs.ACLPolicy)
if policy.ID == structs.ACLPolicyGlobalManagementID { if builtinPolicy, ok := structs.ACLBuiltinPolicies[policy.ID]; ok {
return fmt.Errorf("Deletion of the builtin global-management policy is not permitted") return fmt.Errorf("Deletion of the builtin %s policy is not permitted", builtinPolicy.Name)
} }
return aclPolicyDeleteWithPolicy(tx, policy, idx) return aclPolicyDeleteWithPolicy(tx, policy, idx)

View File

@ -30,16 +30,17 @@ const (
) )
func setupGlobalManagement(t *testing.T, s *Store) { func setupGlobalManagement(t *testing.T, s *Store) {
policy := structs.ACLPolicy{ policy := structs.ACLBuiltinPolicies[structs.ACLPolicyGlobalManagementID]
ID: structs.ACLPolicyGlobalManagementID,
Name: "global-management",
Description: "Builtin Policy that grants unlimited access",
Rules: structs.ACLPolicyGlobalManagement,
}
policy.SetHash(true) policy.SetHash(true)
require.NoError(t, s.ACLPolicySet(1, &policy)) require.NoError(t, s.ACLPolicySet(1, &policy))
} }
func setupBuiltinGlobalReadOnly(t *testing.T, s *Store) {
policy := structs.ACLBuiltinPolicies[structs.ACLPolicyGlobalReadOnlyID]
policy.SetHash(true)
require.NoError(t, s.ACLPolicySet(2, &policy))
}
func setupAnonymous(t *testing.T, s *Store) { func setupAnonymous(t *testing.T, s *Store) {
token := structs.ACLToken{ token := structs.ACLToken{
AccessorID: acl.AnonymousTokenID, AccessorID: acl.AnonymousTokenID,
@ -53,6 +54,7 @@ func setupAnonymous(t *testing.T, s *Store) {
func testACLStateStore(t *testing.T) *Store { func testACLStateStore(t *testing.T) *Store {
s := testStateStore(t) s := testStateStore(t)
setupGlobalManagement(t, s) setupGlobalManagement(t, s)
setupBuiltinGlobalReadOnly(t, s)
setupAnonymous(t, s) setupAnonymous(t, s)
return s return s
} }
@ -184,6 +186,7 @@ func TestStateStore_ACLBootstrap(t *testing.T) {
s := testStateStore(t) s := testStateStore(t)
setupGlobalManagement(t, s) setupGlobalManagement(t, s)
setupBuiltinGlobalReadOnly(t, s)
canBootstrap, index, err := s.CanBootstrapACLToken() canBootstrap, index, err := s.CanBootstrapACLToken()
require.NoError(t, err) require.NoError(t, err)
@ -1430,7 +1433,7 @@ func TestStateStore_ACLPolicy_SetGet(t *testing.T) {
ID: structs.ACLPolicyGlobalManagementID, ID: structs.ACLPolicyGlobalManagementID,
Name: "global-management", Name: "global-management",
Description: "Global Management", Description: "Global Management",
Rules: structs.ACLPolicyGlobalManagement, Rules: structs.ACLPolicyGlobalManagementRules,
Datacenters: []string{"dc1"}, Datacenters: []string{"dc1"},
} }
@ -1444,7 +1447,7 @@ func TestStateStore_ACLPolicy_SetGet(t *testing.T) {
ID: structs.ACLPolicyGlobalManagementID, ID: structs.ACLPolicyGlobalManagementID,
Name: "management", Name: "management",
Description: "Modified", Description: "Modified",
Rules: structs.ACLPolicyGlobalManagement, Rules: structs.ACLPolicyGlobalManagementRules,
} }
require.NoError(t, s.ACLPolicySet(3, &policy)) require.NoError(t, s.ACLPolicySet(3, &policy))
@ -1494,7 +1497,7 @@ func TestStateStore_ACLPolicy_SetGet(t *testing.T) {
require.NotNil(t, rpolicy) require.NotNil(t, rpolicy)
require.Equal(t, "global-management", rpolicy.Name) require.Equal(t, "global-management", rpolicy.Name)
require.Equal(t, "Builtin Policy that grants unlimited access", rpolicy.Description) require.Equal(t, "Builtin Policy that grants unlimited access", rpolicy.Description)
require.Equal(t, structs.ACLPolicyGlobalManagement, rpolicy.Rules) require.Equal(t, structs.ACLPolicyGlobalManagementRules, rpolicy.Rules)
require.Len(t, rpolicy.Datacenters, 0) require.Len(t, rpolicy.Datacenters, 0)
require.Equal(t, uint64(1), rpolicy.CreateIndex) require.Equal(t, uint64(1), rpolicy.CreateIndex)
require.Equal(t, uint64(1), rpolicy.ModifyIndex) require.Equal(t, uint64(1), rpolicy.ModifyIndex)
@ -1664,31 +1667,39 @@ func TestStateStore_ACLPolicy_List(t *testing.T) {
_, policies, err := s.ACLPolicyList(nil, nil) _, policies, err := s.ACLPolicyList(nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, policies, 3) require.Len(t, policies, 4)
policies.Sort() policies.Sort()
require.Equal(t, structs.ACLPolicyGlobalManagementID, policies[0].ID) require.Equal(t, structs.ACLPolicyGlobalManagementID, policies[0].ID)
require.Equal(t, "global-management", policies[0].Name) require.Equal(t, structs.ACLPolicyGlobalManagementName, policies[0].Name)
require.Equal(t, "Builtin Policy that grants unlimited access", policies[0].Description) require.Equal(t, structs.ACLPolicyGlobalManagementDesc, policies[0].Description)
require.Empty(t, policies[0].Datacenters) require.Empty(t, policies[0].Datacenters)
require.NotEqual(t, []byte{}, policies[0].Hash) require.NotEqual(t, []byte{}, policies[0].Hash)
require.Equal(t, uint64(1), policies[0].CreateIndex) require.Equal(t, uint64(1), policies[0].CreateIndex)
require.Equal(t, uint64(1), policies[0].ModifyIndex) require.Equal(t, uint64(1), policies[0].ModifyIndex)
require.Equal(t, "a2719052-40b3-4a4b-baeb-f3df1831a217", policies[1].ID) require.Equal(t, structs.ACLPolicyGlobalReadOnlyID, policies[1].ID)
require.Equal(t, "acl-write-dc3", policies[1].Name) require.Equal(t, structs.ACLPolicyGlobalReadOnlyName, policies[1].Name)
require.Equal(t, "Can manage ACLs in dc3", policies[1].Description) require.Equal(t, structs.ACLPolicyGlobalReadOnlyDesc, policies[1].Description)
require.ElementsMatch(t, []string{"dc3"}, policies[1].Datacenters) require.Empty(t, policies[1].Datacenters)
require.Nil(t, policies[1].Hash) require.NotEqual(t, []byte{}, policies[1].Hash)
require.Equal(t, uint64(2), policies[1].CreateIndex) require.Equal(t, uint64(2), policies[1].CreateIndex)
require.Equal(t, uint64(2), policies[1].ModifyIndex) require.Equal(t, uint64(2), policies[1].ModifyIndex)
require.Equal(t, "a4f68bd6-3af5-4f56-b764-3c6f20247879", policies[2].ID) require.Equal(t, "a2719052-40b3-4a4b-baeb-f3df1831a217", policies[2].ID)
require.Equal(t, "service-read", policies[2].Name) require.Equal(t, "acl-write-dc3", policies[2].Name)
require.Equal(t, "", policies[2].Description) require.Equal(t, "Can manage ACLs in dc3", policies[2].Description)
require.Empty(t, policies[2].Datacenters) require.ElementsMatch(t, []string{"dc3"}, policies[2].Datacenters)
require.Nil(t, policies[2].Hash) require.Nil(t, policies[2].Hash)
require.Equal(t, uint64(2), policies[2].CreateIndex) require.Equal(t, uint64(2), policies[2].CreateIndex)
require.Equal(t, uint64(2), policies[2].ModifyIndex) require.Equal(t, uint64(2), policies[2].ModifyIndex)
require.Equal(t, "a4f68bd6-3af5-4f56-b764-3c6f20247879", policies[3].ID)
require.Equal(t, "service-read", policies[3].Name)
require.Equal(t, "", policies[3].Description)
require.Empty(t, policies[3].Datacenters)
require.Nil(t, policies[3].Hash)
require.Equal(t, uint64(2), policies[3].CreateIndex)
require.Equal(t, uint64(2), policies[3].ModifyIndex)
} }
func TestStateStore_ACLPolicy_Delete(t *testing.T) { func TestStateStore_ACLPolicy_Delete(t *testing.T) {

View File

@ -46,40 +46,67 @@ const (
// This policy gives unlimited access to everything. Users // This policy gives unlimited access to everything. Users
// may rename if desired but cannot delete or modify the rules. // may rename if desired but cannot delete or modify the rules.
ACLPolicyGlobalManagementID = "00000000-0000-0000-0000-000000000001" ACLPolicyGlobalManagementID = "00000000-0000-0000-0000-000000000001"
ACLPolicyGlobalManagement = ` ACLPolicyGlobalManagementName = "global-management"
acl = "write" ACLPolicyGlobalManagementDesc = "Builtin Policy that grants unlimited access"
ACLPolicyGlobalReadOnlyID = "00000000-0000-0000-0000-000000000002"
ACLPolicyGlobalReadOnlyName = "builtin/global-read-only"
ACLPolicyGlobalReadOnlyDesc = "Builtin Policy that grants unlimited read-only access to all components"
ACLReservedIDPrefix = "00000000-0000-0000-0000-0000000000"
aclPolicyGlobalRulesTemplate = `
acl = "%[1]s"
agent_prefix "" { agent_prefix "" {
policy = "write" policy = "%[1]s"
} }
event_prefix "" { event_prefix "" {
policy = "write" policy = "%[1]s"
} }
key_prefix "" { key_prefix "" {
policy = "write" policy = "%[1]s"
} }
keyring = "write" keyring = "%[1]s"
node_prefix "" { node_prefix "" {
policy = "write" policy = "%[1]s"
} }
operator = "write" operator = "%[1]s"
mesh = "write" mesh = "%[1]s"
peering = "write" peering = "%[1]s"
query_prefix "" { query_prefix "" {
policy = "write" policy = "%[1]s"
} }
service_prefix "" { service_prefix "" {
policy = "write" policy = "%[1]s"
intentions = "write" intentions = "%[1]s"
} }
session_prefix "" { session_prefix "" {
policy = "write" policy = "%[1]s"
}` + EnterpriseACLPolicyGlobalManagement }`
)
ACLReservedPrefix = "00000000-0000-0000-0000-0000000000" var (
ACLPolicyGlobalReadOnlyRules = fmt.Sprintf(aclPolicyGlobalRulesTemplate, "read") + EnterpriseACLPolicyGlobalReadOnly
ACLPolicyGlobalManagementRules = fmt.Sprintf(aclPolicyGlobalRulesTemplate, "write") + EnterpriseACLPolicyGlobalManagement
ACLBuiltinPolicies = map[string]ACLPolicy{
ACLPolicyGlobalManagementID: {
ID: ACLPolicyGlobalManagementID,
Name: ACLPolicyGlobalManagementName,
Description: ACLPolicyGlobalManagementDesc,
Rules: ACLPolicyGlobalManagementRules,
},
ACLPolicyGlobalReadOnlyID: {
ID: ACLPolicyGlobalReadOnlyID,
Name: ACLPolicyGlobalReadOnlyName,
Description: ACLPolicyGlobalReadOnlyDesc,
Rules: ACLPolicyGlobalReadOnlyRules,
},
}
) )
func ACLIDReserved(id string) bool { func ACLIDReserved(id string) bool {
return strings.HasPrefix(id, ACLReservedPrefix) return strings.HasPrefix(id, ACLReservedIDPrefix)
} }
// ACLBootstrapNotAllowedErr is returned once we know that a bootstrap can no // ACLBootstrapNotAllowedErr is returned once we know that a bootstrap can no

View File

@ -14,6 +14,7 @@ import (
const ( const (
EnterpriseACLPolicyGlobalManagement = "" EnterpriseACLPolicyGlobalManagement = ""
EnterpriseACLPolicyGlobalReadOnly = ""
// aclPolicyTemplateServiceIdentity is the template used for synthesizing // aclPolicyTemplateServiceIdentity is the template used for synthesizing
// policies for service identities. // policies for service identities.

View File

@ -190,7 +190,7 @@ func TestAPI_ACLPolicy_List(t *testing.T) {
policies, qm, err := acl.PolicyList(nil) policies, qm, err := acl.PolicyList(nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, policies, 4) require.Len(t, policies, 5)
require.NotEqual(t, 0, qm.LastIndex) require.NotEqual(t, 0, qm.LastIndex)
require.True(t, qm.KnownLeader) require.True(t, qm.KnownLeader)
@ -233,6 +233,11 @@ func TestAPI_ACLPolicy_List(t *testing.T) {
policy4, ok := policyMap["00000000-0000-0000-0000-000000000001"] policy4, ok := policyMap["00000000-0000-0000-0000-000000000001"]
require.True(t, ok) require.True(t, ok)
require.NotNil(t, policy4) require.NotNil(t, policy4)
// make sure the 5th policy is the global read-only
policy5, ok := policyMap["00000000-0000-0000-0000-000000000002"]
require.True(t, ok)
require.NotNil(t, policy5)
} }
func prepTokenPolicies(t *testing.T, acl *ACL) (policies []*ACLPolicy) { func prepTokenPolicies(t *testing.T, acl *ACL) (policies []*ACLPolicy) {

View File

@ -391,7 +391,11 @@ New installations of Consul ship with the following built-in policies.
### Global Management ### Global Management
The `global-management` policy grants unrestricted privileges to any token linked to it. The policy is assigned the reserved ID of `00000000-0000-0000-0000-000000000001`. You can rename the global management policy, but Consul will prevent you from modifying any other attributes, including the rule set and datacenter scope. The `global-management` policy grants unrestricted privileges to any token linked to it. The policy is assigned the reserved ID of `00000000-0000-0000-0000-000000000001`. You can rename the global management policy, but Consul prevents you from modifying any other attributes, including the rule set and datacenter scope.
### Global Read-Only
The `builtin/global-read-only` policy grants unrestricted _read-only_ privileges to any token linked to it. The policy is assigned the reserved ID of `00000000-0000-0000-0000-000000000002`. You can rename the global read-only policy, but Consul prevents you from modifying any other attributes, including the rule set and datacenter scope.
### Namespace Management <EnterpriseAlert inline /> ### Namespace Management <EnterpriseAlert inline />