NET-5590 - authorization: check for identity:write in CA certs, xds server, and getting envoy bootstrap params (#19049)

* NET-5590 - authorization: check for identity:write in CA certs, xds server, and getting envoy bootstrap params

* gofmt file
This commit is contained in:
John Murret 2023-10-03 16:02:23 -06:00 committed by GitHub
parent 41e6f6cd8b
commit d67e5c6e35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 9 deletions

View File

@ -1456,7 +1456,10 @@ func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, au
"we are %s", v.Datacenter, dc) "we are %s", v.Datacenter, dc)
} }
case *connect.SpiffeIDWorkloadIdentity: case *connect.SpiffeIDWorkloadIdentity:
// TODO: Check for identity:write on the token when identity permissions are supported. v.GetEnterpriseMeta().FillAuthzContext(&authzContext)
if err := allow.IdentityWriteAllowed(v.WorkloadIdentity, &authzContext); err != nil {
return nil, err
}
case *connect.SpiffeIDAgent: case *connect.SpiffeIDAgent:
v.GetEnterpriseMeta().FillAuthzContext(&authzContext) v.GetEnterpriseMeta().FillAuthzContext(&authzContext)
if err := allow.NodeWriteAllowed(v.Agent, &authzContext); err != nil { if err := allow.NodeWriteAllowed(v.Agent, &authzContext); err != nil {

View File

@ -1317,6 +1317,12 @@ func TestCAManager_AuthorizeAndSignCertificate(t *testing.T) {
Host: "test-host", Host: "test-host",
Partition: "test-partition", Partition: "test-partition",
}.URI() }.URI()
identityURL := connect.SpiffeIDWorkloadIdentity{
TrustDomain: "test-trust-domain",
Partition: "test-partition",
Namespace: "test-namespace",
WorkloadIdentity: "test-workload-identity",
}.URI()
tests := []struct { tests := []struct {
name string name string
@ -1412,6 +1418,15 @@ func TestCAManager_AuthorizeAndSignCertificate(t *testing.T) {
} }
}, },
}, },
{
name: "err_identity_write_not_allowed",
expectErr: "Permission denied",
getCSR: func() *x509.CertificateRequest {
return &x509.CertificateRequest{
URIs: []*url.URL{identityURL},
}
},
},
} }
for _, tc := range tests { for _, tc := range tests {

View File

@ -80,7 +80,10 @@ func (s *Server) GetEnvoyBootstrapParams(ctx context.Context, req *pbdataplane.G
return nil, status.Errorf(codes.InvalidArgument, "workload %q doesn't have identity associated with it", req.ProxyId) return nil, status.Errorf(codes.InvalidArgument, "workload %q doesn't have identity associated with it", req.ProxyId)
} }
// todo (ishustava): ACL enforcement ensuring there's identity:write permissions. // verify identity:write is allowed. if not, give permission denied error.
if err := authz.ToAllowAuthorizer().IdentityWriteAllowed(workload.Identity, &authzContext); err != nil {
return nil, err
}
// Get all proxy configurations for this workload. Currently we're only looking // Get all proxy configurations for this workload. Currently we're only looking
// for proxy configurations in the same tenancy as the workload. // for proxy configurations in the same tenancy as the workload.

View File

@ -34,6 +34,7 @@ import (
) )
const ( const (
testIdentity = "test-identity"
testToken = "acl-token-get-envoy-bootstrap-params" testToken = "acl-token-get-envoy-bootstrap-params"
testServiceName = "web" testServiceName = "web"
proxyServiceID = "web-proxy" proxyServiceID = "web-proxy"
@ -308,7 +309,23 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) {
} }
aclResolver.On("ResolveTokenAndDefaultMeta", testToken, mock.Anything, mock.Anything). aclResolver.On("ResolveTokenAndDefaultMeta", testToken, mock.Anything, mock.Anything).
Return(testutils.ACLServiceRead(t, workloadResource.Id.Name), nil) Return(testutils.ACLUseProvidedPolicy(t,
&acl.Policy{
PolicyRules: acl.PolicyRules{
Services: []*acl.ServiceRule{
{
Name: workloadResource.Id.Name,
Policy: acl.PolicyRead,
},
},
Identities: []*acl.IdentityRule{
{
Name: testIdentity,
Policy: acl.PolicyWrite,
},
},
},
}), nil)
resp, err := client.GetEnvoyBootstrapParams(ctx, req) resp, err := client.GetEnvoyBootstrapParams(ctx, req)
require.NoError(t, err) require.NoError(t, err)
@ -328,14 +345,14 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) {
{ {
name: "workload without node", name: "workload without node",
workloadData: &pbcatalog.Workload{ workloadData: &pbcatalog.Workload{
Identity: "test-identity", Identity: testIdentity,
}, },
expBootstrapCfg: &pbmesh.BootstrapConfig{}, expBootstrapCfg: &pbmesh.BootstrapConfig{},
}, },
{ {
name: "workload with node", name: "workload with node",
workloadData: &pbcatalog.Workload{ workloadData: &pbcatalog.Workload{
Identity: "test-identity", Identity: testIdentity,
NodeName: "test-node", NodeName: "test-node",
}, },
expBootstrapCfg: &pbmesh.BootstrapConfig{}, expBootstrapCfg: &pbmesh.BootstrapConfig{},
@ -343,7 +360,7 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) {
{ {
name: "single proxy configuration", name: "single proxy configuration",
workloadData: &pbcatalog.Workload{ workloadData: &pbcatalog.Workload{
Identity: "test-identity", Identity: testIdentity,
}, },
proxyCfgs: []*pbmesh.ProxyConfiguration{ proxyCfgs: []*pbmesh.ProxyConfiguration{
{ {
@ -360,7 +377,7 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) {
{ {
name: "multiple proxy configurations", name: "multiple proxy configurations",
workloadData: &pbcatalog.Workload{ workloadData: &pbcatalog.Workload{
Identity: "test-identity", Identity: testIdentity,
}, },
proxyCfgs: []*pbmesh.ProxyConfiguration{ proxyCfgs: []*pbmesh.ProxyConfiguration{
{ {

View File

@ -84,6 +84,18 @@ func ACLServiceRead(t *testing.T, serviceName string) resolver.Result {
} }
} }
func ACLUseProvidedPolicy(t *testing.T, aclPolicy *acl.Policy) resolver.Result {
t.Helper()
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{aclPolicy}, nil)
require.NoError(t, err)
return resolver.Result{
Authorizer: authz,
ACLIdentity: randomACLIdentity(t),
}
}
func ACLOperatorRead(t *testing.T) resolver.Result { func ACLOperatorRead(t *testing.T) resolver.Result {
t.Helper() t.Helper()

View File

@ -5,6 +5,7 @@ package proxytracker
import ( import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/resource"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
) )
@ -34,9 +35,13 @@ func (p *ProxyState) AllowEmptyClusters() bool {
} }
func (p *ProxyState) Authorize(authz acl.Authorizer) error { func (p *ProxyState) Authorize(authz acl.Authorizer) error {
// TODO(proxystate): we'll need to implement this once identity policy is implemented // authorize for mesh proxies.
// TODO(proxystate): implement differently for gateways
allow := authz.ToAllowAuthorizer()
if err := allow.IdentityWriteAllowed(p.Identity.Name, resource.AuthorizerContext(p.Identity.Tenancy)); err != nil {
return err
}
// Authed OK!
return nil return nil
} }

View File

@ -0,0 +1,78 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package proxytracker
import (
"github.com/hashicorp/consul/acl"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"strings"
"testing"
)
func TestProxyState_Authorize(t *testing.T) {
testIdentity := &pbresource.Reference{
Type: &pbresource.Type{
Group: "mesh",
GroupVersion: "v1alpha1",
Kind: "Identity",
},
Tenancy: &pbresource.Tenancy{
Partition: "default",
Namespace: "default",
PeerName: "local",
},
Name: "test-identity",
}
type testCase struct {
description string
proxyState *ProxyState
configureAuthorizer func(authorizer *acl.MockAuthorizer)
expectedErrorMessage string
}
testsCases := []testCase{
{
description: "ProxyState - if identity write is allowed for the workload then allow.",
proxyState: &ProxyState{
ProxyState: &pbmesh.ProxyState{
Identity: testIdentity,
},
},
expectedErrorMessage: "",
configureAuthorizer: func(authz *acl.MockAuthorizer) {
authz.On("IdentityWrite", testIdentity.Name, mock.Anything).Return(acl.Allow)
},
},
{
description: "ProxyState - if identity write is not allowed for the workload then deny.",
proxyState: &ProxyState{
ProxyState: &pbmesh.ProxyState{
Identity: testIdentity,
},
},
expectedErrorMessage: "Permission denied: token with AccessorID '' lacks permission 'identity:write' on \"test-identity\"",
configureAuthorizer: func(authz *acl.MockAuthorizer) {
authz.On("IdentityWrite", testIdentity.Name, mock.Anything).Return(acl.Deny)
},
},
}
for _, tc := range testsCases {
t.Run(tc.description, func(t *testing.T) {
authz := &acl.MockAuthorizer{}
authz.On("ToAllow").Return(acl.AllowAuthorizer{Authorizer: authz})
tc.configureAuthorizer(authz)
err := tc.proxyState.Authorize(authz)
errMsg := ""
if err != nil {
errMsg = err.Error()
}
// using contains because Enterprise tests append the parition and namespace
// information to the message.
require.True(t, strings.Contains(errMsg, tc.expectedErrorMessage))
})
}
}