mirror of https://github.com/status-im/consul.git
[Feature] API: Add a internal endpoint to query for ACL authori… (#6888)
* Implement endpoint to query whether the given token is authorized for a set of operations * Updates to allow for remote ACL authorization via RPC This is only used when making an authorization request to a different datacenter.
This commit is contained in:
parent
1d21635a6b
commit
deb91f3d3c
|
@ -1,5 +1,10 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EnforcementDecision int
|
||||
|
||||
const (
|
||||
|
@ -29,6 +34,22 @@ func (d EnforcementDecision) String() string {
|
|||
}
|
||||
}
|
||||
|
||||
type Resource string
|
||||
|
||||
const (
|
||||
ResourceACL Resource = "acl"
|
||||
ResourceAgent Resource = "agent"
|
||||
ResourceEvent Resource = "event"
|
||||
ResourceIntention Resource = "intention"
|
||||
ResourceKey Resource = "key"
|
||||
ResourceKeyring Resource = "keyring"
|
||||
ResourceNode Resource = "node"
|
||||
ResourceOperator Resource = "operator"
|
||||
ResourceQuery Resource = "query"
|
||||
ResourceService Resource = "service"
|
||||
ResourceSession Resource = "session"
|
||||
)
|
||||
|
||||
// Authorizer is the interface for policy enforcement.
|
||||
type Authorizer interface {
|
||||
// ACLRead checks for permission to list all the ACLs
|
||||
|
@ -126,3 +147,98 @@ type Authorizer interface {
|
|||
// Embedded Interface for Consul Enterprise specific ACL enforcement
|
||||
EnterpriseAuthorizer
|
||||
}
|
||||
|
||||
func Enforce(authz Authorizer, rsc Resource, segment string, access string, ctx *EnterpriseAuthorizerContext) (EnforcementDecision, error) {
|
||||
lowerAccess := strings.ToLower(access)
|
||||
|
||||
switch rsc {
|
||||
case ResourceACL:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.ACLRead(ctx), nil
|
||||
case "write":
|
||||
return authz.ACLWrite(ctx), nil
|
||||
}
|
||||
case ResourceAgent:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.AgentRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.AgentWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourceEvent:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.EventRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.EventWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourceIntention:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.IntentionRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.IntentionWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourceKey:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.KeyRead(segment, ctx), nil
|
||||
case "list":
|
||||
return authz.KeyList(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.KeyWrite(segment, ctx), nil
|
||||
case "write-prefix":
|
||||
return authz.KeyWritePrefix(segment, ctx), nil
|
||||
}
|
||||
case ResourceKeyring:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.KeyringRead(ctx), nil
|
||||
case "write":
|
||||
return authz.KeyringWrite(ctx), nil
|
||||
}
|
||||
case ResourceNode:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.NodeRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.NodeWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourceOperator:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.OperatorRead(ctx), nil
|
||||
case "write":
|
||||
return authz.OperatorWrite(ctx), nil
|
||||
}
|
||||
case ResourceQuery:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.PreparedQueryRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.PreparedQueryWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourceService:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.ServiceRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.ServiceWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourceSession:
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.SessionRead(segment, ctx), nil
|
||||
case "write":
|
||||
return authz.SessionWrite(segment, ctx), nil
|
||||
}
|
||||
default:
|
||||
if processed, decision, err := EnforceEnterprise(authz, rsc, segment, lowerAccess, ctx); processed {
|
||||
return decision, err
|
||||
}
|
||||
return Deny, fmt.Errorf("Invalid ACL resource requested: %q", rsc)
|
||||
}
|
||||
|
||||
return Deny, fmt.Errorf("Invalid access level for %s resource: %s", rsc, access)
|
||||
}
|
||||
|
|
|
@ -7,3 +7,7 @@ type EnterpriseAuthorizerContext struct{}
|
|||
|
||||
// EnterpriseAuthorizer stub interface
|
||||
type EnterpriseAuthorizer interface{}
|
||||
|
||||
func EnforceEnterprise(_ Authorizer, _ Resource, _ string, _ string, _ *EnterpriseAuthorizerContext) (bool, EnforcementDecision, error) {
|
||||
return false, Deny, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,621 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockAuthorizer struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ACLRead checks for permission to list all the ACLs
|
||||
func (m *mockAuthorizer) ACLRead(ctx *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
// ACLWrite checks for permission to manipulate ACLs
|
||||
func (m *mockAuthorizer) ACLWrite(ctx *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
// KeyringWrite determines if the keyring can be manipulated
|
||||
func (m *mockAuthorizer) KeyringWrite(ctx *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(segment, 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 *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(segment, 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(segment, ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
// ServiceWrite checks for permission to create or update a given
|
||||
// service
|
||||
func (m *mockAuthorizer) ServiceWrite(segment string, ctx *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(segment, ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
// SessionRead checks for permission to read sessions for a given node.
|
||||
func (m *mockAuthorizer) SessionRead(segment string, ctx *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) 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 *EnterpriseAuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
func TestACL_Enforce(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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{
|
||||
testCase{
|
||||
method: "ACLRead",
|
||||
resource: ResourceACL,
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "ACLRead",
|
||||
resource: ResourceACL,
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "ACLWrite",
|
||||
resource: ResourceACL,
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "ACLWrite",
|
||||
resource: ResourceACL,
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceACL,
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "OperatorRead",
|
||||
resource: ResourceOperator,
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "OperatorRead",
|
||||
resource: ResourceOperator,
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "OperatorWrite",
|
||||
resource: ResourceOperator,
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "OperatorWrite",
|
||||
resource: ResourceOperator,
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceOperator,
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "KeyringRead",
|
||||
resource: ResourceKeyring,
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyringRead",
|
||||
resource: ResourceKeyring,
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyringWrite",
|
||||
resource: ResourceKeyring,
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyringWrite",
|
||||
resource: ResourceKeyring,
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceKeyring,
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "AgentRead",
|
||||
resource: ResourceAgent,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "AgentRead",
|
||||
resource: ResourceAgent,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "AgentWrite",
|
||||
resource: ResourceAgent,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "AgentWrite",
|
||||
resource: ResourceAgent,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceAgent,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "EventRead",
|
||||
resource: ResourceEvent,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "EventRead",
|
||||
resource: ResourceEvent,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "EventWrite",
|
||||
resource: ResourceEvent,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "EventWrite",
|
||||
resource: ResourceEvent,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceEvent,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "IntentionRead",
|
||||
resource: ResourceIntention,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "IntentionRead",
|
||||
resource: ResourceIntention,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "IntentionWrite",
|
||||
resource: ResourceIntention,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "IntentionWrite",
|
||||
resource: ResourceIntention,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceIntention,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "NodeRead",
|
||||
resource: ResourceNode,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "NodeRead",
|
||||
resource: ResourceNode,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "NodeWrite",
|
||||
resource: ResourceNode,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "NodeWrite",
|
||||
resource: ResourceNode,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceNode,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "PreparedQueryRead",
|
||||
resource: ResourceQuery,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "PreparedQueryRead",
|
||||
resource: ResourceQuery,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "PreparedQueryWrite",
|
||||
resource: ResourceQuery,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "PreparedQueryWrite",
|
||||
resource: ResourceQuery,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceQuery,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "ServiceRead",
|
||||
resource: ResourceService,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "ServiceRead",
|
||||
resource: ResourceService,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "ServiceWrite",
|
||||
resource: ResourceService,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "ServiceWrite",
|
||||
resource: ResourceService,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceSession,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "SessionRead",
|
||||
resource: ResourceSession,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "SessionRead",
|
||||
resource: ResourceSession,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "SessionWrite",
|
||||
resource: ResourceSession,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "SessionWrite",
|
||||
resource: ResourceSession,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceSession,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
method: "KeyRead",
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyRead",
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyWrite",
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyWrite",
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyList",
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Deny,
|
||||
},
|
||||
testCase{
|
||||
method: "KeyList",
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "list",
|
||||
ret: Allow,
|
||||
},
|
||||
testCase{
|
||||
resource: ResourceKey,
|
||||
segment: "foo",
|
||||
access: "deny",
|
||||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
testCase{
|
||||
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 *EnterpriseAuthorizerContext
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -981,7 +981,7 @@ func (s *HTTPServer) ACLLogin(resp http.ResponseWriter, req *http.Request) (inte
|
|||
s.parseEntMeta(req, &args.Auth.EnterpriseMeta)
|
||||
|
||||
if err := decodeBody(req.Body, &args.Auth); err != nil {
|
||||
return nil, BadRequestError{Reason: fmt.Sprintf("Failed to decode request body:: %v", err)}
|
||||
return nil, BadRequestError{Reason: fmt.Sprintf("Failed to decode request body: %v", err)}
|
||||
}
|
||||
|
||||
var out structs.ACLToken
|
||||
|
@ -1027,3 +1027,83 @@ func fixupAuthMethodConfig(method *structs.ACLAuthMethod) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) ACLAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// At first glance it may appear like this endpoint is going to leak security relevant information.
|
||||
// There are a number of reason why this is okay.
|
||||
//
|
||||
// 1. The authorizations performed here are the same as what would be done if other HTTP APIs
|
||||
// were used. This is just a way to see if it would be allowed. In the future when we have
|
||||
// audit logging, these authorization checks will be logged along with those from the real
|
||||
// endpoints. In that respect, you can figure out if you have access just as easily by
|
||||
// attempting to perform the requested operation.
|
||||
// 2. In order to use this API you must have a valid ACL token secret.
|
||||
// 3. Along with #2 you can use the ACL.GetPolicy RPC endpoint which will return a rolled up
|
||||
// set of policy rules showing your tokens effective policy. This RPC endpoint exposes
|
||||
// more information than this one and has been around since before v1.0.0. With that other
|
||||
// endpoint you get to see all things possible rather than having to have a list of things
|
||||
// you may want to do and to request authorizations for each one.
|
||||
// 4. In addition to the legacy ACL.GetPolicy RPC endpoint we have an ACL.PolicyResolve and
|
||||
// ACL.RoleResolve endpoints. These RPC endpoints allow reading roles and policies so long
|
||||
// as the token used for the request is linked with them. This is needed to allow client
|
||||
// agents to pull the policy and roles for a token that they are resolving. The only
|
||||
// alternative to this style of access would be to make every agent use a token
|
||||
// with acl:read privileges for all policy and role resolution requests. Once you have
|
||||
// all the associated policies and roles it would be easy enough to recreate the effective
|
||||
// policy.
|
||||
const maxRequests = 64
|
||||
|
||||
if s.checkACLDisabled(resp, req) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
request := structs.RemoteACLAuthorizationRequest{
|
||||
Datacenter: s.agent.config.Datacenter,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
AllowStale: true,
|
||||
RequireConsistent: false,
|
||||
},
|
||||
}
|
||||
var responses []structs.ACLAuthorizationResponse
|
||||
|
||||
s.parseToken(req, &request.Token)
|
||||
s.parseDC(req, &request.Datacenter)
|
||||
|
||||
if err := decodeBody(req.Body, &request.Requests); err != nil {
|
||||
return nil, BadRequestError{Reason: fmt.Sprintf("Failed to decode request body: %v", err)}
|
||||
}
|
||||
|
||||
if len(request.Requests) > maxRequests {
|
||||
return nil, BadRequestError{Reason: fmt.Sprintf("Refusing to process more than %d authorizations at once", maxRequests)}
|
||||
}
|
||||
|
||||
if len(request.Requests) == 0 {
|
||||
return make([]structs.ACLAuthorizationResponse, 0), nil
|
||||
}
|
||||
|
||||
if request.Datacenter != "" && request.Datacenter != s.agent.config.Datacenter {
|
||||
// when we are targeting a datacenter other than our own then we must issue an RPC
|
||||
// to perform the resolution as it may involve a local token
|
||||
if err := s.agent.RPC("ACL.Authorize", &request, &responses); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
authz, err := s.agent.resolveToken(request.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if authz == nil {
|
||||
return nil, fmt.Errorf("Failed to initialize authorizer")
|
||||
}
|
||||
|
||||
responses, err = structs.CreateACLAuthorizationResponses(authz, request.Requests)
|
||||
if err != nil {
|
||||
return nil, BadRequestError{Reason: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
if responses == nil {
|
||||
responses = make([]structs.ACLAuthorizationResponse, 0)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package agent
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -53,6 +54,7 @@ func TestACL_Disabled_Response(t *testing.T) {
|
|||
{"ACLAuthMethodCRUD", a.srv.ACLAuthMethodCRUD},
|
||||
{"ACLLogin", a.srv.ACLLogin},
|
||||
{"ACLLogout", a.srv.ACLLogout},
|
||||
{"ACLAuthorize", a.srv.ACLAuthorize},
|
||||
}
|
||||
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
||||
for _, tt := range tests {
|
||||
|
@ -1576,3 +1578,445 @@ func TestACL_LoginProcedure_HTTP(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestACL_Authorize(t *testing.T) {
|
||||
t.Parallel()
|
||||
a1 := NewTestAgent(t, t.Name(), TestACLConfigWithParams(nil))
|
||||
defer a1.Shutdown()
|
||||
|
||||
testrpc.WaitForTestAgent(t, a1.RPC, "dc1", testrpc.WithToken(TestDefaultMasterToken))
|
||||
|
||||
policyReq := structs.ACLPolicySetRequest{
|
||||
Policy: structs.ACLPolicy{
|
||||
Name: "test",
|
||||
Rules: `acl = "read" operator = "write" service_prefix "" { policy = "read"} node_prefix "" { policy= "write" } key_prefix "/foo" { policy = "write" } `,
|
||||
},
|
||||
Datacenter: "dc1",
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
var policy structs.ACLPolicy
|
||||
require.NoError(t, a1.RPC("ACL.PolicySet", &policyReq, &policy))
|
||||
|
||||
tokenReq := structs.ACLTokenSetRequest{
|
||||
ACLToken: structs.ACLToken{
|
||||
Policies: []structs.ACLTokenPolicyLink{
|
||||
structs.ACLTokenPolicyLink{
|
||||
ID: policy.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
Datacenter: "dc1",
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
|
||||
var token structs.ACLToken
|
||||
require.NoError(t, a1.RPC("ACL.TokenSet", &tokenReq, &token))
|
||||
|
||||
// secondary also needs to setup a replication token to pull tokens and policies
|
||||
secondaryParams := DefaulTestACLConfigParams()
|
||||
secondaryParams.ReplicationToken = secondaryParams.MasterToken
|
||||
secondaryParams.EnableTokenReplication = true
|
||||
|
||||
a2 := NewTestAgent(t, t.Name(), `datacenter = "dc2" `+TestACLConfigWithParams(secondaryParams))
|
||||
defer a2.Shutdown()
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", a1.Config.SerfPortWAN)
|
||||
_, err := a2.JoinWAN([]string{addr})
|
||||
require.NoError(t, err)
|
||||
|
||||
testrpc.WaitForTestAgent(t, a2.RPC, "dc2", testrpc.WithToken(TestDefaultMasterToken))
|
||||
// this actually ensures a few things. First the dcs got connect okay, secondly that the policy we
|
||||
// are about ready to use in our local token creation exists in the secondary DC
|
||||
testrpc.WaitForACLReplication(t, a2.RPC, "dc2", structs.ACLReplicateTokens, policy.CreateIndex, 1, 0)
|
||||
|
||||
localTokenReq := structs.ACLTokenSetRequest{
|
||||
ACLToken: structs.ACLToken{
|
||||
Policies: []structs.ACLTokenPolicyLink{
|
||||
structs.ACLTokenPolicyLink{
|
||||
ID: policy.ID,
|
||||
},
|
||||
},
|
||||
Local: true,
|
||||
},
|
||||
Datacenter: "dc2",
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
|
||||
var localToken structs.ACLToken
|
||||
require.NoError(t, a2.RPC("ACL.TokenSet", &localTokenReq, &localToken))
|
||||
|
||||
t.Run("master-token", func(t *testing.T) {
|
||||
request := []structs.ACLAuthorizationRequest{
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "acl",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "acl",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "agent",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "agent",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "event",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "event",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "intention",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "intention",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "key",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "key",
|
||||
Segment: "foo",
|
||||
Access: "list",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "key",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "keyring",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "keyring",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "node",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "node",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "operator",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "operator",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "query",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "query",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "service",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "service",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "session",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "session",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
}
|
||||
|
||||
for _, dc := range []string{"dc1", "dc2"} {
|
||||
t.Run(dc, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize?dc="+dc, jsonBody(request))
|
||||
req.Header.Add("X-Consul-Token", TestDefaultMasterToken)
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.NoError(t, err)
|
||||
responses, ok := raw.([]structs.ACLAuthorizationResponse)
|
||||
require.True(t, ok)
|
||||
require.Len(t, responses, len(request))
|
||||
|
||||
for idx, req := range request {
|
||||
resp := responses[idx]
|
||||
|
||||
require.Equal(t, req, resp.ACLAuthorizationRequest)
|
||||
require.True(t, resp.Allow, "should have allowed all access for master token")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
customAuthorizationRequests := []structs.ACLAuthorizationRequest{
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "acl",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "acl",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "agent",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "agent",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "event",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "event",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "intention",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "intention",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "key",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "key",
|
||||
Segment: "foo",
|
||||
Access: "list",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "key",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "keyring",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "keyring",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "node",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "node",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "operator",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "operator",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "query",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "query",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "service",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "service",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "session",
|
||||
Segment: "foo",
|
||||
Access: "read",
|
||||
},
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "session",
|
||||
Segment: "foo",
|
||||
Access: "write",
|
||||
},
|
||||
}
|
||||
|
||||
expectedCustomAuthorizationResponses := []bool{
|
||||
true, // acl:read
|
||||
false, // acl:write
|
||||
false, // agent:read
|
||||
false, // agent:write
|
||||
false, // event:read
|
||||
false, // event:write
|
||||
true, // intention:read
|
||||
false, // intention:write
|
||||
false, // key:read
|
||||
false, // key:list
|
||||
false, // key:write
|
||||
false, // keyring:read
|
||||
false, // keyring:write
|
||||
true, // node:read
|
||||
true, // node:write
|
||||
true, // operator:read
|
||||
true, // operator:write
|
||||
false, // query:read
|
||||
false, // query:write
|
||||
true, // service:read
|
||||
false, // service:write
|
||||
false, // session:read
|
||||
false, // session:write
|
||||
}
|
||||
|
||||
t.Run("custom-token", func(t *testing.T) {
|
||||
for _, dc := range []string{"dc1", "dc2"} {
|
||||
t.Run(dc, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(customAuthorizationRequests))
|
||||
req.Header.Add("X-Consul-Token", token.SecretID)
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.NoError(t, err)
|
||||
responses, ok := raw.([]structs.ACLAuthorizationResponse)
|
||||
require.True(t, ok)
|
||||
require.Len(t, responses, len(customAuthorizationRequests))
|
||||
require.Len(t, responses, len(expectedCustomAuthorizationResponses))
|
||||
|
||||
for idx, req := range customAuthorizationRequests {
|
||||
resp := responses[idx]
|
||||
|
||||
require.Equal(t, req, resp.ACLAuthorizationRequest)
|
||||
require.Equal(t, expectedCustomAuthorizationResponses[idx], resp.Allow, "request %d - %+v returned unexpected response", idx, resp.ACLAuthorizationRequest)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("too-many-requests", func(t *testing.T) {
|
||||
var request []structs.ACLAuthorizationRequest
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
request = append(request, structs.ACLAuthorizationRequest{Resource: "acl", Access: "read"})
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(request))
|
||||
req.Header.Add("X-Consul-Token", token.SecretID)
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "Refusing to process more than 64 authorizations at once")
|
||||
require.Nil(t, raw)
|
||||
})
|
||||
|
||||
t.Run("decode-failure", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(structs.ACLAuthorizationRequest{Resource: "acl", Access: "read"}))
|
||||
req.Header.Add("X-Consul-Token", token.SecretID)
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "Failed to decode request body")
|
||||
require.Nil(t, raw)
|
||||
})
|
||||
|
||||
t.Run("acl-not-found", func(t *testing.T) {
|
||||
request := []structs.ACLAuthorizationRequest{
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "acl",
|
||||
Access: "read",
|
||||
},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(request))
|
||||
req.Header.Add("X-Consul-Token", "d908c0be-22e1-433e-84db-8718e1a019de")
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, acl.ErrNotFound, err)
|
||||
require.Nil(t, raw)
|
||||
})
|
||||
|
||||
t.Run("local-token-in-secondary-dc", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize?dc=dc2", jsonBody(customAuthorizationRequests))
|
||||
req.Header.Add("X-Consul-Token", localToken.SecretID)
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.NoError(t, err)
|
||||
responses, ok := raw.([]structs.ACLAuthorizationResponse)
|
||||
require.True(t, ok)
|
||||
require.Len(t, responses, len(customAuthorizationRequests))
|
||||
require.Len(t, responses, len(expectedCustomAuthorizationResponses))
|
||||
|
||||
for idx, req := range customAuthorizationRequests {
|
||||
resp := responses[idx]
|
||||
|
||||
require.Equal(t, req, resp.ACLAuthorizationRequest)
|
||||
require.Equal(t, expectedCustomAuthorizationResponses[idx], resp.Allow, "request %d - %+v returned unexpected response", idx, resp.ACLAuthorizationRequest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("local-token-wrong-dc", func(t *testing.T) {
|
||||
request := []structs.ACLAuthorizationRequest{
|
||||
structs.ACLAuthorizationRequest{
|
||||
Resource: "acl",
|
||||
Access: "read",
|
||||
},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(request))
|
||||
req.Header.Add("X-Consul-Token", localToken.SecretID)
|
||||
recorder := httptest.NewRecorder()
|
||||
raw, err := a1.srv.ACLAuthorize(recorder, req)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, acl.ErrNotFound, err)
|
||||
require.Nil(t, raw)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2372,3 +2372,28 @@ func (a *ACL) Logout(args *structs.ACLLogoutRequest, reply *bool) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ACL) Authorize(args *structs.RemoteACLAuthorizationRequest, reply *[]structs.ACLAuthorizationResponse) error {
|
||||
if err := a.aclPreCheck(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward("ACL.Authorize", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
authz, err := a.srv.ResolveToken(args.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if authz == nil {
|
||||
return fmt.Errorf("Failed to initialize authorizer")
|
||||
}
|
||||
|
||||
responses, err := structs.CreateACLAuthorizationResponses(authz, args.Requests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*reply = responses
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ func init() {
|
|||
registerEndpoint("/v1/internal/ui/nodes", []string{"GET"}, (*HTTPServer).UINodes)
|
||||
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
|
||||
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)
|
||||
registerEndpoint("/v1/internal/acl/authorize", []string{"POST"}, (*HTTPServer).ACLAuthorize)
|
||||
registerEndpoint("/v1/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).KVSEndpoint)
|
||||
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPServer).OperatorRaftConfiguration)
|
||||
registerEndpoint("/v1/operator/raft/peer", []string{"DELETE"}, (*HTTPServer).OperatorRaftPeer)
|
||||
|
|
|
@ -1562,3 +1562,43 @@ type ACLLogoutRequest struct {
|
|||
func (r *ACLLogoutRequest) RequestDatacenter() string {
|
||||
return r.Datacenter
|
||||
}
|
||||
|
||||
type RemoteACLAuthorizationRequest struct {
|
||||
Datacenter string
|
||||
Requests []ACLAuthorizationRequest
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
type ACLAuthorizationRequest struct {
|
||||
Resource acl.Resource
|
||||
Segment string `json:",omitempty"`
|
||||
Access string
|
||||
EnterpriseMeta
|
||||
}
|
||||
|
||||
type ACLAuthorizationResponse struct {
|
||||
ACLAuthorizationRequest
|
||||
Allow bool
|
||||
}
|
||||
|
||||
func (r *RemoteACLAuthorizationRequest) RequestDatacenter() string {
|
||||
return r.Datacenter
|
||||
}
|
||||
|
||||
func CreateACLAuthorizationResponses(authz acl.Authorizer, requests []ACLAuthorizationRequest) ([]ACLAuthorizationResponse, error) {
|
||||
responses := make([]ACLAuthorizationResponse, len(requests))
|
||||
var ctx acl.EnterpriseAuthorizerContext
|
||||
|
||||
for idx, req := range requests {
|
||||
req.FillAuthzContext(&ctx)
|
||||
decision, err := acl.Enforce(authz, req.Resource, req.Segment, req.Access, &ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses[idx].ACLAuthorizationRequest = req
|
||||
responses[idx].Allow = decision == acl.Allow
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
|
|
@ -480,13 +480,14 @@ const (
|
|||
)
|
||||
|
||||
type TestACLConfigParams struct {
|
||||
PrimaryDatacenter string
|
||||
DefaultPolicy string
|
||||
MasterToken string
|
||||
AgentToken string
|
||||
DefaultToken string
|
||||
AgentMasterToken string
|
||||
ReplicationToken string
|
||||
PrimaryDatacenter string
|
||||
DefaultPolicy string
|
||||
MasterToken string
|
||||
AgentToken string
|
||||
DefaultToken string
|
||||
AgentMasterToken string
|
||||
ReplicationToken string
|
||||
EnableTokenReplication bool
|
||||
}
|
||||
|
||||
func DefaulTestACLConfigParams() *TestACLConfigParams {
|
||||
|
@ -526,6 +527,7 @@ var aclConfigTpl = template.Must(template.New("ACL Config").Parse(`
|
|||
{{if ne .DefaultPolicy ""}}
|
||||
default_policy = "{{ .DefaultPolicy }}"
|
||||
{{end}}
|
||||
enable_token_replication = {{printf "%t" .EnableTokenReplication }}
|
||||
{{if .HasConfiguredTokens }}
|
||||
tokens {
|
||||
{{if ne .MasterToken ""}}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type rpcFn func(string, interface{}, interface{}) error
|
||||
|
@ -142,3 +143,20 @@ func WaitForActiveCARoot(t *testing.T, rpc rpcFn, dc string, expect *structs.CAR
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func WaitForACLReplication(t *testing.T, rpc rpcFn, dc string, expectedReplicationType structs.ACLReplicationType, minPolicyIndex, minTokenIndex, minRoleIndex uint64) {
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
args := structs.DCSpecificRequest{
|
||||
Datacenter: dc,
|
||||
}
|
||||
var reply structs.ACLReplicationStatus
|
||||
|
||||
require.NoError(r, rpc("ACL.ReplicationStatus", &args, &reply))
|
||||
|
||||
require.Equal(r, expectedReplicationType, reply.ReplicationType)
|
||||
require.True(r, reply.Running, "Server not running new replicator yet")
|
||||
require.True(r, reply.ReplicatedIndex >= minPolicyIndex, "Server hasn't replicated enough policies")
|
||||
require.True(r, reply.ReplicatedTokenIndex >= minTokenIndex, "Server hasn't replicated enough tokens")
|
||||
require.True(r, reply.ReplicatedRoleIndex >= minRoleIndex, "Server hasn't replicated enough roles")
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue