diff --git a/agent/consul/leader_connect_ca.go b/agent/consul/leader_connect_ca.go index fd190ed35e..92cdf40a6a 100644 --- a/agent/consul/leader_connect_ca.go +++ b/agent/consul/leader_connect_ca.go @@ -1456,7 +1456,10 @@ func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, au "we are %s", v.Datacenter, dc) } 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: v.GetEnterpriseMeta().FillAuthzContext(&authzContext) if err := allow.NodeWriteAllowed(v.Agent, &authzContext); err != nil { diff --git a/agent/consul/leader_connect_ca_test.go b/agent/consul/leader_connect_ca_test.go index b3e8fdc9d0..e372c010a7 100644 --- a/agent/consul/leader_connect_ca_test.go +++ b/agent/consul/leader_connect_ca_test.go @@ -1317,6 +1317,12 @@ func TestCAManager_AuthorizeAndSignCertificate(t *testing.T) { Host: "test-host", Partition: "test-partition", }.URI() + identityURL := connect.SpiffeIDWorkloadIdentity{ + TrustDomain: "test-trust-domain", + Partition: "test-partition", + Namespace: "test-namespace", + WorkloadIdentity: "test-workload-identity", + }.URI() tests := []struct { 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 { diff --git a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go index aaa3a33728..ea5d9c47b4 100644 --- a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go +++ b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go @@ -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) } - // 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 // for proxy configurations in the same tenancy as the workload. diff --git a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go index ff365d9ff1..a3761305e9 100644 --- a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go +++ b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go @@ -34,6 +34,7 @@ import ( ) const ( + testIdentity = "test-identity" testToken = "acl-token-get-envoy-bootstrap-params" testServiceName = "web" proxyServiceID = "web-proxy" @@ -308,7 +309,23 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) { } 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) require.NoError(t, err) @@ -328,14 +345,14 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) { { name: "workload without node", workloadData: &pbcatalog.Workload{ - Identity: "test-identity", + Identity: testIdentity, }, expBootstrapCfg: &pbmesh.BootstrapConfig{}, }, { name: "workload with node", workloadData: &pbcatalog.Workload{ - Identity: "test-identity", + Identity: testIdentity, NodeName: "test-node", }, expBootstrapCfg: &pbmesh.BootstrapConfig{}, @@ -343,7 +360,7 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) { { name: "single proxy configuration", workloadData: &pbcatalog.Workload{ - Identity: "test-identity", + Identity: testIdentity, }, proxyCfgs: []*pbmesh.ProxyConfiguration{ { @@ -360,7 +377,7 @@ func TestGetEnvoyBootstrapParams_Success_EnableV2(t *testing.T) { { name: "multiple proxy configurations", workloadData: &pbcatalog.Workload{ - Identity: "test-identity", + Identity: testIdentity, }, proxyCfgs: []*pbmesh.ProxyConfiguration{ { diff --git a/agent/grpc-external/testutils/acl.go b/agent/grpc-external/testutils/acl.go index caa5c7ae81..72e0897e71 100644 --- a/agent/grpc-external/testutils/acl.go +++ b/agent/grpc-external/testutils/acl.go @@ -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 { t.Helper() diff --git a/internal/mesh/proxy-tracker/proxy_state_exports.go b/internal/mesh/proxy-tracker/proxy_state_exports.go index d1051c3cac..cdc6d6d845 100644 --- a/internal/mesh/proxy-tracker/proxy_state_exports.go +++ b/internal/mesh/proxy-tracker/proxy_state_exports.go @@ -5,6 +5,7 @@ package proxytracker import ( "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/internal/resource" 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 { - // 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 } diff --git a/internal/mesh/proxy-tracker/proxy_state_exports_test.go b/internal/mesh/proxy-tracker/proxy_state_exports_test.go new file mode 100644 index 0000000000..18d15fb53d --- /dev/null +++ b/internal/mesh/proxy-tracker/proxy_state_exports_test.go @@ -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)) + }) + } +}