[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:
Matt Keeler 2019-12-06 09:25:26 -05:00 committed by GitHub
parent 1d21635a6b
commit deb91f3d3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1359 additions and 8 deletions

View File

@ -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)
}

View File

@ -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
}

621
acl/authorizer_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -487,6 +487,7 @@ type TestACLConfigParams struct {
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 ""}}

View File

@ -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")
})
}