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
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type EnforcementDecision int
|
type EnforcementDecision int
|
||||||
|
|
||||||
const (
|
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.
|
// Authorizer is the interface for policy enforcement.
|
||||||
type Authorizer interface {
|
type Authorizer interface {
|
||||||
// ACLRead checks for permission to list all the ACLs
|
// ACLRead checks for permission to list all the ACLs
|
||||||
|
@ -126,3 +147,98 @@ type Authorizer interface {
|
||||||
// Embedded Interface for Consul Enterprise specific ACL enforcement
|
// Embedded Interface for Consul Enterprise specific ACL enforcement
|
||||||
EnterpriseAuthorizer
|
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
|
// EnterpriseAuthorizer stub interface
|
||||||
type EnterpriseAuthorizer 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)
|
s.parseEntMeta(req, &args.Auth.EnterpriseMeta)
|
||||||
|
|
||||||
if err := decodeBody(req.Body, &args.Auth); err != nil {
|
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
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -53,6 +54,7 @@ func TestACL_Disabled_Response(t *testing.T) {
|
||||||
{"ACLAuthMethodCRUD", a.srv.ACLAuthMethodCRUD},
|
{"ACLAuthMethodCRUD", a.srv.ACLAuthMethodCRUD},
|
||||||
{"ACLLogin", a.srv.ACLLogin},
|
{"ACLLogin", a.srv.ACLLogin},
|
||||||
{"ACLLogout", a.srv.ACLLogout},
|
{"ACLLogout", a.srv.ACLLogout},
|
||||||
|
{"ACLAuthorize", a.srv.ACLAuthorize},
|
||||||
}
|
}
|
||||||
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
||||||
for _, tt := range tests {
|
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
|
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/nodes", []string{"GET"}, (*HTTPServer).UINodes)
|
||||||
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
|
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
|
||||||
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)
|
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/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).KVSEndpoint)
|
||||||
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPServer).OperatorRaftConfiguration)
|
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPServer).OperatorRaftConfiguration)
|
||||||
registerEndpoint("/v1/operator/raft/peer", []string{"DELETE"}, (*HTTPServer).OperatorRaftPeer)
|
registerEndpoint("/v1/operator/raft/peer", []string{"DELETE"}, (*HTTPServer).OperatorRaftPeer)
|
||||||
|
|
|
@ -1562,3 +1562,43 @@ type ACLLogoutRequest struct {
|
||||||
func (r *ACLLogoutRequest) RequestDatacenter() string {
|
func (r *ACLLogoutRequest) RequestDatacenter() string {
|
||||||
return r.Datacenter
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -487,6 +487,7 @@ type TestACLConfigParams struct {
|
||||||
DefaultToken string
|
DefaultToken string
|
||||||
AgentMasterToken string
|
AgentMasterToken string
|
||||||
ReplicationToken string
|
ReplicationToken string
|
||||||
|
EnableTokenReplication bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaulTestACLConfigParams() *TestACLConfigParams {
|
func DefaulTestACLConfigParams() *TestACLConfigParams {
|
||||||
|
@ -526,6 +527,7 @@ var aclConfigTpl = template.Must(template.New("ACL Config").Parse(`
|
||||||
{{if ne .DefaultPolicy ""}}
|
{{if ne .DefaultPolicy ""}}
|
||||||
default_policy = "{{ .DefaultPolicy }}"
|
default_policy = "{{ .DefaultPolicy }}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
enable_token_replication = {{printf "%t" .EnableTokenReplication }}
|
||||||
{{if .HasConfiguredTokens }}
|
{{if .HasConfiguredTokens }}
|
||||||
tokens {
|
tokens {
|
||||||
{{if ne .MasterToken ""}}
|
{{if ne .MasterToken ""}}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type rpcFn func(string, interface{}, interface{}) error
|
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