package acl import ( "fmt" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) type mockAuthorizer struct { mock.Mock } var _ Authorizer = (*mockAuthorizer)(nil) // ACLRead checks for permission to list all the ACLs func (m *mockAuthorizer) ACLRead(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // ACLWrite checks for permission to manipulate ACLs func (m *mockAuthorizer) ACLWrite(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // AgentRead checks for permission to read from agent endpoints for a // given node. func (m *mockAuthorizer) AgentRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // AgentWrite checks for permission to make changes via agent endpoints // for a given node. func (m *mockAuthorizer) AgentWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // EventRead determines if a specific event can be queried. func (m *mockAuthorizer) EventRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // EventWrite determines if a specific event may be fired. func (m *mockAuthorizer) EventWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // IntentionDefaultAllow determines the default authorized behavior // when no intentions match a Connect request. func (m *mockAuthorizer) IntentionDefaultAllow(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // IntentionRead determines if a specific intention can be read. func (m *mockAuthorizer) IntentionRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // IntentionWrite determines if a specific intention can be // created, modified, or deleted. func (m *mockAuthorizer) IntentionWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // KeyList checks for permission to list keys under a prefix func (m *mockAuthorizer) KeyList(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // KeyRead checks for permission to read a given key func (m *mockAuthorizer) KeyRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // KeyWrite checks for permission to write a given key func (m *mockAuthorizer) KeyWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // KeyWritePrefix checks for permission to write to an // entire key prefix. This means there must be no sub-policies // that deny a write. func (m *mockAuthorizer) KeyWritePrefix(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // KeyringRead determines if the encryption keyring used in // the gossip layer can be read. func (m *mockAuthorizer) KeyringRead(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // KeyringWrite determines if the keyring can be manipulated func (m *mockAuthorizer) KeyringWrite(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // NodeRead checks for permission to read (discover) a given node. func (m *mockAuthorizer) NodeRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } func (m *mockAuthorizer) NodeReadAll(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // NodeWrite checks for permission to create or update (register) a // given node. func (m *mockAuthorizer) NodeWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) 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 { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // OperatorWrite determines if the state-changing Consul operator // functions can be used. func (m *mockAuthorizer) OperatorWrite(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // PreparedQueryRead determines if a specific prepared query can be read // to show its contents (this is not used for execution). func (m *mockAuthorizer) PreparedQueryRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // PreparedQueryWrite determines if a specific prepared query can be // created, modified, or deleted. func (m *mockAuthorizer) PreparedQueryWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // ServiceRead checks for permission to read a given service func (m *mockAuthorizer) ServiceRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } func (m *mockAuthorizer) ServiceReadAll(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // ServiceWrite checks for permission to create or update a given // service func (m *mockAuthorizer) ServiceWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } func (m *mockAuthorizer) ServiceWriteAny(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } // SessionRead checks for permission to read sessions for a given node. func (m *mockAuthorizer) SessionRead(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // SessionWrite checks for permission to create sessions for a given // node. func (m *mockAuthorizer) SessionWrite(segment string, ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(segment, ctx) return ret.Get(0).(EnforcementDecision) } // Snapshot checks for permission to take and restore snapshots. func (m *mockAuthorizer) Snapshot(ctx *AuthorizerContext) EnforcementDecision { ret := m.Called(ctx) return ret.Get(0).(EnforcementDecision) } func TestACL_Enforce(t *testing.T) { type testCase struct { method string resource Resource segment string access string ret EnforcementDecision err string } testName := func(t testCase) string { if t.segment != "" { return fmt.Sprintf("%s/%s/%s/%s", t.resource, t.segment, t.access, t.ret.String()) } return fmt.Sprintf("%s/%s/%s", t.resource, t.access, t.ret.String()) } cases := []testCase{ { method: "ACLRead", resource: ResourceACL, access: "read", ret: Deny, }, { method: "ACLRead", resource: ResourceACL, access: "read", ret: Allow, }, { method: "ACLWrite", resource: ResourceACL, access: "write", ret: Deny, }, { method: "ACLWrite", resource: ResourceACL, access: "write", ret: Allow, }, { resource: ResourceACL, access: "list", ret: Deny, err: "Invalid access level", }, { method: "OperatorRead", resource: ResourceOperator, access: "read", ret: Deny, }, { method: "OperatorRead", resource: ResourceOperator, access: "read", ret: Allow, }, { method: "OperatorWrite", resource: ResourceOperator, access: "write", ret: Deny, }, { method: "OperatorWrite", resource: ResourceOperator, access: "write", ret: Allow, }, { resource: ResourceOperator, access: "list", ret: Deny, err: "Invalid access level", }, { method: "KeyringRead", resource: ResourceKeyring, access: "read", ret: Deny, }, { method: "KeyringRead", resource: ResourceKeyring, access: "read", ret: Allow, }, { method: "KeyringWrite", resource: ResourceKeyring, access: "write", ret: Deny, }, { method: "KeyringWrite", resource: ResourceKeyring, access: "write", ret: Allow, }, { resource: ResourceKeyring, access: "list", ret: Deny, err: "Invalid access level", }, { method: "AgentRead", resource: ResourceAgent, segment: "foo", access: "read", ret: Deny, }, { method: "AgentRead", resource: ResourceAgent, segment: "foo", access: "read", ret: Allow, }, { method: "AgentWrite", resource: ResourceAgent, segment: "foo", access: "write", ret: Deny, }, { method: "AgentWrite", resource: ResourceAgent, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceAgent, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "EventRead", resource: ResourceEvent, segment: "foo", access: "read", ret: Deny, }, { method: "EventRead", resource: ResourceEvent, segment: "foo", access: "read", ret: Allow, }, { method: "EventWrite", resource: ResourceEvent, segment: "foo", access: "write", ret: Deny, }, { method: "EventWrite", resource: ResourceEvent, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceEvent, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "IntentionRead", resource: ResourceIntention, segment: "foo", access: "read", ret: Deny, }, { method: "IntentionRead", resource: ResourceIntention, segment: "foo", access: "read", ret: Allow, }, { method: "IntentionWrite", resource: ResourceIntention, segment: "foo", access: "write", ret: Deny, }, { method: "IntentionWrite", resource: ResourceIntention, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceIntention, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "NodeRead", resource: ResourceNode, segment: "foo", access: "read", ret: Deny, }, { method: "NodeRead", resource: ResourceNode, segment: "foo", access: "read", ret: Allow, }, { method: "NodeWrite", resource: ResourceNode, segment: "foo", access: "write", ret: Deny, }, { method: "NodeWrite", resource: ResourceNode, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceNode, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "PreparedQueryRead", resource: ResourceQuery, segment: "foo", access: "read", ret: Deny, }, { method: "PreparedQueryRead", resource: ResourceQuery, segment: "foo", access: "read", ret: Allow, }, { method: "PreparedQueryWrite", resource: ResourceQuery, segment: "foo", access: "write", ret: Deny, }, { method: "PreparedQueryWrite", resource: ResourceQuery, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceQuery, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "ServiceRead", resource: ResourceService, segment: "foo", access: "read", ret: Deny, }, { method: "ServiceRead", resource: ResourceService, segment: "foo", access: "read", ret: Allow, }, { method: "ServiceWrite", resource: ResourceService, segment: "foo", access: "write", ret: Deny, }, { method: "ServiceWrite", resource: ResourceService, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceSession, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "SessionRead", resource: ResourceSession, segment: "foo", access: "read", ret: Deny, }, { method: "SessionRead", resource: ResourceSession, segment: "foo", access: "read", ret: Allow, }, { method: "SessionWrite", resource: ResourceSession, segment: "foo", access: "write", ret: Deny, }, { method: "SessionWrite", resource: ResourceSession, segment: "foo", access: "write", ret: Allow, }, { resource: ResourceSession, segment: "foo", access: "list", ret: Deny, err: "Invalid access level", }, { method: "KeyRead", resource: ResourceKey, segment: "foo", access: "read", ret: Deny, }, { method: "KeyRead", resource: ResourceKey, segment: "foo", access: "read", ret: Allow, }, { method: "KeyWrite", resource: ResourceKey, segment: "foo", access: "write", ret: Deny, }, { method: "KeyWrite", resource: ResourceKey, segment: "foo", access: "write", ret: Allow, }, { method: "KeyList", resource: ResourceKey, segment: "foo", access: "list", ret: Deny, }, { method: "KeyList", resource: ResourceKey, segment: "foo", access: "list", ret: Allow, }, { resource: ResourceKey, segment: "foo", access: "deny", ret: Deny, err: "Invalid access level", }, { resource: "not-a-real-resource", access: "read", ret: Deny, err: "Invalid ACL resource requested:", }, } for _, tcase := range cases { t.Run(testName(tcase), func(t *testing.T) { m := &mockAuthorizer{} if tcase.err == "" { var nilCtx *AuthorizerContext if tcase.segment != "" { m.On(tcase.method, tcase.segment, nilCtx).Return(tcase.ret) } else { m.On(tcase.method, nilCtx).Return(tcase.ret) } } ret, err := Enforce(m, tcase.resource, tcase.segment, tcase.access, nil) if tcase.err == "" { require.NoError(t, err) } else { require.Error(t, err) require.Contains(t, err.Error(), tcase.err) } require.Equal(t, tcase.ret, ret) m.AssertExpectations(t) }) } }