diff --git a/acl/acl_test.go b/acl/acl_test.go index becc3b7096..c99c564475 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -26,6 +26,7 @@ func legacyPolicy(policy *Policy) *Policy { PreparedQueryPrefixes: policy.PreparedQueries, Keyring: policy.Keyring, Operator: policy.Operator, + Mesh: policy.Mesh, }, } } @@ -108,6 +109,14 @@ func checkAllowNodeWrite(t *testing.T, authz Authorizer, prefix string, entCtx * require.Equal(t, Allow, authz.NodeWrite(prefix, entCtx)) } +func checkAllowMeshRead(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { + require.Equal(t, Allow, authz.MeshRead(entCtx)) +} + +func checkAllowMeshWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { + require.Equal(t, Allow, authz.MeshWrite(entCtx)) +} + func checkAllowOperatorRead(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { require.Equal(t, Allow, authz.OperatorRead(entCtx)) } @@ -220,6 +229,14 @@ func checkDenyNodeWrite(t *testing.T, authz Authorizer, prefix string, entCtx *A require.Equal(t, Deny, authz.NodeWrite(prefix, entCtx)) } +func checkDenyMeshRead(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { + require.Equal(t, Deny, authz.MeshRead(entCtx)) +} + +func checkDenyMeshWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { + require.Equal(t, Deny, authz.MeshWrite(entCtx)) +} + func checkDenyOperatorRead(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { require.Equal(t, Deny, authz.OperatorRead(entCtx)) } @@ -332,6 +349,14 @@ func checkDefaultNodeWrite(t *testing.T, authz Authorizer, prefix string, entCtx require.Equal(t, Default, authz.NodeWrite(prefix, entCtx)) } +func checkDefaultMeshRead(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { + require.Equal(t, Default, authz.MeshRead(entCtx)) +} + +func checkDefaultMeshWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { + require.Equal(t, Default, authz.MeshWrite(entCtx)) +} + func checkDefaultOperatorRead(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) { require.Equal(t, Default, authz.OperatorRead(entCtx)) } @@ -407,6 +432,8 @@ func TestACL(t *testing.T) { {name: "DenyNodeRead", check: checkDenyNodeRead}, {name: "DenyNodeReadAll", check: checkDenyNodeReadAll}, {name: "DenyNodeWrite", check: checkDenyNodeWrite}, + {name: "DenyMeshRead", check: checkDenyMeshRead}, + {name: "DenyMeshWrite", check: checkDenyMeshWrite}, {name: "DenyOperatorRead", check: checkDenyOperatorRead}, {name: "DenyOperatorWrite", check: checkDenyOperatorWrite}, {name: "DenyPreparedQueryRead", check: checkDenyPreparedQueryRead}, @@ -439,6 +466,8 @@ func TestACL(t *testing.T) { {name: "AllowNodeRead", check: checkAllowNodeRead}, {name: "AllowNodeReadAll", check: checkAllowNodeReadAll}, {name: "AllowNodeWrite", check: checkAllowNodeWrite}, + {name: "AllowMeshRead", check: checkAllowMeshRead}, + {name: "AllowMeshWrite", check: checkAllowMeshWrite}, {name: "AllowOperatorRead", check: checkAllowOperatorRead}, {name: "AllowOperatorWrite", check: checkAllowOperatorWrite}, {name: "AllowPreparedQueryRead", check: checkAllowPreparedQueryRead}, @@ -471,6 +500,8 @@ func TestACL(t *testing.T) { {name: "AllowNodeRead", check: checkAllowNodeRead}, {name: "AllowNodeReadAll", check: checkAllowNodeReadAll}, {name: "AllowNodeWrite", check: checkAllowNodeWrite}, + {name: "AllowMeshRead", check: checkAllowMeshRead}, + {name: "AllowMeshWrite", check: checkAllowMeshWrite}, {name: "AllowOperatorRead", check: checkAllowOperatorRead}, {name: "AllowOperatorWrite", check: checkAllowOperatorWrite}, {name: "AllowPreparedQueryRead", check: checkAllowPreparedQueryRead}, @@ -861,6 +892,319 @@ func TestACL(t *testing.T) { {name: "WriteDenied", check: checkDenyKeyringWrite}, }, }, + { + name: "MeshDefaultAllowPolicyDeny", + defaultPolicy: AllowAll(), + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Mesh: PolicyDeny, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + name: "MeshDefaultAllowPolicyRead", + defaultPolicy: AllowAll(), + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Mesh: PolicyRead, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + name: "MeshDefaultAllowPolicyWrite", + defaultPolicy: AllowAll(), + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Mesh: PolicyWrite, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, + { + name: "MeshDefaultAllowPolicyNone", + defaultPolicy: AllowAll(), + policyStack: []*Policy{ + {}, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, + { + name: "MeshDefaultDenyPolicyDeny", + defaultPolicy: DenyAll(), + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Mesh: PolicyDeny, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + name: "MeshDefaultDenyPolicyRead", + defaultPolicy: DenyAll(), + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Mesh: PolicyRead, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + name: "MeshDefaultDenyPolicyWrite", + defaultPolicy: DenyAll(), + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Mesh: PolicyWrite, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, + { + name: "MeshDefaultDenyPolicyNone", + defaultPolicy: DenyAll(), + policyStack: []*Policy{ + {}, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:deny, m:deny = deny + name: "MeshOperatorDenyPolicyDeny", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyDeny, + Mesh: PolicyDeny, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:read, m:deny = deny + name: "MeshOperatorReadPolicyDeny", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyRead, + Mesh: PolicyDeny, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:write, m:deny = deny + name: "MeshOperatorWritePolicyDeny", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyWrite, + Mesh: PolicyDeny, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:deny, m:read = read + name: "MeshOperatorDenyPolicyRead", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyDeny, + Mesh: PolicyRead, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:read, m:read = read + name: "MeshOperatorReadPolicyRead", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyRead, + Mesh: PolicyRead, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:write, m:read = read + name: "MeshOperatorWritePolicyRead", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyWrite, + Mesh: PolicyRead, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:deny, m:write = write + name: "MeshOperatorDenyPolicyWrite", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyDeny, + Mesh: PolicyWrite, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, + { + // o:read, m:write = write + name: "MeshOperatorReadPolicyWrite", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyRead, + Mesh: PolicyWrite, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, + { + // o:write, m:write = write + name: "MeshOperatorWritePolicyWrite", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyWrite, + Mesh: PolicyWrite, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, + { + // o:deny, m: = deny + name: "MeshOperatorDenyPolicyNone", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyDeny, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadDenied", check: checkDenyMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:read, m: = read + name: "MeshOperatorReadPolicyNone", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyRead, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteDenied", check: checkDenyMeshWrite}, + }, + }, + { + // o:write, m: = write + name: "MeshOperatorWritePolicyNone", + defaultPolicy: nil, // test both + policyStack: []*Policy{ + { + PolicyRules: PolicyRules{ + Operator: PolicyWrite, + }, + }, + }, + checks: []aclCheck{ + {name: "ReadAllowed", check: checkAllowMeshRead}, + {name: "WriteAllowed", check: checkAllowMeshWrite}, + }, + }, { name: "OperatorDefaultAllowPolicyDeny", defaultPolicy: AllowAll(), @@ -2002,23 +2346,36 @@ func TestACL(t *testing.T) { }, } + run := func(t *testing.T, tcase aclTest, defaultPolicy Authorizer) { + acl := defaultPolicy + for _, policy := range tcase.policyStack { + newACL, err := NewPolicyAuthorizerWithDefaults(acl, []*Policy{policy}, nil) + require.NoError(t, err) + acl = newACL + } + + for _, check := range tcase.checks { + checkName := check.name + if check.prefix != "" { + checkName = fmt.Sprintf("%s.Prefix(%s)", checkName, check.prefix) + } + t.Run(checkName, func(t *testing.T) { + check.check(t, acl, check.prefix, nil) + }) + } + } + for _, tcase := range tests { t.Run(tcase.name, func(t *testing.T) { - acl := tcase.defaultPolicy - for _, policy := range tcase.policyStack { - newACL, err := NewPolicyAuthorizerWithDefaults(acl, []*Policy{policy}, nil) - require.NoError(t, err) - acl = newACL - } - - for _, check := range tcase.checks { - checkName := check.name - if check.prefix != "" { - checkName = fmt.Sprintf("%s.Prefix(%s)", checkName, check.prefix) - } - t.Run(checkName, func(t *testing.T) { - check.check(t, acl, check.prefix, nil) + if tcase.defaultPolicy == nil { + t.Run("default-allow", func(t *testing.T) { + run(t, tcase, AllowAll()) }) + t.Run("default-deny", func(t *testing.T) { + run(t, tcase, DenyAll()) + }) + } else { + run(t, tcase, tcase.defaultPolicy) } }) } diff --git a/acl/authorizer.go b/acl/authorizer.go index 9693786f0f..43d50544bf 100644 --- a/acl/authorizer.go +++ b/acl/authorizer.go @@ -45,6 +45,7 @@ const ( ResourceKeyring Resource = "keyring" ResourceNode Resource = "node" ResourceOperator Resource = "operator" + ResourceMesh Resource = "mesh" ResourceQuery Resource = "query" ResourceService Resource = "service" ResourceSession Resource = "session" @@ -104,6 +105,14 @@ type Authorizer interface { // KeyringWrite determines if the keyring can be manipulated KeyringWrite(*AuthorizerContext) EnforcementDecision + // MeshRead determines if the read-only Consul mesh functions + // can be used. + MeshRead(*AuthorizerContext) EnforcementDecision + + // MeshWrite determines if the state-changing Consul mesh + // functions can be used. + MeshWrite(*AuthorizerContext) EnforcementDecision + // NodeRead checks for permission to read (discover) a given node. NodeRead(string, *AuthorizerContext) EnforcementDecision @@ -204,6 +213,13 @@ func Enforce(authz Authorizer, rsc Resource, segment string, access string, ctx case "write": return authz.KeyringWrite(ctx), nil } + case ResourceMesh: + switch lowerAccess { + case "read": + return authz.MeshRead(ctx), nil + case "write": + return authz.MeshWrite(ctx), nil + } case ResourceNode: switch lowerAccess { case "read": diff --git a/acl/authorizer_test.go b/acl/authorizer_test.go index b1833efd14..d74029f239 100644 --- a/acl/authorizer_test.go +++ b/acl/authorizer_test.go @@ -129,6 +129,16 @@ func (m *mockAuthorizer) NodeWrite(segment string, ctx *AuthorizerContext) Enfor return ret.Get(0).(EnforcementDecision) } +func (m *mockAuthorizer) MeshRead(ctx *AuthorizerContext) EnforcementDecision { + ret := m.Called(ctx) + return ret.Get(0).(EnforcementDecision) +} + +func (m *mockAuthorizer) MeshWrite(ctx *AuthorizerContext) EnforcementDecision { + ret := m.Called(ctx) + return ret.Get(0).(EnforcementDecision) +} + // OperatorRead determines if the read-only Consul operator functions // can be used. ret := m.Called(segment, ctx) func (m *mockAuthorizer) OperatorRead(ctx *AuthorizerContext) EnforcementDecision { diff --git a/acl/chained_authorizer.go b/acl/chained_authorizer.go index 3915630034..1b3aed4978 100644 --- a/acl/chained_authorizer.go +++ b/acl/chained_authorizer.go @@ -145,6 +145,22 @@ func (c *ChainedAuthorizer) KeyringWrite(entCtx *AuthorizerContext) EnforcementD }) } +// MeshRead determines if the read-only Consul mesh functions +// can be used. +func (c *ChainedAuthorizer) MeshRead(entCtx *AuthorizerContext) EnforcementDecision { + return c.executeChain(func(authz Authorizer) EnforcementDecision { + return authz.MeshRead(entCtx) + }) +} + +// MeshWrite determines if the state-changing Consul mesh +// functions can be used. +func (c *ChainedAuthorizer) MeshWrite(entCtx *AuthorizerContext) EnforcementDecision { + return c.executeChain(func(authz Authorizer) EnforcementDecision { + return authz.MeshWrite(entCtx) + }) +} + // NodeRead checks for permission to read (discover) a given node. func (c *ChainedAuthorizer) NodeRead(node string, entCtx *AuthorizerContext) EnforcementDecision { return c.executeChain(func(authz Authorizer) EnforcementDecision { diff --git a/acl/chained_authorizer_test.go b/acl/chained_authorizer_test.go index 870a00f9c3..7a1aba2396 100644 --- a/acl/chained_authorizer_test.go +++ b/acl/chained_authorizer_test.go @@ -62,6 +62,12 @@ func (authz testAuthorizer) NodeReadAll(*AuthorizerContext) EnforcementDecision func (authz testAuthorizer) NodeWrite(string, *AuthorizerContext) EnforcementDecision { return EnforcementDecision(authz) } +func (authz testAuthorizer) MeshRead(*AuthorizerContext) EnforcementDecision { + return EnforcementDecision(authz) +} +func (authz testAuthorizer) MeshWrite(*AuthorizerContext) EnforcementDecision { + return EnforcementDecision(authz) +} func (authz testAuthorizer) OperatorRead(*AuthorizerContext) EnforcementDecision { return EnforcementDecision(authz) } @@ -113,6 +119,8 @@ func TestChainedAuthorizer(t *testing.T) { checkDenyKeyWritePrefix(t, authz, "foo", nil) checkDenyNodeRead(t, authz, "foo", nil) checkDenyNodeWrite(t, authz, "foo", nil) + checkDenyMeshRead(t, authz, "foo", nil) + checkDenyMeshWrite(t, authz, "foo", nil) checkDenyOperatorRead(t, authz, "foo", nil) checkDenyOperatorWrite(t, authz, "foo", nil) checkDenyPreparedQueryRead(t, authz, "foo", nil) @@ -143,6 +151,8 @@ func TestChainedAuthorizer(t *testing.T) { checkDenyKeyWritePrefix(t, authz, "foo", nil) checkDenyNodeRead(t, authz, "foo", nil) checkDenyNodeWrite(t, authz, "foo", nil) + checkDenyMeshRead(t, authz, "foo", nil) + checkDenyMeshWrite(t, authz, "foo", nil) checkDenyOperatorRead(t, authz, "foo", nil) checkDenyOperatorWrite(t, authz, "foo", nil) checkDenyPreparedQueryRead(t, authz, "foo", nil) @@ -173,6 +183,8 @@ func TestChainedAuthorizer(t *testing.T) { checkAllowKeyWritePrefix(t, authz, "foo", nil) checkAllowNodeRead(t, authz, "foo", nil) checkAllowNodeWrite(t, authz, "foo", nil) + checkAllowMeshRead(t, authz, "foo", nil) + checkAllowMeshWrite(t, authz, "foo", nil) checkAllowOperatorRead(t, authz, "foo", nil) checkAllowOperatorWrite(t, authz, "foo", nil) checkAllowPreparedQueryRead(t, authz, "foo", nil) @@ -203,6 +215,8 @@ func TestChainedAuthorizer(t *testing.T) { checkDenyKeyWritePrefix(t, authz, "foo", nil) checkDenyNodeRead(t, authz, "foo", nil) checkDenyNodeWrite(t, authz, "foo", nil) + checkDenyMeshRead(t, authz, "foo", nil) + checkDenyMeshWrite(t, authz, "foo", nil) checkDenyOperatorRead(t, authz, "foo", nil) checkDenyOperatorWrite(t, authz, "foo", nil) checkDenyPreparedQueryRead(t, authz, "foo", nil) @@ -231,6 +245,8 @@ func TestChainedAuthorizer(t *testing.T) { checkAllowKeyWritePrefix(t, authz, "foo", nil) checkAllowNodeRead(t, authz, "foo", nil) checkAllowNodeWrite(t, authz, "foo", nil) + checkAllowMeshRead(t, authz, "foo", nil) + checkAllowMeshWrite(t, authz, "foo", nil) checkAllowOperatorRead(t, authz, "foo", nil) checkAllowOperatorWrite(t, authz, "foo", nil) checkAllowPreparedQueryRead(t, authz, "foo", nil) diff --git a/acl/policy.go b/acl/policy.go index 8181794f0d..d4f9c6088c 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -84,6 +84,7 @@ type PolicyRules struct { PreparedQueryPrefixes []*PreparedQueryRule `hcl:"query_prefix,expand"` Keyring string `hcl:"keyring"` Operator string `hcl:"operator"` + Mesh string `hcl:"mesh"` } // Policy is used to represent the policy specified by an ACL configuration. @@ -285,6 +286,11 @@ func (pr *PolicyRules) Validate(conf *Config) error { return fmt.Errorf("Invalid operator policy: %#v", pr.Operator) } + // Validate the mesh policy - this one is allowed to be empty + if pr.Mesh != "" && !isPolicyValid(pr.Mesh, false) { + return fmt.Errorf("Invalid mesh policy: %#v", pr.Mesh) + } + return nil } @@ -318,6 +324,7 @@ func parseLegacy(rules string, conf *Config) (*Policy, error) { PreparedQueries []*PreparedQueryRule `hcl:"query,expand"` Keyring string `hcl:"keyring"` Operator string `hcl:"operator"` + // NOTE: mesh resources not supported here } lp := &LegacyPolicy{} @@ -446,6 +453,7 @@ func NewPolicyFromSource(id string, revision uint64, rules string, syntax Syntax return policy, err } +// TODO(ACL-Legacy): remove this func (policy *Policy) ConvertToLegacy() *Policy { converted := &Policy{ ID: policy.ID, @@ -474,6 +482,7 @@ func (policy *Policy) ConvertToLegacy() *Policy { return converted } +// TODO(ACL-Legacy): remove this func (policy *Policy) ConvertFromLegacy() *Policy { return &Policy{ ID: policy.ID, diff --git a/acl/policy_authorizer.go b/acl/policy_authorizer.go index 9985c8feb1..5d06513ac8 100644 --- a/acl/policy_authorizer.go +++ b/acl/policy_authorizer.go @@ -40,6 +40,9 @@ type policyAuthorizer struct { // operatorRule contains the operator policies. operatorRule *policyAuthorizerRule + // meshRule contains the mesh policies. + meshRule *policyAuthorizerRule + // embedded enterprise policy authorizer enterprisePolicyAuthorizer } @@ -310,6 +313,15 @@ func (p *policyAuthorizer) loadRules(policy *PolicyRules) error { p.operatorRule = &policyAuthorizerRule{access: access} } + // Load the mesh policy + if policy.Mesh != "" { + access, err := AccessLevelFromString(policy.Mesh) + if err != nil { + return err + } + p.meshRule = &policyAuthorizerRule{access: access} + } + return nil } @@ -664,6 +676,25 @@ func (p *policyAuthorizer) KeyringWrite(*AuthorizerContext) EnforcementDecision return Default } +// MeshRead determines if the read-only mesh functions are allowed. +func (p *policyAuthorizer) MeshRead(ctx *AuthorizerContext) EnforcementDecision { + if p.meshRule != nil { + return enforce(p.meshRule.access, AccessRead) + } + // default to OperatorRead access + return p.OperatorRead(ctx) +} + +// MeshWrite determines if the state-changing mesh functions are +// allowed. +func (p *policyAuthorizer) MeshWrite(ctx *AuthorizerContext) EnforcementDecision { + if p.meshRule != nil { + return enforce(p.meshRule.access, AccessWrite) + } + // default to OperatorWrite access + return p.OperatorWrite(ctx) +} + // OperatorRead determines if the read-only operator functions are allowed. func (p *policyAuthorizer) OperatorRead(*AuthorizerContext) EnforcementDecision { if p.operatorRule != nil { diff --git a/acl/policy_authorizer_test.go b/acl/policy_authorizer_test.go index d303eea924..f873260326 100644 --- a/acl/policy_authorizer_test.go +++ b/acl/policy_authorizer_test.go @@ -48,6 +48,8 @@ func TestPolicyAuthorizer(t *testing.T) { {name: "DefaultKeyWritePrefix", prefix: "foo", check: checkDefaultKeyWritePrefix}, {name: "DefaultNodeRead", prefix: "foo", check: checkDefaultNodeRead}, {name: "DefaultNodeWrite", prefix: "foo", check: checkDefaultNodeWrite}, + {name: "DefaultMeshRead", prefix: "foo", check: checkDefaultMeshRead}, + {name: "DefaultMeshWrite", prefix: "foo", check: checkDefaultMeshWrite}, {name: "DefaultOperatorRead", prefix: "foo", check: checkDefaultOperatorRead}, {name: "DefaultOperatorWrite", prefix: "foo", check: checkDefaultOperatorWrite}, {name: "DefaultPreparedQueryRead", prefix: "foo", check: checkDefaultPreparedQueryRead}, diff --git a/acl/policy_merger.go b/acl/policy_merger.go index c21cc8e39b..c04ae129a7 100644 --- a/acl/policy_merger.go +++ b/acl/policy_merger.go @@ -17,6 +17,7 @@ type policyRulesMergeContext struct { keyringRule string keyRules map[string]*KeyRule keyPrefixRules map[string]*KeyRule + meshRule string nodeRules map[string]*NodeRule nodePrefixRules map[string]*NodeRule operatorRule string @@ -37,6 +38,7 @@ func (p *policyRulesMergeContext) init() { p.keyringRule = "" p.keyRules = make(map[string]*KeyRule) p.keyPrefixRules = make(map[string]*KeyRule) + p.meshRule = "" p.nodeRules = make(map[string]*NodeRule) p.nodePrefixRules = make(map[string]*NodeRule) p.operatorRule = "" @@ -123,6 +125,10 @@ func (p *policyRulesMergeContext) merge(policy *PolicyRules) { } } + if takesPrecedenceOver(policy.Mesh, p.meshRule) { + p.meshRule = policy.Mesh + } + for _, np := range policy.Nodes { update := true if permission, found := p.nodeRules[np.Name]; found { @@ -234,6 +240,7 @@ func (p *policyRulesMergeContext) update(merged *PolicyRules) { merged.ACL = p.aclRule merged.Keyring = p.keyringRule merged.Operator = p.operatorRule + merged.Mesh = p.meshRule // All the for loop appends are ugly but Go doesn't have a way to get // a slice of all values within a map so this is necessary diff --git a/acl/policy_test.go b/acl/policy_test.go index fd0f4745e4..46f8fb24ef 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -14,80 +14,439 @@ func errStartsWith(t *testing.T, actual error, expected string) { } func TestPolicySourceParse(t *testing.T) { - ljoin := func(lines ...string) string { - return strings.Join(lines, "\n") - } cases := []struct { - Name string - Syntax SyntaxVersion - Rules string - Expected *Policy - Err string + Name string + Syntax SyntaxVersion + Rules string + RulesJSON string + Expected *Policy + Err string }{ { - "Legacy Basic", - SyntaxLegacy, - ljoin( - `agent "foo" { `, - ` policy = "read" `, - `} `, - `agent "bar" { `, - ` policy = "write" `, - `} `, - `event "" { `, - ` policy = "read" `, - `} `, - `event "foo" { `, - ` policy = "write" `, - `} `, - `event "bar" { `, - ` policy = "deny" `, - `} `, - `key "" { `, - ` policy = "read" `, - `} `, - `key "foo/" { `, - ` policy = "write" `, - `} `, - `key "foo/bar/" { `, - ` policy = "read" `, - `} `, - `key "foo/bar/baz" {`, - ` policy = "deny" `, - `} `, - `keyring = "deny" `, - `node "" { `, - ` policy = "read" `, - `} `, - `node "foo" { `, - ` policy = "write" `, - `} `, - `node "bar" { `, - ` policy = "deny" `, - `} `, - `operator = "deny" `, - `service "" { `, - ` policy = "write" `, - `} `, - `service "foo" { `, - ` policy = "read" `, - `} `, - `session "foo" { `, - ` policy = "write" `, - `} `, - `session "bar" { `, - ` policy = "deny" `, - `} `, - `query "" { `, - ` policy = "read" `, - `} `, - `query "foo" { `, - ` policy = "write" `, - `} `, - `query "bar" { `, - ` policy = "deny" `, - `} `), - &Policy{PolicyRules: PolicyRules{ + Name: "Basic", + Syntax: SyntaxCurrent, + Rules: ` + agent_prefix "bar" { + policy = "write" + } + agent "foo" { + policy = "read" + } + event_prefix "" { + policy = "read" + } + event "foo" { + policy = "write" + } + event "bar" { + policy = "deny" + } + key_prefix "" { + policy = "read" + } + key_prefix "foo/" { + policy = "write" + } + key_prefix "foo/bar/" { + policy = "read" + } + key "foo/bar/baz" { + policy = "deny" + } + keyring = "deny" + node_prefix "" { + policy = "read" + } + node "foo" { + policy = "write" + } + node "bar" { + policy = "deny" + } + operator = "deny" + mesh = "deny" + service_prefix "" { + policy = "write" + } + service "foo" { + policy = "read" + } + session "foo" { + policy = "write" + } + session "bar" { + policy = "deny" + } + session_prefix "baz" { + policy = "deny" + } + query_prefix "" { + policy = "read" + } + query "foo" { + policy = "write" + } + query "bar" { + policy = "deny" + } + `, + RulesJSON: ` + { + "agent_prefix": { + "bar": { + "policy": "write" + } + }, + "agent": { + "foo": { + "policy": "read" + } + }, + "event_prefix": { + "": { + "policy": "read" + } + }, + "event": { + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + }, + "key_prefix": { + "": { + "policy": "read" + }, + "foo/": { + "policy": "write" + }, + "foo/bar/": { + "policy": "read" + } + }, + "key": { + "foo/bar/baz": { + "policy": "deny" + } + }, + "keyring": "deny", + "node_prefix": { + "": { + "policy": "read" + } + }, + "node": { + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + }, + "operator": "deny", + "mesh": "deny", + "service_prefix": { + "": { + "policy": "write" + } + }, + "service": { + "foo": { + "policy": "read" + } + }, + "session_prefix": { + "baz": { + "policy": "deny" + } + }, + "session": { + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + }, + "query_prefix": { + "": { + "policy": "read" + } + }, + "query": { + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + } + } + `, + Expected: &Policy{PolicyRules: PolicyRules{ + AgentPrefixes: []*AgentRule{ + { + Node: "bar", + Policy: PolicyWrite, + }, + }, + Agents: []*AgentRule{ + { + Node: "foo", + Policy: PolicyRead, + }, + }, + EventPrefixes: []*EventRule{ + { + Event: "", + Policy: PolicyRead, + }, + }, + Events: []*EventRule{ + { + Event: "foo", + Policy: PolicyWrite, + }, + { + Event: "bar", + Policy: PolicyDeny, + }, + }, + Keyring: PolicyDeny, + KeyPrefixes: []*KeyRule{ + { + Prefix: "", + Policy: PolicyRead, + }, + { + Prefix: "foo/", + Policy: PolicyWrite, + }, + { + Prefix: "foo/bar/", + Policy: PolicyRead, + }, + }, + Keys: []*KeyRule{ + { + Prefix: "foo/bar/baz", + Policy: PolicyDeny, + }, + }, + NodePrefixes: []*NodeRule{ + { + Name: "", + Policy: PolicyRead, + }, + }, + Nodes: []*NodeRule{ + { + Name: "foo", + Policy: PolicyWrite, + }, + { + Name: "bar", + Policy: PolicyDeny, + }, + }, + Operator: PolicyDeny, + Mesh: PolicyDeny, + PreparedQueryPrefixes: []*PreparedQueryRule{ + { + Prefix: "", + Policy: PolicyRead, + }, + }, + PreparedQueries: []*PreparedQueryRule{ + { + Prefix: "foo", + Policy: PolicyWrite, + }, + { + Prefix: "bar", + Policy: PolicyDeny, + }, + }, + ServicePrefixes: []*ServiceRule{ + { + Name: "", + Policy: PolicyWrite, + }, + }, + Services: []*ServiceRule{ + { + Name: "foo", + Policy: PolicyRead, + }, + }, + SessionPrefixes: []*SessionRule{ + { + Node: "baz", + Policy: PolicyDeny, + }, + }, + Sessions: []*SessionRule{ + { + Node: "foo", + Policy: PolicyWrite, + }, + { + Node: "bar", + Policy: PolicyDeny, + }, + }, + }}, + }, + { + Name: "Legacy Basic", + Syntax: SyntaxLegacy, + Rules: ` + agent "foo" { + policy = "read" + } + agent "bar" { + policy = "write" + } + event "" { + policy = "read" + } + event "foo" { + policy = "write" + } + event "bar" { + policy = "deny" + } + key "" { + policy = "read" + } + key "foo/" { + policy = "write" + } + key "foo/bar/" { + policy = "read" + } + key "foo/bar/baz" { + policy = "deny" + } + keyring = "deny" + node "" { + policy = "read" + } + node "foo" { + policy = "write" + } + node "bar" { + policy = "deny" + } + operator = "deny" + service "" { + policy = "write" + } + service "foo" { + policy = "read" + } + session "foo" { + policy = "write" + } + session "bar" { + policy = "deny" + } + session "baz" { + policy = "deny" + } + query "" { + policy = "read" + } + query "foo" { + policy = "write" + } + query "bar" { + policy = "deny" + } + `, + RulesJSON: ` + { + "agent": { + "foo": { + "policy": "read" + }, + "bar": { + "policy": "write" + } + }, + "event": { + "": { + "policy": "read" + }, + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + }, + "key": { + "": { + "policy": "read" + }, + "foo/": { + "policy": "write" + }, + "foo/bar/": { + "policy": "read" + }, + "foo/bar/baz": { + "policy": "deny" + } + }, + "keyring": "deny", + "node": { + "": { + "policy": "read" + }, + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + }, + "operator": "deny", + "service": { + "": { + "policy": "write" + }, + "foo": { + "policy": "read" + } + }, + "session": { + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + }, + "baz": { + "policy": "deny" + } + }, + "query": { + "": { + "policy": "read" + }, + "foo": { + "policy": "write" + }, + "bar": { + "policy": "deny" + } + } + } + `, + Expected: &Policy{PolicyRules: PolicyRules{ AgentPrefixes: []*AgentRule{ { Node: "foo", @@ -179,193 +538,19 @@ func TestPolicySourceParse(t *testing.T) { Node: "bar", Policy: PolicyDeny, }, - }, - }}, - "", - }, - { - "Legacy (JSON)", - SyntaxLegacy, - ljoin( - `{ `, - ` "agent": { `, - ` "foo": { `, - ` "policy": "write" `, - ` }, `, - ` "bar": { `, - ` "policy": "deny" `, - ` } `, - ` }, `, - ` "event": { `, - ` "": { `, - ` "policy": "read" `, - ` }, `, - ` "foo": { `, - ` "policy": "write" `, - ` }, `, - ` "bar": { `, - ` "policy": "deny" `, - ` } `, - ` }, `, - ` "key": { `, - ` "": { `, - ` "policy": "read" `, - ` }, `, - ` "foo/": { `, - ` "policy": "write" `, - ` }, `, - ` "foo/bar/": { `, - ` "policy": "read" `, - ` }, `, - ` "foo/bar/baz": { `, - ` "policy": "deny" `, - ` } `, - ` }, `, - ` "keyring": "deny", `, - ` "node": { `, - ` "": { `, - ` "policy": "read" `, - ` }, `, - ` "foo": { `, - ` "policy": "write" `, - ` }, `, - ` "bar": { `, - ` "policy": "deny" `, - ` } `, - ` }, `, - ` "operator": "deny", `, - ` "query": { `, - ` "": { `, - ` "policy": "read" `, - ` }, `, - ` "foo": { `, - ` "policy": "write" `, - ` }, `, - ` "bar": { `, - ` "policy": "deny" `, - ` } `, - ` }, `, - ` "service": { `, - ` "": { `, - ` "policy": "write" `, - ` }, `, - ` "foo": { `, - ` "policy": "read" `, - ` } `, - ` }, `, - ` "session": { `, - ` "foo": { `, - ` "policy": "write" `, - ` }, `, - ` "bar": { `, - ` "policy": "deny" `, - ` } `, - ` } `, - `} `), - &Policy{PolicyRules: PolicyRules{ - AgentPrefixes: []*AgentRule{ { - Node: "foo", - Policy: PolicyWrite, - }, - { - Node: "bar", - Policy: PolicyDeny, - }, - }, - EventPrefixes: []*EventRule{ - { - Event: "", - Policy: PolicyRead, - }, - { - Event: "foo", - Policy: PolicyWrite, - }, - { - Event: "bar", - Policy: PolicyDeny, - }, - }, - Keyring: PolicyDeny, - KeyPrefixes: []*KeyRule{ - { - Prefix: "", - Policy: PolicyRead, - }, - { - Prefix: "foo/", - Policy: PolicyWrite, - }, - { - Prefix: "foo/bar/", - Policy: PolicyRead, - }, - { - Prefix: "foo/bar/baz", - Policy: PolicyDeny, - }, - }, - NodePrefixes: []*NodeRule{ - { - Name: "", - Policy: PolicyRead, - }, - { - Name: "foo", - Policy: PolicyWrite, - }, - { - Name: "bar", - Policy: PolicyDeny, - }, - }, - Operator: PolicyDeny, - PreparedQueryPrefixes: []*PreparedQueryRule{ - { - Prefix: "", - Policy: PolicyRead, - }, - { - Prefix: "foo", - Policy: PolicyWrite, - }, - { - Prefix: "bar", - Policy: PolicyDeny, - }, - }, - ServicePrefixes: []*ServiceRule{ - { - Name: "", - Policy: PolicyWrite, - }, - { - Name: "foo", - Policy: PolicyRead, - }, - }, - SessionPrefixes: []*SessionRule{ - { - Node: "foo", - Policy: PolicyWrite, - }, - { - Node: "bar", + Node: "baz", Policy: PolicyDeny, }, }, }}, - "", }, { - "Service No Intentions (Legacy)", - SyntaxLegacy, - ljoin( - `service "foo" { `, - ` policy = "write"`, - `} `), - &Policy{PolicyRules: PolicyRules{ + Name: "Service No Intentions (Legacy)", + Syntax: SyntaxLegacy, + Rules: `service "foo" { policy = "write" }`, + RulesJSON: `{ "service": { "foo": { "policy": "write" }}}`, + Expected: &Policy{PolicyRules: PolicyRules{ ServicePrefixes: []*ServiceRule{ { Name: "foo", @@ -373,17 +558,13 @@ func TestPolicySourceParse(t *testing.T) { }, }, }}, - "", }, { - "Service Intentions (Legacy)", - SyntaxLegacy, - ljoin( - `service "foo" { `, - ` policy = "write" `, - ` intentions = "read"`, - `} `), - &Policy{PolicyRules: PolicyRules{ + Name: "Service Intentions (Legacy)", + Syntax: SyntaxLegacy, + Rules: `service "foo" { policy = "write" intentions = "read" }`, + RulesJSON: `{ "service": { "foo": { "policy": "write", "intentions": "read" }}}`, + Expected: &Policy{PolicyRules: PolicyRules{ ServicePrefixes: []*ServiceRule{ { Name: "foo", @@ -392,163 +573,221 @@ func TestPolicySourceParse(t *testing.T) { }, }, }}, - "", }, { - "Service Intention: invalid value (Legacy)", - SyntaxLegacy, - ljoin( - `service "foo" { `, - ` policy = "write" `, - ` intentions = "foo"`, - `} `), - nil, - "Invalid service intentions policy", + Name: "Service Intention: invalid value (Legacy)", + Syntax: SyntaxLegacy, + Rules: `service "foo" { policy = "write" intentions = "foo" }`, + RulesJSON: `{ "service": { "foo": { "policy": "write", "intentions": "foo" }}}`, + Err: "Invalid service intentions policy", }, { - "Bad Policy - ACL", - - SyntaxCurrent, - `acl = "list"`, // there is no list policy but this helps to exercise another check in isPolicyValid - nil, - "Invalid acl policy", + Name: "Service No Intentions", + Syntax: SyntaxCurrent, + Rules: `service "foo" { policy = "write" }`, + RulesJSON: `{ "service": { "foo": { "policy": "write" }}}`, + Expected: &Policy{PolicyRules: PolicyRules{ + Services: []*ServiceRule{ + { + Name: "foo", + Policy: "write", + }, + }, + }}, }, { - "Bad Policy - Agent", - SyntaxCurrent, - `agent "foo" { policy = "nope" }`, - nil, - "Invalid agent policy", + Name: "Service Intentions", + Syntax: SyntaxCurrent, + Rules: `service "foo" { policy = "write" intentions = "read" }`, + RulesJSON: `{ "service": { "foo": { "policy": "write", "intentions": "read" }}}`, + Expected: &Policy{PolicyRules: PolicyRules{ + Services: []*ServiceRule{ + { + Name: "foo", + Policy: "write", + Intentions: "read", + }, + }, + }}, }, { - "Bad Policy - Agent Prefix", - SyntaxCurrent, - `agent_prefix "foo" { policy = "nope" }`, - nil, - "Invalid agent_prefix policy", + Name: "Service Intention: invalid value", + Syntax: SyntaxCurrent, + Rules: `service "foo" { policy = "write" intentions = "foo" }`, + RulesJSON: `{ "service": { "foo": { "policy": "write", "intentions": "foo" }}}`, + Err: "Invalid service intentions policy", }, { - "Bad Policy - Key", - SyntaxCurrent, - `key "foo" { policy = "nope" }`, - nil, - "Invalid key policy", + Name: "Bad Policy - ACL", + Syntax: SyntaxCurrent, + Rules: `acl = "list"`, // there is no list policy but this helps to exercise another check in isPolicyValid + RulesJSON: `{ "acl": "list" }`, // there is no list policy but this helps to exercise another check in isPolicyValid + Err: "Invalid acl policy", }, { - "Bad Policy - Key Prefix", - SyntaxCurrent, - `key_prefix "foo" { policy = "nope" }`, - nil, - "Invalid key_prefix policy", + Name: "Bad Policy - Agent", + Syntax: SyntaxCurrent, + Rules: `agent "foo" { policy = "nope" }`, + RulesJSON: `{ "agent": { "foo": { "policy": "nope" }}}`, + Err: "Invalid agent policy", }, { - "Bad Policy - Node", - SyntaxCurrent, - `node "foo" { policy = "nope" }`, - nil, - "Invalid node policy", + Name: "Bad Policy - Agent Prefix", + Syntax: SyntaxCurrent, + Rules: `agent_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "agent_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid agent_prefix policy", }, { - "Bad Policy - Node Prefix", - SyntaxCurrent, - `node_prefix "foo" { policy = "nope" }`, - nil, - "Invalid node_prefix policy", + Name: "Bad Policy - Key", + Syntax: SyntaxCurrent, + Rules: `key "foo" { policy = "nope" }`, + RulesJSON: `{ "key": { "foo": { "policy": "nope" }}}`, + Err: "Invalid key policy", }, { - "Bad Policy - Service", - SyntaxCurrent, - `service "foo" { policy = "nope" }`, - nil, - "Invalid service policy", + Name: "Bad Policy - Key Prefix", + Syntax: SyntaxCurrent, + Rules: `key_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "key_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid key_prefix policy", }, { - "Bad Policy - Service Prefix", - SyntaxCurrent, - `service_prefix "foo" { policy = "nope" }`, - nil, - "Invalid service_prefix policy", + Name: "Bad Policy - Node", + Syntax: SyntaxCurrent, + Rules: `node "foo" { policy = "nope" }`, + RulesJSON: `{ "node": { "foo": { "policy": "nope" }}}`, + Err: "Invalid node policy", }, { - "Bad Policy - Session", - SyntaxCurrent, - `session "foo" { policy = "nope" }`, - nil, - "Invalid session policy", + Name: "Bad Policy - Node Prefix", + Syntax: SyntaxCurrent, + Rules: `node_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "node_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid node_prefix policy", }, { - "Bad Policy - Session Prefix", - SyntaxCurrent, - `session_prefix "foo" { policy = "nope" }`, - nil, - "Invalid session_prefix policy", + Name: "Bad Policy - Service", + Syntax: SyntaxCurrent, + Rules: `service "foo" { policy = "nope" }`, + RulesJSON: `{ "service": { "foo": { "policy": "nope" }}}`, + Err: "Invalid service policy", }, { - "Bad Policy - Event", - SyntaxCurrent, - `event "foo" { policy = "nope" }`, - nil, - "Invalid event policy", + Name: "Bad Policy - Service Prefix", + Syntax: SyntaxCurrent, + Rules: `service_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "service_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid service_prefix policy", }, { - "Bad Policy - Event Prefix", - SyntaxCurrent, - `event_prefix "foo" { policy = "nope" }`, - nil, - "Invalid event_prefix policy", + Name: "Bad Policy - Session", + Syntax: SyntaxCurrent, + Rules: `session "foo" { policy = "nope" }`, + RulesJSON: `{ "session": { "foo": { "policy": "nope" }}}`, + Err: "Invalid session policy", }, { - "Bad Policy - Prepared Query", - SyntaxCurrent, - `query "foo" { policy = "nope" }`, - nil, - "Invalid query policy", + Name: "Bad Policy - Session Prefix", + Syntax: SyntaxCurrent, + Rules: `session_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "session_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid session_prefix policy", }, { - "Bad Policy - Prepared Query Prefix", - SyntaxCurrent, - `query_prefix "foo" { policy = "nope" }`, - nil, - "Invalid query_prefix policy", + Name: "Bad Policy - Event", + Syntax: SyntaxCurrent, + Rules: `event "foo" { policy = "nope" }`, + RulesJSON: `{ "event": { "foo": { "policy": "nope" }}}`, + Err: "Invalid event policy", }, { - "Bad Policy - Keyring", - SyntaxCurrent, - `keyring = "nope"`, - nil, - "Invalid keyring policy", + Name: "Bad Policy - Event Prefix", + Syntax: SyntaxCurrent, + Rules: `event_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "event_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid event_prefix policy", }, { - "Bad Policy - Operator", - SyntaxCurrent, - `operator = "nope"`, - nil, - "Invalid operator policy", + Name: "Bad Policy - Prepared Query", + Syntax: SyntaxCurrent, + Rules: `query "foo" { policy = "nope" }`, + RulesJSON: `{ "query": { "foo": { "policy": "nope" }}}`, + Err: "Invalid query policy", }, { - "Keyring Empty", - SyntaxCurrent, - `keyring = ""`, - &Policy{PolicyRules: PolicyRules{Keyring: ""}}, - "", + Name: "Bad Policy - Prepared Query Prefix", + Syntax: SyntaxCurrent, + Rules: `query_prefix "foo" { policy = "nope" }`, + RulesJSON: `{ "query_prefix": { "foo": { "policy": "nope" }}}`, + Err: "Invalid query_prefix policy", }, { - "Operator Empty", - SyntaxCurrent, - `operator = ""`, - &Policy{PolicyRules: PolicyRules{Operator: ""}}, - "", + Name: "Bad Policy - Keyring", + Syntax: SyntaxCurrent, + Rules: `keyring = "nope"`, + RulesJSON: `{ "keyring": "nope" }`, + Err: "Invalid keyring policy", + }, + { + Name: "Bad Policy - Operator", + Syntax: SyntaxCurrent, + Rules: `operator = "nope"`, + RulesJSON: `{ "operator": "nope" }`, + Err: "Invalid operator policy", + }, + { + Name: "Bad Policy - Mesh", + Syntax: SyntaxCurrent, + Rules: `mesh = "nope"`, + RulesJSON: `{ "mesh": "nope" }`, + Err: "Invalid mesh policy", + }, + { + Name: "Keyring Empty", + Syntax: SyntaxCurrent, + Rules: `keyring = ""`, + RulesJSON: `{ "keyring": "" }`, + Expected: &Policy{PolicyRules: PolicyRules{Keyring: ""}}, + }, + { + Name: "Operator Empty", + Syntax: SyntaxCurrent, + Rules: `operator = ""`, + RulesJSON: `{ "operator": "" }`, + Expected: &Policy{PolicyRules: PolicyRules{Operator: ""}}, + }, + { + Name: "Mesh Empty", + Syntax: SyntaxCurrent, + Rules: `mesh = ""`, + RulesJSON: `{ "mesh": "" }`, + Expected: &Policy{PolicyRules: PolicyRules{Mesh: ""}}, }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { - req := require.New(t) - actual, err := NewPolicyFromSource("", 0, tc.Rules, tc.Syntax, nil, nil) - if tc.Err != "" { - errStartsWith(t, err, tc.Err) - } else { - req.Equal(tc.Expected, actual) + require.True(t, tc.Rules != "" || tc.RulesJSON != "") + if tc.Rules != "" { + t.Run("hcl", func(t *testing.T) { + actual, err := NewPolicyFromSource("", 0, tc.Rules, tc.Syntax, nil, nil) + if tc.Err != "" { + errStartsWith(t, err, tc.Err) + } else { + require.Equal(t, tc.Expected, actual) + } + }) + } + if tc.RulesJSON != "" { + t.Run("json", func(t *testing.T) { + actual, err := NewPolicyFromSource("", 0, tc.RulesJSON, tc.Syntax, nil, nil) + if tc.Err != "" { + errStartsWith(t, err, tc.Err) + } else { + require.Equal(t, tc.Expected, actual) + } + }) } }) } @@ -1218,17 +1457,20 @@ func TestMergePolicies(t *testing.T) { ACL: PolicyRead, Keyring: PolicyRead, Operator: PolicyRead, + Mesh: PolicyRead, }}, {PolicyRules: PolicyRules{ ACL: PolicyWrite, Keyring: PolicyWrite, Operator: PolicyWrite, + Mesh: PolicyWrite, }}, }, expected: &Policy{PolicyRules: PolicyRules{ ACL: PolicyWrite, Keyring: PolicyWrite, Operator: PolicyWrite, + Mesh: PolicyWrite, }}, }, { @@ -1238,17 +1480,20 @@ func TestMergePolicies(t *testing.T) { ACL: PolicyWrite, Keyring: PolicyWrite, Operator: PolicyWrite, + Mesh: PolicyWrite, }}, {PolicyRules: PolicyRules{ ACL: PolicyDeny, Keyring: PolicyDeny, Operator: PolicyDeny, + Mesh: PolicyDeny, }}, }, expected: &Policy{PolicyRules: PolicyRules{ ACL: PolicyDeny, Keyring: PolicyDeny, Operator: PolicyDeny, + Mesh: PolicyDeny, }}, }, { @@ -1258,6 +1503,7 @@ func TestMergePolicies(t *testing.T) { ACL: PolicyRead, Keyring: PolicyRead, Operator: PolicyRead, + Mesh: PolicyRead, }}, {}, }, @@ -1265,6 +1511,7 @@ func TestMergePolicies(t *testing.T) { ACL: PolicyRead, Keyring: PolicyRead, Operator: PolicyRead, + Mesh: PolicyRead, }}, }, } @@ -1278,6 +1525,7 @@ func TestMergePolicies(t *testing.T) { req.Equal(exp.ACL, act.ACL) req.Equal(exp.Keyring, act.Keyring) req.Equal(exp.Operator, act.Operator) + req.Equal(exp.Mesh, act.Mesh) req.ElementsMatch(exp.Agents, act.Agents) req.ElementsMatch(exp.AgentPrefixes, act.AgentPrefixes) req.ElementsMatch(exp.Events, act.Events) @@ -1348,6 +1596,9 @@ keyring = "write" # comment operator = "write" + +# comment +mesh = "write" ` expected := ` @@ -1400,6 +1651,9 @@ keyring = "write" # comment operator = "write" + +# comment +mesh = "write" ` output, err := TranslateLegacyRules([]byte(input)) diff --git a/acl/static_authorizer.go b/acl/static_authorizer.go index 4523f0636f..5074bc4b5f 100644 --- a/acl/static_authorizer.go +++ b/acl/static_authorizer.go @@ -156,6 +156,20 @@ func (s *staticAuthorizer) NodeWrite(string, *AuthorizerContext) EnforcementDeci return Deny } +func (s *staticAuthorizer) MeshRead(*AuthorizerContext) EnforcementDecision { + if s.defaultAllow { + return Allow + } + return Deny +} + +func (s *staticAuthorizer) MeshWrite(*AuthorizerContext) EnforcementDecision { + if s.defaultAllow { + return Allow + } + return Deny +} + func (s *staticAuthorizer) OperatorRead(*AuthorizerContext) EnforcementDecision { if s.defaultAllow { return Allow diff --git a/agent/acl_endpoint_test.go b/agent/acl_endpoint_test.go index 15d8ce4a04..0debb7da80 100644 --- a/agent/acl_endpoint_test.go +++ b/agent/acl_endpoint_test.go @@ -1963,6 +1963,14 @@ func TestACL_Authorize(t *testing.T) { Resource: "operator", Access: "write", }, + { + Resource: "mesh", + Access: "read", + }, + { + Resource: "mesh", + Access: "write", + }, { Resource: "query", Segment: "foo", @@ -2097,6 +2105,14 @@ func TestACL_Authorize(t *testing.T) { Resource: "operator", Access: "write", }, + { + Resource: "mesh", + Access: "read", + }, + { + Resource: "mesh", + Access: "write", + }, { Resource: "query", Segment: "foo", @@ -2147,6 +2163,8 @@ func TestACL_Authorize(t *testing.T) { true, // node:write true, // operator:read true, // operator:write + true, // mesh:read + true, // mesh:write false, // query:read false, // query:write true, // service:read diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index e30013ddfd..82c44ffe5a 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -584,6 +584,7 @@ func (s *HTTPHandlers) AgentForceLeave(resp http.ResponseWriter, req *http.Reque if err != nil { return nil, err } + // TODO(partitions): should this be possible in a partition? if authz.OperatorWrite(nil) != acl.Allow { return nil, acl.ErrPermissionDenied } @@ -1536,6 +1537,7 @@ func (s *HTTPHandlers) AgentHost(resp http.ResponseWriter, req *http.Request) (i return nil, err } + // TODO(partitions): should this be possible in a partition? if authz.OperatorRead(nil) != acl.Allow { return nil, acl.ErrPermissionDenied } diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index c6939b1466..37d9494cef 100644 --- a/agent/consul/acl_endpoint.go +++ b/agent/consul/acl_endpoint.go @@ -1379,6 +1379,8 @@ func makeACLETag(parent string, policy *acl.Policy) string { // GetPolicy is used to retrieve a compiled policy object with a TTL. Does not // support a blocking query. +// +// TODO(ACL-Legacy): remove this func (a *ACL) GetPolicy(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error { if done, err := a.srv.ForwardRPC("ACL.GetPolicy", args, reply); done { return err diff --git a/agent/consul/acl_test.go b/agent/consul/acl_test.go index c6c2ac7c07..5f2f8b0805 100644 --- a/agent/consul/acl_test.go +++ b/agent/consul/acl_test.go @@ -1906,6 +1906,7 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega authz, err := r.ResolveToken("legacy-client") require.NoError(t, err) require.NotNil(t, authz) + require.Equal(t, acl.Deny, authz.MeshRead(nil)) require.Equal(t, acl.Deny, authz.OperatorRead(nil)) require.Equal(t, acl.Allow, authz.ServiceRead("foo", nil)) }) diff --git a/agent/http.go b/agent/http.go index 735cad81bf..02e95e6ba1 100644 --- a/agent/http.go +++ b/agent/http.go @@ -253,6 +253,7 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler { // If the token provided does not have the necessary permissions, // write a forbidden response + // TODO(partitions): should this be possible in a partition? if authz.OperatorRead(nil) != acl.Allow { resp.WriteHeader(http.StatusForbidden) return diff --git a/agent/structs/acl.go b/agent/structs/acl.go index 84bfacdaa2..50d51647cf 100644 --- a/agent/structs/acl.go +++ b/agent/structs/acl.go @@ -73,6 +73,7 @@ node_prefix "" { policy = "write" } operator = "write" +mesh = "write" query_prefix "" { policy = "write" } diff --git a/agent/structs/config_entry.go b/agent/structs/config_entry.go index 94db3c1a56..114881ee0e 100644 --- a/agent/structs/config_entry.go +++ b/agent/structs/config_entry.go @@ -315,7 +315,7 @@ func (e *ProxyConfigEntry) CanRead(authz acl.Authorizer) bool { func (e *ProxyConfigEntry) CanWrite(authz acl.Authorizer) bool { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) - return authz.OperatorWrite(&authzContext) == acl.Allow + return authz.MeshWrite(&authzContext) == acl.Allow } func (e *ProxyConfigEntry) GetRaftIndex() *RaftIndex { diff --git a/agent/structs/config_entry_discoverychain_test.go b/agent/structs/config_entry_discoverychain_test.go index 09ca4eaa66..b1311991c2 100644 --- a/agent/structs/config_entry_discoverychain_test.go +++ b/agent/structs/config_entry_discoverychain_test.go @@ -7,14 +7,24 @@ import ( "testing" "time" - "github.com/hashicorp/consul/acl" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/acl" ) func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { // This test tests both of these because they are related functions. + newAuthz := func(t *testing.T, src string) acl.Authorizer { + policy, err := acl.NewPolicyFromSource("", 0, src, acl.SyntaxCurrent, nil, nil) + require.NoError(t, err) + + authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil) + require.NoError(t, err) + return authorizer + } + newServiceACL := func(t *testing.T, canRead, canWrite []string) acl.Authorizer { var buf bytes.Buffer for _, s := range canRead { @@ -32,13 +42,37 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { return authorizer } - type testACL struct { - name string - authorizer acl.Authorizer - canRead bool - canWrite bool + newServiceAndOperatorACL := func(t *testing.T, service, operator string) acl.Authorizer { + switch { + case service != "" && operator != "": + return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q } operator = %q`, service, operator)) + case service == "" && operator != "": + return newAuthz(t, fmt.Sprintf(`operator = %q`, operator)) + case service != "" && operator == "": + return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q }`, service)) + default: + t.Fatalf("one of these should be set") + return nil + } } + newServiceAndMeshACL := func(t *testing.T, service, mesh string) acl.Authorizer { + switch { + case service != "" && mesh != "": + return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q } mesh = %q`, service, mesh)) + case service == "" && mesh != "": + return newAuthz(t, fmt.Sprintf(`mesh = %q`, mesh)) + case service != "" && mesh == "": + return newAuthz(t, fmt.Sprintf(`service "test" { policy = %q }`, service)) + default: + t.Fatalf("one of these should be set") + return nil + } + } + + type testACL = configEntryTestACL + type testcase = configEntryACLTestCase + defaultDenyCase := testACL{ name: "deny", authorizer: newServiceACL(t, nil, nil), @@ -64,12 +98,7 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { canWrite: false, } - for _, tc := range []struct { - name string - entry discoveryChainConfigEntry - expectServices []ServiceID - expectACLs []testACL - }{ + cases := []testcase{ { name: "resolver: self", entry: &ServiceResolverConfigEntry{ @@ -226,25 +255,261 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { }, }, }, - } { - tc := tc - t.Run(tc.name, func(t *testing.T) { - // sanity check inputs - require.NoError(t, tc.entry.Normalize()) - require.NoError(t, tc.entry.Validate()) + { + name: "ingress-gateway", + entry: &IngressGatewayConfigEntry{Name: "test"}, + expectACLs: []testACL{ + { + name: "no-authz", + authorizer: newAuthz(t, ``), + canRead: false, + canWrite: false, + }, - got := tc.entry.ListRelatedServices() - require.Equal(t, tc.expectServices, got) + { + name: "service deny and operator deny", + authorizer: newServiceAndOperatorACL(t, "deny", "deny"), + canRead: false, + canWrite: false, + }, + { + name: "service read and operator deny", + authorizer: newServiceAndOperatorACL(t, "read", "deny"), + canRead: true, + canWrite: false, + }, + { + name: "service write and operator deny", + authorizer: newServiceAndOperatorACL(t, "write", "deny"), + canRead: true, + canWrite: false, + }, - for _, a := range tc.expectACLs { - a := a - t.Run(a.name, func(t *testing.T) { - require.Equal(t, a.canRead, tc.entry.CanRead(a.authorizer)) - require.Equal(t, a.canWrite, tc.entry.CanWrite(a.authorizer)) - }) - } - }) + { + name: "service deny and mesh deny", + authorizer: newServiceAndMeshACL(t, "deny", "deny"), + canRead: false, + canWrite: false, + }, + { + name: "service read and mesh deny", + authorizer: newServiceAndMeshACL(t, "read", "deny"), + canRead: true, + canWrite: false, + }, + { + name: "service write and mesh deny", + authorizer: newServiceAndMeshACL(t, "write", "deny"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and operator read", + authorizer: newServiceAndOperatorACL(t, "deny", "read"), + canRead: false, + canWrite: false, + }, + { + name: "service read and operator read", + authorizer: newServiceAndOperatorACL(t, "read", "read"), + canRead: true, + canWrite: false, + }, + { + name: "service write and operator read", + authorizer: newServiceAndOperatorACL(t, "write", "read"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and operator write", + authorizer: newServiceAndOperatorACL(t, "deny", "write"), + canRead: false, + canWrite: true, + }, + { + name: "service read and operator write", + authorizer: newServiceAndOperatorACL(t, "read", "write"), + canRead: true, + canWrite: true, + }, + { + name: "service write and operator write", + authorizer: newServiceAndOperatorACL(t, "write", "write"), + canRead: true, + canWrite: true, + }, + + { + name: "service deny and mesh read", + authorizer: newServiceAndMeshACL(t, "deny", "read"), + canRead: false, + canWrite: false, + }, + { + name: "service read and mesh read", + authorizer: newServiceAndMeshACL(t, "read", "read"), + canRead: true, + canWrite: false, + }, + { + name: "service write and mesh read", + authorizer: newServiceAndMeshACL(t, "write", "read"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and mesh write", + authorizer: newServiceAndMeshACL(t, "deny", "write"), + canRead: false, + canWrite: true, + }, + { + name: "service read and mesh write", + authorizer: newServiceAndMeshACL(t, "read", "write"), + canRead: true, + canWrite: true, + }, + { + name: "service write and mesh write", + authorizer: newServiceAndMeshACL(t, "write", "write"), + canRead: true, + canWrite: true, + }, + }, + }, + { + name: "terminating-gateway", + entry: &TerminatingGatewayConfigEntry{Name: "test"}, + expectACLs: []testACL{ + { + name: "no-authz", + authorizer: newAuthz(t, ``), + canRead: false, + canWrite: false, + }, + + { + name: "service deny and operator deny", + authorizer: newServiceAndOperatorACL(t, "deny", "deny"), + canRead: false, + canWrite: false, + }, + { + name: "service read and operator deny", + authorizer: newServiceAndOperatorACL(t, "read", "deny"), + canRead: true, + canWrite: false, + }, + { + name: "service write and operator deny", + authorizer: newServiceAndOperatorACL(t, "write", "deny"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and mesh deny", + authorizer: newServiceAndMeshACL(t, "deny", "deny"), + canRead: false, + canWrite: false, + }, + { + name: "service read and mesh deny", + authorizer: newServiceAndMeshACL(t, "read", "deny"), + canRead: true, + canWrite: false, + }, + { + name: "service write and mesh deny", + authorizer: newServiceAndMeshACL(t, "write", "deny"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and operator read", + authorizer: newServiceAndOperatorACL(t, "deny", "read"), + canRead: false, + canWrite: false, + }, + { + name: "service read and operator read", + authorizer: newServiceAndOperatorACL(t, "read", "read"), + canRead: true, + canWrite: false, + }, + { + name: "service write and operator read", + authorizer: newServiceAndOperatorACL(t, "write", "read"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and operator write", + authorizer: newServiceAndOperatorACL(t, "deny", "write"), + canRead: false, + canWrite: true, + }, + { + name: "service read and operator write", + authorizer: newServiceAndOperatorACL(t, "read", "write"), + canRead: true, + canWrite: true, + }, + { + name: "service write and operator write", + authorizer: newServiceAndOperatorACL(t, "write", "write"), + canRead: true, + canWrite: true, + }, + + { + name: "service deny and mesh read", + authorizer: newServiceAndMeshACL(t, "deny", "read"), + canRead: false, + canWrite: false, + }, + { + name: "service read and mesh read", + authorizer: newServiceAndMeshACL(t, "read", "read"), + canRead: true, + canWrite: false, + }, + { + name: "service write and mesh read", + authorizer: newServiceAndMeshACL(t, "write", "read"), + canRead: true, + canWrite: false, + }, + + { + name: "service deny and mesh write", + authorizer: newServiceAndMeshACL(t, "deny", "write"), + canRead: false, + canWrite: true, + }, + { + name: "service read and mesh write", + authorizer: newServiceAndMeshACL(t, "read", "write"), + canRead: true, + canWrite: true, + }, + { + name: "service write and mesh write", + authorizer: newServiceAndMeshACL(t, "write", "write"), + canRead: true, + canWrite: true, + }, + }, + }, } + + testConfigEntries_ListRelatedServices_AndACLs(t, cases) } func TestServiceResolverConfigEntry(t *testing.T) { diff --git a/agent/structs/config_entry_gateways.go b/agent/structs/config_entry_gateways.go index 319287dbf8..551d2c2aba 100644 --- a/agent/structs/config_entry_gateways.go +++ b/agent/structs/config_entry_gateways.go @@ -271,7 +271,7 @@ func (e *IngressGatewayConfigEntry) CanRead(authz acl.Authorizer) bool { func (e *IngressGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) - return authz.OperatorWrite(&authzContext) == acl.Allow + return authz.MeshWrite(&authzContext) == acl.Allow } func (e *IngressGatewayConfigEntry) GetRaftIndex() *RaftIndex { @@ -407,15 +407,13 @@ func (e *TerminatingGatewayConfigEntry) Validate() error { func (e *TerminatingGatewayConfigEntry) CanRead(authz acl.Authorizer) bool { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) - return authz.ServiceRead(e.Name, &authzContext) == acl.Allow } func (e *TerminatingGatewayConfigEntry) CanWrite(authz acl.Authorizer) bool { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) - - return authz.OperatorWrite(&authzContext) == acl.Allow + return authz.MeshWrite(&authzContext) == acl.Allow } func (e *TerminatingGatewayConfigEntry) GetRaftIndex() *RaftIndex { diff --git a/agent/structs/config_entry_mesh.go b/agent/structs/config_entry_mesh.go index 9f32c101ca..2e405a2b81 100644 --- a/agent/structs/config_entry_mesh.go +++ b/agent/structs/config_entry_mesh.go @@ -71,7 +71,7 @@ func (e *MeshConfigEntry) CanRead(authz acl.Authorizer) bool { func (e *MeshConfigEntry) CanWrite(authz acl.Authorizer) bool { var authzContext acl.AuthorizerContext e.FillAuthzContext(&authzContext) - return authz.OperatorWrite(&authzContext) == acl.Allow + return authz.MeshWrite(&authzContext) == acl.Allow } func (e *MeshConfigEntry) GetRaftIndex() *RaftIndex { diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index d76fc3b237..2844ea8c7a 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -11,10 +11,195 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/sdk/testutil" ) +func TestConfigEntries_ACLs(t *testing.T) { + type testACL = configEntryTestACL + type testcase = configEntryACLTestCase + + newAuthz := func(t *testing.T, src string) acl.Authorizer { + policy, err := acl.NewPolicyFromSource("", 0, src, acl.SyntaxCurrent, nil, nil) + require.NoError(t, err) + + authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil) + require.NoError(t, err) + return authorizer + } + + cases := []testcase{ + // =================== proxy-defaults =================== + { + name: "proxy-defaults", + entry: &ProxyConfigEntry{}, + expectACLs: []testACL{ + { + name: "no-authz", + authorizer: newAuthz(t, ``), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "proxy-defaults: operator deny", + authorizer: newAuthz(t, `operator = "deny"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "proxy-defaults: operator read", + authorizer: newAuthz(t, `operator = "read"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "proxy-defaults: operator write", + authorizer: newAuthz(t, `operator = "write"`), + canRead: true, // unauthenticated + canWrite: true, + }, + { + name: "proxy-defaults: mesh deny", + authorizer: newAuthz(t, `mesh = "deny"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "proxy-defaults: mesh read", + authorizer: newAuthz(t, `mesh = "read"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "proxy-defaults: mesh write", + authorizer: newAuthz(t, `mesh = "write"`), + canRead: true, // unauthenticated + canWrite: true, + }, + { + name: "proxy-defaults: operator deny and mesh read", + authorizer: newAuthz(t, `operator = "deny" mesh = "read"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "proxy-defaults: operator deny and mesh write", + authorizer: newAuthz(t, `operator = "deny" mesh = "write"`), + canRead: true, // unauthenticated + canWrite: true, + }, + }, + }, + // =================== mesh =================== + { + name: "mesh", + entry: &MeshConfigEntry{}, + expectACLs: []testACL{ + { + name: "no-authz", + authorizer: newAuthz(t, ``), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "mesh: operator deny", + authorizer: newAuthz(t, `operator = "deny"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "mesh: operator read", + authorizer: newAuthz(t, `operator = "read"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "mesh: operator write", + authorizer: newAuthz(t, `operator = "write"`), + canRead: true, // unauthenticated + canWrite: true, + }, + { + name: "mesh: mesh deny", + authorizer: newAuthz(t, `mesh = "deny"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "mesh: mesh read", + authorizer: newAuthz(t, `mesh = "read"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "mesh: mesh write", + authorizer: newAuthz(t, `mesh = "write"`), + canRead: true, // unauthenticated + canWrite: true, + }, + { + name: "mesh: operator deny and mesh read", + authorizer: newAuthz(t, `operator = "deny" mesh = "read"`), + canRead: true, // unauthenticated + canWrite: false, + }, + { + name: "mesh: operator deny and mesh write", + authorizer: newAuthz(t, `operator = "deny" mesh = "write"`), + canRead: true, // unauthenticated + canWrite: true, + }, + }, + }, + } + + testConfigEntries_ListRelatedServices_AndACLs(t, cases) +} + +type configEntryTestACL struct { + name string + authorizer acl.Authorizer + canRead bool + canWrite bool +} + +type configEntryACLTestCase struct { + name string + entry ConfigEntry + expectServices []ServiceID // optional + expectACLs []configEntryTestACL +} + +func testConfigEntries_ListRelatedServices_AndACLs(t *testing.T, cases []configEntryACLTestCase) { + // This test tests both of these because they are related functions. + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // verify test inputs + require.NoError(t, tc.entry.Normalize()) + require.NoError(t, tc.entry.Validate()) + + if dce, ok := tc.entry.(discoveryChainConfigEntry); ok { + got := dce.ListRelatedServices() + require.Equal(t, tc.expectServices, got) + } + + if len(tc.expectACLs) == 1 { + a := tc.expectACLs[0] + require.Empty(t, a.name) + } else { + for _, a := range tc.expectACLs { + require.NotEmpty(t, a.name) + t.Run(a.name, func(t *testing.T) { + require.Equal(t, a.canRead, tc.entry.CanRead(a.authorizer), "unexpected CanRead result") + require.Equal(t, a.canWrite, tc.entry.CanWrite(a.authorizer), "unexpected CanWrite result") + }) + } + } + }) + } +} + // TestDecodeConfigEntry is the 'structs' mirror image of // command/config/write/config_write_test.go:TestParseConfigEntry func TestDecodeConfigEntry(t *testing.T) {