// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package resourcetest import ( "testing" "github.com/stretchr/testify/require" "google.golang.org/protobuf/reflect/protoreflect" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/proto-public/pbresource" ) const ( DENY = "deny" ALLOW = "allow" DEFAULT = "default" ) var checkF = func(t *testing.T, expect string, got error) { switch expect { case ALLOW: if acl.IsErrPermissionDenied(got) { t.Fatal("should be allowed") } case DENY: if !acl.IsErrPermissionDenied(got) { t.Fatal("should be denied") } case DEFAULT: require.Nil(t, got, "expected fallthrough decision") default: t.Fatalf("unexpected expectation: %q", expect) } } type ACLTestCase struct { Rules string // AuthCtx is optional. If not provided an empty one will be used. AuthCtx *acl.AuthorizerContext // One of either Res or Data/Owner/Typ should be set. Res *pbresource.Resource Data protoreflect.ProtoMessage Owner *pbresource.ID Typ *pbresource.Type ReadOK string WriteOK string ListOK string ReadHookRequiresResource bool } func RunACLTestCase(t *testing.T, tc ACLTestCase, registry resource.Registry) { var ( typ *pbresource.Type res *pbresource.Resource ) if tc.Res != nil { require.Nil(t, tc.Data) require.Nil(t, tc.Owner) require.Nil(t, tc.Typ) typ = tc.Res.Id.GetType() res = tc.Res } else { require.NotNil(t, tc.Data) require.NotNil(t, tc.Typ) typ = tc.Typ resolvedType, ok := registry.Resolve(typ) require.True(t, ok) res = Resource(tc.Typ, "test"). WithTenancy(DefaultTenancyForType(t, resolvedType)). WithOwner(tc.Owner). WithData(t, tc.Data). Build() } reg, ok := registry.Resolve(typ) require.True(t, ok) ValidateAndNormalize(t, registry, res) config := acl.Config{ WildcardName: structs.WildcardSpecifier, } authz, err := acl.NewAuthorizerFromRules(tc.Rules, &config, nil) require.NoError(t, err) authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()}) if tc.AuthCtx == nil { tc.AuthCtx = &acl.AuthorizerContext{} } if tc.ReadHookRequiresResource { err = reg.ACLs.Read(authz, tc.AuthCtx, res.Id, nil) require.ErrorIs(t, err, resource.ErrNeedResource, "read hook should require the data payload") } t.Run("read", func(t *testing.T) { err := reg.ACLs.Read(authz, tc.AuthCtx, res.Id, res) checkF(t, tc.ReadOK, err) }) t.Run("write", func(t *testing.T) { err := reg.ACLs.Write(authz, tc.AuthCtx, res) checkF(t, tc.WriteOK, err) }) t.Run("list", func(t *testing.T) { err := reg.ACLs.List(authz, tc.AuthCtx) checkF(t, tc.ListOK, err) }) }