From 7c4451dc73470742536494a673f7ab794d141315 Mon Sep 17 00:00:00 2001 From: "Sorawis Nilparuk (Bo)" Date: Fri, 24 Jan 2025 10:23:33 -0800 Subject: [PATCH] Bug: fix groups in children scopes being filtered out by grants (#5418) * add utility functions for grants tests * use passed in scopeID to create user * add test for groups list * groups: set ParentScopeId before FetchActionSetForId * add comment to TestRoleGrantsForToken * lint and ran make gen * fix import groups * add an additional test case * remove print * changelog * fix(alias): set parent scope id for alias resource (#5434) set the `ParentScopeId` before fetching authorized actions for alias * fix(worker): set parent scope id for worker resource (#5435) set the `ParentScopeId` before fetching authorized actions for worker * fix(user): children scopes being filtered out by grants for user (#5436) * fix(user): children scopes being filtered out by grants for user Resources are being filtered out due to missing ParentScopeId when constructing Resource to pass into authResults.FetchActionSetForId. * fix(scope): set parent scope id for worker resource (#5439) set the `ParentScopeId` before fetching authorized actions for worker * fix(target): set parent scope id for target resource (#5447) set the `ParentScopeId` before fetching authorized actions for target * fix(roles): set parent scope id for roles resource (#5452) * fix(roles): set parent scope id for roles resource set the `ParentScopeId` before fetching authorized actions for role resource * test(managed-group): add grants test coverage (#5453) * fix(managed-group): set parent scope id for managed-group resource set the ParentScopeId before fetching authorized actions for managed resource * fix(host): set parent scope id for host resource (#5455) set the `ParentScopeId` before fetching authorized actions for host resource * fix(host-set): set parent scope id for host-set resource (#5456) set the ParentScopeId before fetching authorized actions for host-set resource * fix(host-catalog): set parent scope id for host-catalog resource (#5457) set the ParentScopeId before fetching authorized actions for host-catalog resource * fix(credential-store): set parent scope id for credential-store resource (#5458) set the ParentScopeId before fetching authorized actions for credential-store resource * fix(authmethods): set parent scope ID for auth methods resource (#5448) * handlers/authmethods: fix children permission * documentation * formatting * fix(credential): set parent scope id for credential resource (#5459) set the `ParentScopeId` before fetching authorized actions for credential resource * fix(accounts): bug grants filter children accounts (#5431) * handlers/accounts: fix children grants filtering out results unexpectedly * make gen * fix(authtokens): set parent scope ID for auth token resource (#5451) * fix authtokens not passing children scope ID * make gen * fix(credential-libraries): set parent scope ID (#5463) * fix(common) set parent ID before fetching action setsparent ID before fetching action sets (#5467) * fix(common) set parent ID before fetching action sets * make gen * additional test * rename test * more tests * remove duplicate test * make gen * Update CHANGELOG.md Co-authored-by: Johan Brandhorst-Satzkorn --------- Co-authored-by: Elim Tsiagbey Co-authored-by: Johan Brandhorst-Satzkorn --- CHANGELOG.md | 3 + internal/authtoken/testing.go | 36 ++ internal/daemon/controller/auth/testing.go | 35 ++ .../controller/common/scopeids/scope_ids.go | 1 + .../handlers/accounts/account_service.go | 7 +- .../handlers/accounts/grants_test.go | 111 +++++++ .../handlers/aliases/alias_service.go | 1 + .../handlers/aliases/grants_test.go | 107 ++++++ .../authmethods/authmethod_service.go | 1 + .../handlers/authmethods/grants_test.go | 266 +++++++++++++++ .../handlers/authtokens/authtoken_service.go | 2 + .../handlers/authtokens/grants_test.go | 130 ++++++++ .../credentiallibrary_service.go | 9 +- .../credentiallibraries/grants_test.go | 104 ++++++ .../credentials/credential_service.go | 7 +- .../handlers/credentials/grants_test.go | 139 ++++++++ .../credentialstore_service.go | 7 +- .../handlers/credentialstores/grants_test.go | 149 +++++++++ .../controller/handlers/groups/grants_test.go | 119 +++++++ .../handlers/groups/group_service.go | 1 + .../handlers/host_catalogs/grants_test.go | 151 +++++++++ .../host_catalogs/host_catalog_service.go | 7 +- .../handlers/host_sets/grants_test.go | 140 ++++++++ .../handlers/host_sets/host_set_service.go | 9 +- .../controller/handlers/hosts/grants_test.go | 142 ++++++++ .../controller/handlers/hosts/host_service.go | 9 +- .../handlers/managed_groups/grants_test.go | 126 +++++++ .../managed_groups/managed_group_service.go | 7 +- .../controller/handlers/roles/grants_test.go | 181 ++++++++++ .../controller/handlers/roles/role_service.go | 1 + .../controller/handlers/scopes/grants_test.go | 112 +++++++ .../handlers/scopes/scope_service.go | 7 +- .../handlers/targets/target_service.go | 1 + .../handlers/targets/tcp/grants_test.go | 107 ++++++ .../controller/handlers/users/grants_test.go | 312 ++++++++++++++++++ .../controller/handlers/users/user_service.go | 1 + .../handlers/workers/grants_test.go | 127 +++++++ .../handlers/workers/worker_service.go | 1 + internal/iam/testing.go | 30 ++ 39 files changed, 2676 insertions(+), 30 deletions(-) create mode 100644 internal/daemon/controller/handlers/accounts/grants_test.go create mode 100644 internal/daemon/controller/handlers/aliases/grants_test.go create mode 100644 internal/daemon/controller/handlers/authmethods/grants_test.go create mode 100644 internal/daemon/controller/handlers/authtokens/grants_test.go create mode 100644 internal/daemon/controller/handlers/credentiallibraries/grants_test.go create mode 100644 internal/daemon/controller/handlers/credentials/grants_test.go create mode 100644 internal/daemon/controller/handlers/credentialstores/grants_test.go create mode 100644 internal/daemon/controller/handlers/groups/grants_test.go create mode 100644 internal/daemon/controller/handlers/host_catalogs/grants_test.go create mode 100644 internal/daemon/controller/handlers/host_sets/grants_test.go create mode 100644 internal/daemon/controller/handlers/hosts/grants_test.go create mode 100644 internal/daemon/controller/handlers/managed_groups/grants_test.go create mode 100644 internal/daemon/controller/handlers/roles/grants_test.go create mode 100644 internal/daemon/controller/handlers/scopes/grants_test.go create mode 100644 internal/daemon/controller/handlers/targets/tcp/grants_test.go create mode 100644 internal/daemon/controller/handlers/users/grants_test.go create mode 100644 internal/daemon/controller/handlers/workers/grants_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 42daf3c3b..38213b375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,9 @@ maintainability of worker queries, and improve DB performance. ([PR](https://git ([PR](https://github.com/hashicorp/boundary/pull/5221)). * Fix an issue where, when starting a session, the connection limit always displays 0. ([PR](https://github.com/hashicorp/boundary/pull/5396)). +* Fix bug which caused the `children` keyword not to apply the appropriate + permissions for a number of resources. + ([PR](https://github.com/hashicorp/boundary/pull/5418)). ## 0.18.2 (2024/12/12) ### Bug fixes diff --git a/internal/authtoken/testing.go b/internal/authtoken/testing.go index 2d89bc24e..1748d8bd5 100644 --- a/internal/authtoken/testing.go +++ b/internal/authtoken/testing.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/require" ) @@ -46,3 +47,38 @@ func TestAuthToken(t testing.TB, conn *db.DB, kms *kms.Kms, scopeId string, opt require.NoError(t, err) return at } + +// TestRoleGrantsForToken contains information used by TestAuthTokenWithRoles to create +// roles and their associated grants (with grant scopes) +type TestRoleGrantsForToken struct { + RoleScopeID string + GrantStrings []string + GrantScopes []string +} + +// TestAuthTokenWithRoles creates auth token associated with roles as requested by the caller along +// with any required resources to achieve said token +func TestAuthTokenWithRoles(t testing.TB, conn *db.DB, kms *kms.Kms, scopeId string, roles []TestRoleGrantsForToken) *AuthToken { + t.Helper() + ctx := context.Background() + rw := db.New(conn) + atRepo, err := NewRepository(ctx, rw, rw, kms) + require.NoError(t, err) + + iamRepo, err := iam.NewRepository(ctx, rw, rw, kms) + require.NoError(t, err) + + authMethod := password.TestAuthMethods(t, conn, scopeId, 1)[0] + + loginName, err := uuid.GenerateUUID() + require.NoError(t, err) + acct := password.TestAccount(t, conn, authMethod.GetPublicId(), loginName) + user := iam.TestUser(t, iamRepo, scopeId, iam.WithAccountIds(acct.GetPublicId())) + for _, r := range roles { + role := iam.TestRoleWithGrants(t, conn, r.RoleScopeID, r.GrantScopes, r.GrantStrings) + _ = iam.TestUserRole(t, conn, role.PublicId, user.PublicId) + } + fullGrantToken, err := atRepo.CreateAuthToken(ctx, user, acct.GetPublicId()) + require.NoError(t, err) + return fullGrantToken +} diff --git a/internal/daemon/controller/auth/testing.go b/internal/daemon/controller/auth/testing.go index 0a9c52a75..b100d1e22 100644 --- a/internal/daemon/controller/auth/testing.go +++ b/internal/daemon/controller/auth/testing.go @@ -5,11 +5,19 @@ package auth import ( "context" + "testing" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/common" + "github.com/hashicorp/boundary/internal/db" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/requests" + "github.com/hashicorp/boundary/internal/server" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/stretchr/testify/require" ) // DisabledAuthTestContext is meant for testing, and uses a context that has @@ -30,3 +38,30 @@ func DisabledAuthTestContext(iamRepoFn common.IamRepoFactory, scopeId string, op requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) return NewVerifierContext(requestContext, iamRepoFn, nil, nil, opts.withKms, &reqInfo) } + +// TestAuthContextFromToken creates an auth context with provided token +// This is used in conjunction with TestAuthTokenWithRoles which creates a test token +func TestAuthContextFromToken(t *testing.T, conn *db.DB, wrap wrapping.Wrapper, token *authtoken.AuthToken, iamRepo *iam.Repository) context.Context { + t.Helper() + ctx := context.Background() + rw := db.New(conn) + kmsCache := kms.TestKms(t, conn, wrap) + atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + serversRepoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + atRepoFn := func() (*authtoken.Repository, error) { + return atRepo, nil + } + fullGrantAuthCtx := NewVerifierContext(requests.NewRequestContext(ctx, requests.WithUserId(token.GetIamUserId())), + iamRepoFn, atRepoFn, serversRepoFn, kmsCache, &authpb.RequestInfo{ + PublicId: token.PublicId, + Token: token.GetToken(), + TokenFormat: uint32(AuthTokenTypeBearer), + }) + return fullGrantAuthCtx +} diff --git a/internal/daemon/controller/common/scopeids/scope_ids.go b/internal/daemon/controller/common/scopeids/scope_ids.go index ea0c6a01e..53b7116ee 100644 --- a/internal/daemon/controller/common/scopeids/scope_ids.go +++ b/internal/daemon/controller/common/scopeids/scope_ids.go @@ -150,6 +150,7 @@ func GetListingResourceInformation( for _, scp := range scps { scpId := scp.GetPublicId() res.ScopeId = scpId + res.ParentScopeId = scp.GetParentId() aSet := input.AuthResults.FetchActionSetForType(ctx, // This is overridden by WithResource resource.Unknown, diff --git a/internal/daemon/controller/handlers/accounts/account_service.go b/internal/daemon/controller/handlers/accounts/account_service.go index a132fc3a7..af84da346 100644 --- a/internal/daemon/controller/handlers/accounts/account_service.go +++ b/internal/daemon/controller/handlers/accounts/account_service.go @@ -1488,9 +1488,10 @@ func validateSetPasswordRequest(ctx context.Context, req *pbs.SetPasswordRequest func newOutputOpts(ctx context.Context, item auth.Account, authMethodId string, authResults requestauth.VerifyResults) ([]handlers.Option, bool) { res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.Account, - Pin: authMethodId, + ScopeId: authResults.Scope.Id, + ParentScopeId: authResults.Scope.ParentScopeId, + Type: resource.Account, + Pin: authMethodId, } res.Id = item.GetPublicId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings() diff --git a/internal/daemon/controller/handlers/accounts/grants_test.go b/internal/daemon/controller/handlers/accounts/grants_test.go new file mode 100644 index 000000000..53fc16ce8 --- /dev/null +++ b/internal/daemon/controller/handlers/accounts/grants_test.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package accounts_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/accounts" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +func TestListPassword_Grants(t *testing.T) { + ctx := context.TODO() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrap) + iamRepo, err := iam.NewRepository(ctx, rw, rw, kms) + require.NoError(t, err) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(ctx, rw, rw, kms) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kms) + } + + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kms) + } + + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + orgAM := password.TestAuthMethod(t, conn, org.GetPublicId()) + projAM := password.TestAuthMethod(t, conn, proj.GetPublicId()) + orgPWAccounts := password.TestMultipleAccounts(t, conn, orgAM.GetPublicId(), 3) + projPWAccounts := password.TestMultipleAccounts(t, conn, projAM.GetPublicId(), 3) + + testcases := []struct { + name string + roleRequest []authtoken.TestRoleGrantsForToken + input *pbs.ListAccountsRequest + wantAccountIDs []string + wantErr error + }{ + { + name: "children grants global scope list org accounts return org accounts", + input: &pbs.ListAccountsRequest{ + AuthMethodId: orgAM.PublicId, + PageSize: 100, + }, + roleRequest: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=*;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + wantAccountIDs: []string{orgPWAccounts[0].PublicId, orgPWAccounts[1].PublicId, orgPWAccounts[2].PublicId}, + wantErr: nil, + }, + { + name: "children grants org scope list project accounts return project accounts", + input: &pbs.ListAccountsRequest{ + AuthMethodId: projAM.PublicId, + PageSize: 100, + }, + roleRequest: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.GetPublicId(), + GrantStrings: []string{"ids=*;type=*;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + wantAccountIDs: []string{projPWAccounts[0].PublicId, projPWAccounts[1].PublicId, projPWAccounts[2].PublicId}, + wantErr: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + s, err := accounts.NewService(ctx, pwRepoFn, oidcRepoFn, ldapRepoFn, 1000) + require.NoError(t, err, "Couldn't create new user service.") + tok := authtoken.TestAuthTokenWithRoles(t, conn, kms, globals.GlobalPrefix, tc.roleRequest) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, gErr := s.ListAccounts(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.Error(t, err) + require.ErrorIs(t, err, tc.wantErr) + return + } + require.NoError(t, gErr) + require.Len(t, got.Items, len(orgPWAccounts)) + var gotIDs []string + for _, item := range got.Items { + gotIDs = append(gotIDs, item.Id) + } + require.ElementsMatch(t, tc.wantAccountIDs, gotIDs) + }) + } +} diff --git a/internal/daemon/controller/handlers/aliases/alias_service.go b/internal/daemon/controller/handlers/aliases/alias_service.go index 04eb0a8ae..26df2c532 100644 --- a/internal/daemon/controller/handlers/aliases/alias_service.go +++ b/internal/daemon/controller/handlers/aliases/alias_service.go @@ -648,6 +648,7 @@ func newOutputOpts(ctx context.Context, item *target.Alias, scopeInfoMap map[str } res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { return nil, false diff --git a/internal/daemon/controller/handlers/aliases/grants_test.go b/internal/daemon/controller/handlers/aliases/grants_test.go new file mode 100644 index 000000000..81973163d --- /dev/null +++ b/internal/daemon/controller/handlers/aliases/grants_test.go @@ -0,0 +1,107 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package aliases_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/alias/target" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/aliases" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// Scopes [resource]: +// - globalAlias1 [globalAlias] +// - globalAlias2 [globalAlias] +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + iamRepo := iam.TestRepo(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*target.Repository, error) { + return target.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := aliases.NewService(ctx, repoFn, iamRepoFn, 1000) + require.NoError(t, err) + globalAlias1 := target.TestAlias(t, rw, "test.alias.one", target.WithDescription("alias_1"), target.WithName("alias_one")) + globalAlias2 := target.TestAlias(t, rw, "test.alias.two", target.WithDescription("alias_2"), target.WithName("alias_two")) + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListAliasesRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created aliases", + input: &pbs.ListAliasesRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=alias;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{globalAlias1.PublicId, globalAlias2.PublicId}, + }, + { + name: "global role grant this with a non-applicable type throws an error", + input: &pbs.ListAliasesRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=group;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: handlers.ApiErrorWithCode(codes.PermissionDenied), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListAliases(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/authmethods/authmethod_service.go b/internal/daemon/controller/handlers/authmethods/authmethod_service.go index 109868e32..32f86e852 100644 --- a/internal/daemon/controller/handlers/authmethods/authmethod_service.go +++ b/internal/daemon/controller/handlers/authmethods/authmethod_service.go @@ -1577,6 +1577,7 @@ func newOutputOpts(ctx context.Context, item auth.AuthMethod, scopeInfoMap map[s } res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings() if len(authorizedActions) == 0 { return nil, false, nil diff --git a/internal/daemon/controller/handlers/authmethods/grants_test.go b/internal/daemon/controller/handlers/authmethods/grants_test.go new file mode 100644 index 000000000..02ebd550c --- /dev/null +++ b/internal/daemon/controller/handlers/authmethods/grants_test.go @@ -0,0 +1,266 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package authmethods_test + +import ( + "context" + "slices" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/ldap" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + controllerauth "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + rw := db.New(conn) + kmsCache := kms.TestKms(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(ctx, rw, rw, kmsCache) + } + ldapRepoFn := func() (*ldap.Repository, error) { + return ldap.NewRepository(ctx, rw, rw, kmsCache) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(ctx, rw, rw, kmsCache) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ctx, rw, rw, kmsCache) + } + authMethodRepoFn := func() (*auth.AuthMethodRepository, error) { + return auth.NewAuthMethodRepository(ctx, rw, rw, kmsCache) + } + + s, err := authmethods.NewService(ctx, + kmsCache, + pwRepoFn, + oidcRepoFn, + iamRepoFn, + atRepoFn, + ldapRepoFn, + authMethodRepoFn, + 1000) + require.NoError(t, err) + org1, _ := iam.TestScopes(t, iamRepo) + org2, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), globals.GlobalPrefix, kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcGlobal := oidc.TestAuthMethod(t, conn, databaseWrapper, globals.GlobalPrefix, oidc.InactiveState, "client-id", "secret") + oidcOrg1 := oidc.TestAuthMethod(t, conn, databaseWrapper, org1.GetPublicId(), oidc.InactiveState, "client-id", "secret") + oidcOrg2 := oidc.TestAuthMethod(t, conn, databaseWrapper, org2.GetPublicId(), oidc.InactiveState, "client-id", "secret") + + ldapGlobal := ldap.TestAuthMethod(t, conn, wrap, globals.GlobalPrefix, []string{"ldaps://alice.com"}) + ldapOrg1 := ldap.TestAuthMethod(t, conn, wrap, org1.GetPublicId(), []string{"ldaps://alice.com"}) + ldapOrg2 := ldap.TestAuthMethod(t, conn, wrap, org2.GetPublicId(), []string{"ldaps://alice.com"}) + + pwGlobal := password.TestAuthMethod(t, conn, globals.GlobalPrefix) + pwOrg1 := password.TestAuthMethod(t, conn, org1.GetPublicId()) + pwOrg2 := password.TestAuthMethod(t, conn, org2.GetPublicId()) + + // ignoreList consists of auth method IDs to omit from the result set + // this is to handle auth methods that are created during the auth token + // creation + ignoreList := []string{} + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListAuthMethodsRequest + includeGlobalAuthMethods bool + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this and children returns all auth methods", + input: &pbs.ListAuthMethodsRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=auth-method;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + oidcGlobal.PublicId, + oidcOrg1.PublicId, + oidcOrg2.PublicId, + ldapGlobal.PublicId, + ldapOrg1.PublicId, + ldapOrg2.PublicId, + pwGlobal.PublicId, + pwOrg1.PublicId, + pwOrg2.PublicId, + }, + }, + { + name: "no grants return children org", + input: &pbs.ListAuthMethodsRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{}, + wantErr: nil, + // auth methods in `global` are filtered out because the user does not have grants to read + // them in the global scope. Auth methods in org 1 and org 2 show up - my guess is because + // the u_anon grants allow auth methods to be read on any org. + wantIDs: []string{ + // oidcGlobal.PublicId, + oidcOrg1.PublicId, + oidcOrg2.PublicId, + // ldapGlobal.PublicId, + ldapOrg1.PublicId, + ldapOrg2.PublicId, + // pwGlobal.PublicId, + pwOrg1.PublicId, + pwOrg2.PublicId, + }, + }, + { + name: "org role grant this and children returns auth methods in org1", + input: &pbs.ListAuthMethodsRequest{ + ScopeId: org1.PublicId, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org1.PublicId, + GrantStrings: []string{"ids=*;type=auth-method;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{oidcOrg1.PublicId, ldapOrg1.PublicId, pwOrg1.PublicId}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + // auth method created during token generation will not be taken into considerations during this test + // adding to the ignoreList so it can be ignored later + ignoreList = append(ignoreList, tok.GetAuthMethodId()) + fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListAuthMethods(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + // authtoken.TestAuthTokenWithRoles creates an auth method at `global` scope so + // include AuthMethodID of the user used to generate AuthToken in the "wantIDs" set + // when listing auth methods at global scope + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + // do not include IDs in the ignore list in the + // result sets + if slices.Contains(ignoreList, g.Id) { + continue + } + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) + + t.Run("Get", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListAuthMethodsRequest + amIDExpectErrMap map[string]error + rolesToCreate []authtoken.TestRoleGrantsForToken + }{ + { + name: "global role grant this and children returns all auth methods", + amIDExpectErrMap: map[string]error{ + pwGlobal.GetPublicId(): nil, + pwOrg1.GetPublicId(): nil, + pwOrg2.GetPublicId(): nil, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=auth-method;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + }, + { + name: "org role grant this and children returns auth methods in org1", + input: &pbs.ListAuthMethodsRequest{ + ScopeId: org1.PublicId, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=auth-method;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + amIDExpectErrMap: map[string]error{ + pwGlobal.GetPublicId(): handlers.ForbiddenError(), + pwOrg1.GetPublicId(): nil, + pwOrg2.GetPublicId(): nil, + }, + }, + { + name: "no grants return all auth methods", + input: &pbs.ListAuthMethodsRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + PageSize: 500, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{}, + amIDExpectErrMap: map[string]error{ + pwGlobal.GetPublicId(): handlers.ForbiddenError(), + pwOrg1.GetPublicId(): handlers.ForbiddenError(), + pwOrg2.GetPublicId(): handlers.ForbiddenError(), + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + for amId, wantErr := range tc.amIDExpectErrMap { + _, err := s.GetAuthMethod(fullGrantAuthCtx, &pbs.GetAuthMethodRequest{ + Id: amId, + }) + if wantErr != nil { + require.ErrorIs(t, err, wantErr) + continue + } + require.NoError(t, err) + } + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/authtokens/authtoken_service.go b/internal/daemon/controller/handlers/authtokens/authtoken_service.go index 8c66454e6..8cf7ebe76 100644 --- a/internal/daemon/controller/handlers/authtokens/authtoken_service.go +++ b/internal/daemon/controller/handlers/authtokens/authtoken_service.go @@ -457,6 +457,8 @@ func newOutputOpts(ctx context.Context, item *authtoken.AuthToken, scopeInfoMap } res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() + authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { return nil, false diff --git a/internal/daemon/controller/handlers/authtokens/grants_test.go b/internal/daemon/controller/handlers/authtokens/grants_test.go new file mode 100644 index 000000000..2394c0b39 --- /dev/null +++ b/internal/daemon/controller/handlers/authtokens/grants_test.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package authtokens_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + at "github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - project level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project +// Scopes [resource]: +// - global [globalGroup] +// - org1 [org1Group] +// - proj1 [proj1Group] +// - org2 [org2Group] +// - proj2 [proj2Group] +// - proj3 [proj3Group] +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + kmsCache := kms.TestKms(t, conn, wrap) + + atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache) + require.NoError(t, err) + atRepoFn := func() (*authtoken.Repository, error) { return atRepo, nil } + + s, err := at.NewService(ctx, atRepoFn, iamRepoFn, 1000) + + require.NoError(t, err) + + org1, _ := iam.TestScopes(t, iamRepo) + org2, _ := iam.TestScopes(t, iamRepo) + + globalAT := authtoken.TestAuthToken(t, conn, kmsCache, globals.GlobalPrefix) + org1AT := authtoken.TestAuthToken(t, conn, kmsCache, org1.GetPublicId()) + org2AT := authtoken.TestAuthToken(t, conn, kmsCache, org2.GetPublicId()) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListAuthTokensRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this and children returns global and org groups", + input: &pbs.ListAuthTokensRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=auth-token;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{globalAT.PublicId, org1AT.PublicId, org2AT.PublicId}, + }, + { + name: "org role grant this and children returns org groups", + input: &pbs.ListAuthTokensRequest{ + ScopeId: org1.PublicId, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org1.PublicId, + GrantStrings: []string{"ids=*;type=auth-token;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{org1AT.PublicId}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListAuthTokens(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + fmt.Println("ID:", g.GetId(), "Scope:", g.GetScopeId()) + gotIDs = append(gotIDs, g.GetId()) + } + + for _, id := range tc.wantIDs { + require.Contains(t, gotIDs, id) + } + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go index c84182a37..01e2c3e98 100644 --- a/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go +++ b/internal/daemon/controller/handlers/credentiallibraries/credentiallibrary_service.go @@ -678,10 +678,11 @@ func newOutputOpts( authResults auth.VerifyResults, ) ([]handlers.Option, bool) { res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.CredentialLibrary, - Pin: credentialStoreId, - Id: item.GetPublicId(), + ScopeId: authResults.Scope.Id, + ParentScopeId: authResults.Scope.GetParentScopeId(), + Type: resource.CredentialLibrary, + Pin: credentialStoreId, + Id: item.GetPublicId(), } authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() if len(authorizedActions) == 0 { diff --git a/internal/daemon/controller/handlers/credentiallibraries/grants_test.go b/internal/daemon/controller/handlers/credentiallibraries/grants_test.go new file mode 100644 index 000000000..7e4a12ff0 --- /dev/null +++ b/internal/daemon/controller/handlers/credentiallibraries/grants_test.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentiallibraries_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/credential/vault" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentiallibraries" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/stretchr/testify/require" +) + +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + kmsCache := kms.TestKms(t, conn, wrap) + sche := scheduler.TestScheduler(t, conn, wrap) + + vaultRepoFn := func() (*vault.Repository, error) { + return vault.NewRepository(ctx, rw, rw, kmsCache, sche) + } + + s, err := credentiallibraries.NewService(ctx, iamRepoFn, vaultRepoFn, 1000) + require.NoError(t, err) + org, proj := iam.TestScopes(t, iamRepo) + + proj1CredStore := vault.TestCredentialStores(t, conn, wrap, proj.PublicId, 1) + proj1Libs := vault.TestCredentialLibraries(t, conn, wrap, proj1CredStore[0].GetPublicId(), 3) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListCredentialLibrariesRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant descendant returns all credentials library", + input: &pbs.ListCredentialLibrariesRequest{ + CredentialStoreId: proj1CredStore[0].GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=credential-library;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: []string{proj1Libs[0].GetPublicId(), proj1Libs[1].GetPublicId(), proj1Libs[2].GetPublicId()}, + }, + { + name: "org role grant this and children returns all credentials library", + input: &pbs.ListCredentialLibrariesRequest{ + CredentialStoreId: proj1CredStore[0].GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.GetPublicId(), + GrantStrings: []string{"ids=*;type=credential-library;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{proj1Libs[0].GetPublicId(), proj1Libs[1].GetPublicId(), proj1Libs[2].GetPublicId()}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListCredentialLibraries(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index 66da53067..88ca1a05c 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -640,9 +640,10 @@ func newOutputOpts( authResults auth.VerifyResults, ) ([]handlers.Option, bool) { res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.Credential, - Pin: credentialStoreId, + ScopeId: authResults.Scope.Id, + ParentScopeId: authResults.Scope.ParentScopeId, + Type: resource.Credential, + Pin: credentialStoreId, } res.Id = item.GetPublicId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() diff --git a/internal/daemon/controller/handlers/credentials/grants_test.go b/internal/daemon/controller/handlers/credentials/grants_test.go new file mode 100644 index 000000000..2799d0d3e --- /dev/null +++ b/internal/daemon/controller/handlers/credentials/grants_test.go @@ -0,0 +1,139 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentials_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/credential/static" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentials" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - proj level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project: this +// Scopes [resource]: +// - global +// - org1 +// - proj1 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + rw := db.New(conn) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + staticRepoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := credentials.NewService(ctx, iamRepoFn, staticRepoFn, 1000) + require.NoError(t, err) + + org, proj := iam.TestScopes(t, iamRepo) + + credStore := static.TestCredentialStore(t, conn, wrap, proj.GetPublicId()) + var wantCreds []string + for i := 0; i < 10; i++ { + user := fmt.Sprintf("user-%d", i) + pass := fmt.Sprintf("pass-%d", i) + c := static.TestUsernamePasswordCredential(t, conn, wrap, user, pass, credStore.GetPublicId(), proj.GetPublicId()) + require.NoError(t, err) + wantCreds = append(wantCreds, c.GetPublicId()) + } + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListCredentialsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created credentials", + input: &pbs.ListCredentialsRequest{ + CredentialStoreId: credStore.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=credential;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: wantCreds, + }, + { + name: "org role grant this returns all created credentials", + input: &pbs.ListCredentialsRequest{ + CredentialStoreId: credStore.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.PublicId, + GrantStrings: []string{"ids=*;type=credential;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: wantCreds, + }, + { + name: "project role grant this returns all created credentials", + input: &pbs.ListCredentialsRequest{ + CredentialStoreId: credStore.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: proj.PublicId, + GrantStrings: []string{"ids=*;type=credential;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: wantCreds, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListCredentials(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go index 694f2bd58..f47effbdb 100644 --- a/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go +++ b/internal/daemon/controller/handlers/credentialstores/credentialstore_service.go @@ -676,9 +676,10 @@ func newOutputOpts( authzScopes map[string]*scopes.ScopeInfo, ) ([]handlers.Option, bool, error) { res := perms.Resource{ - Type: resource.CredentialStore, - Id: item.GetPublicId(), - ScopeId: item.GetProjectId(), + Type: resource.CredentialStore, + Id: item.GetPublicId(), + ScopeId: item.GetProjectId(), + ParentScopeId: authzScopes[item.GetProjectId()].GetParentScopeId(), } authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() if len(authorizedActions) == 0 { diff --git a/internal/daemon/controller/handlers/credentialstores/grants_test.go b/internal/daemon/controller/handlers/credentialstores/grants_test.go new file mode 100644 index 000000000..5ff3f5052 --- /dev/null +++ b/internal/daemon/controller/handlers/credentialstores/grants_test.go @@ -0,0 +1,149 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentialstores_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static" + "github.com/hashicorp/boundary/internal/credential/vault" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - proj level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project: this +// Scopes [resource]: +// - global +// - org1 +// - proj1 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + rw := db.New(conn) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + + sche := scheduler.TestScheduler(t, conn, wrap) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + vaultRepoFn := func() (*vault.Repository, error) { + return vault.NewRepository(ctx, rw, rw, kmsCache, sche) + } + staticRepoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kmsCache) + } + credStoreRepoFn := func() (*credential.StoreRepository, error) { + return credential.NewStoreRepository(context.Background(), rw, rw) + } + s, err := credentialstores.NewService(ctx, iamRepoFn, vaultRepoFn, staticRepoFn, credStoreRepoFn, 1000) + require.NoError(t, err) + + org, proj := iam.TestScopes(t, iamRepo) + + var wantStores []string + for _, s := range vault.TestCredentialStores(t, conn, wrap, proj.GetPublicId(), 5) { + wantStores = append(wantStores, s.GetPublicId()) + } + + for _, s := range static.TestCredentialStores(t, conn, wrap, proj.GetPublicId(), 5) { + wantStores = append(wantStores, s.GetPublicId()) + } + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListCredentialStoresRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created credential stores", + input: &pbs.ListCredentialStoresRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=credential-store;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: wantStores, + }, + { + name: "org role grant this returns all created credential stores", + input: &pbs.ListCredentialStoresRequest{ + ScopeId: org.GetPublicId(), + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.PublicId, + GrantStrings: []string{"ids=*;type=credential-store;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: wantStores, + }, + { + name: "project role grant this returns all created credential stores", + input: &pbs.ListCredentialStoresRequest{ + ScopeId: proj.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: proj.PublicId, + GrantStrings: []string{"ids=*;type=credential-store;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: wantStores, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListCredentialStores(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/groups/grants_test.go b/internal/daemon/controller/handlers/groups/grants_test.go new file mode 100644 index 000000000..d0daddb6e --- /dev/null +++ b/internal/daemon/controller/handlers/groups/grants_test.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package groups_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/groups" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - project level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project +// Scopes [resource]: +// - global [globalGroup] +// - org1 [org1Group] +// - proj1 [proj1Group] +// - org2 [org2Group] +// - proj2 [proj2Group] +// - proj3 [proj3Group] +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + repoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + kmsCache := kms.TestKms(t, conn, wrap) + s, err := groups.NewService(ctx, repoFn, 1000) + require.NoError(t, err) + org1, _ := iam.TestScopes(t, iamRepo) + org2, proj2 := iam.TestScopes(t, iamRepo) + proj3 := iam.TestProject(t, iamRepo, org2.PublicId) + globalGroup := iam.TestGroup(t, conn, globals.GlobalPrefix, iam.WithDescription("global"), iam.WithName("global")) + org1Group := iam.TestGroup(t, conn, org1.GetPublicId(), iam.WithDescription("org1"), iam.WithName("org1")) + org2Group := iam.TestGroup(t, conn, org2.GetPublicId(), iam.WithDescription("org2"), iam.WithName("org2")) + + proj2Group := iam.TestGroup(t, conn, proj2.GetPublicId(), iam.WithDescription("proj2"), iam.WithName("proj2")) + proj3Group := iam.TestGroup(t, conn, proj3.GetPublicId(), iam.WithDescription("proj3"), iam.WithName("proj3")) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListGroupsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this and children returns global and org groups", + input: &pbs.ListGroupsRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=group;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{globalGroup.PublicId, org1Group.PublicId, org2Group.PublicId}, + }, + { + name: "org role grant this and children returns org and project groups", + input: &pbs.ListGroupsRequest{ + ScopeId: org2.PublicId, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org2.PublicId, + GrantStrings: []string{"ids=*;type=group;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{org2Group.PublicId, proj2Group.PublicId, proj3Group.PublicId}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListGroups(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/groups/group_service.go b/internal/daemon/controller/handlers/groups/group_service.go index 2e2dc62c0..a74442541 100644 --- a/internal/daemon/controller/handlers/groups/group_service.go +++ b/internal/daemon/controller/handlers/groups/group_service.go @@ -840,6 +840,7 @@ func newOutputOpts(ctx context.Context, item *iam.Group, scopeInfoMap map[string } res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { return nil, false diff --git a/internal/daemon/controller/handlers/host_catalogs/grants_test.go b/internal/daemon/controller/handlers/host_catalogs/grants_test.go new file mode 100644 index 000000000..9576fd010 --- /dev/null +++ b/internal/daemon/controller/handlers/host_catalogs/grants_test.go @@ -0,0 +1,151 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package host_catalogs_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_catalogs" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/host" + hostplugin "github.com/hashicorp/boundary/internal/host/plugin" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/plugin" + "github.com/hashicorp/boundary/internal/scheduler" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - proj level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project: this +// Scopes [resource]: +// - global +// - org1 +// - proj1 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + rw := db.New(conn) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + sche := scheduler.TestScheduler(t, conn, wrap) + staticRepoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kmsCache) + } + pluginHostRepoFn := func() (*hostplugin.Repository, error) { + return hostplugin.NewRepository(ctx, rw, rw, kmsCache, sche, map[string]plgpb.HostPluginServiceClient{}) + } + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(ctx, rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrap), nil + } + catalogServiceFn := func() (*host.CatalogRepository, error) { + return host.NewCatalogRepository(ctx, rw, rw) + } + s, err := host_catalogs.NewService(ctx, staticRepoFn, pluginHostRepoFn, pluginRepoFn, iamRepoFn, catalogServiceFn, 1000) + require.NoError(t, err) + + org, proj := iam.TestScopes(t, iamRepo) + + hcs := static.TestCatalogs(t, conn, proj.GetPublicId(), 5) + var wantHcs []string + for _, h := range hcs { + wantHcs = append(wantHcs, h.GetPublicId()) + } + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListHostCatalogsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created host catalogs", + input: &pbs.ListHostCatalogsRequest{ + ScopeId: proj.GetPublicId(), + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=host-catalog;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: wantHcs, + }, + { + name: "org role grant this returns all created host catalogs", + input: &pbs.ListHostCatalogsRequest{ + ScopeId: proj.GetPublicId(), + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.PublicId, + GrantStrings: []string{"ids=*;type=host-catalog;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: wantHcs, + }, + { + name: "project role grant this returns all created host catalogs", + input: &pbs.ListHostCatalogsRequest{ + ScopeId: proj.GetPublicId(), + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: proj.PublicId, + GrantStrings: []string{"ids=*;type=host-catalog;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: wantHcs, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListHostCatalogs(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go index 112d762d9..2388e65d2 100644 --- a/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/daemon/controller/handlers/host_catalogs/host_catalog_service.go @@ -775,9 +775,10 @@ func newOutputOpts( pluginMap map[string]*plugin.Plugin, ) ([]handlers.Option, bool, error) { res := perms.Resource{ - Type: resource.HostCatalog, - Id: item.GetPublicId(), - ScopeId: item.GetProjectId(), + Type: resource.HostCatalog, + Id: item.GetPublicId(), + ScopeId: item.GetProjectId(), + ParentScopeId: scopeInfoMap[item.GetProjectId()].GetParentScopeId(), } authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() if len(authorizedActions) == 0 { diff --git a/internal/daemon/controller/handlers/host_sets/grants_test.go b/internal/daemon/controller/handlers/host_sets/grants_test.go new file mode 100644 index 000000000..4fa763221 --- /dev/null +++ b/internal/daemon/controller/handlers/host_sets/grants_test.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package host_sets_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_sets" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + hostplugin "github.com/hashicorp/boundary/internal/host/plugin" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - proj level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project: this +// Scopes [resource]: +// - global +// - org1 +// - proj1 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + rw := db.New(conn) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + sche := scheduler.TestScheduler(t, conn, wrap) + pluginRepoFn := func() (*hostplugin.Repository, error) { + return hostplugin.NewRepository(ctx, rw, rw, kmsCache, sche, map[string]plgpb.HostPluginServiceClient{}) + } + repoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := host_sets.NewService(ctx, repoFn, pluginRepoFn, 1000) + require.NoError(t, err) + + org, proj := iam.TestScopes(t, iamRepo) + + hcs := static.TestCatalogs(t, conn, proj.GetPublicId(), 1) + hc := hcs[0] + + hsets := static.TestSets(t, conn, hc.GetPublicId(), 5) + var wantHostSets []string + for _, h := range hsets { + wantHostSets = append(wantHostSets, h.GetPublicId()) + } + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListHostSetsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created host-set", + input: &pbs.ListHostSetsRequest{ + HostCatalogId: hc.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=host-set;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: wantHostSets, + }, + { + name: "org role grant this returns all created host-set", + input: &pbs.ListHostSetsRequest{ + HostCatalogId: hc.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.PublicId, + GrantStrings: []string{"ids=*;type=host-set;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: wantHostSets, + }, + { + name: "project role grant this returns all created host-set", + input: &pbs.ListHostSetsRequest{ + HostCatalogId: hc.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: proj.PublicId, + GrantStrings: []string{"ids=*;type=host-set;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: wantHostSets, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListHostSets(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/host_sets/host_set_service.go b/internal/daemon/controller/handlers/host_sets/host_set_service.go index d081e2061..918c83b1a 100644 --- a/internal/daemon/controller/handlers/host_sets/host_set_service.go +++ b/internal/daemon/controller/handlers/host_sets/host_set_service.go @@ -755,10 +755,11 @@ func (s Service) deleteFromRepo(ctx context.Context, projectId, id string) (bool func newOutputOpts(ctx context.Context, item host.Set, plg *plugins.PluginInfo, authResults auth.VerifyResults) ([]handlers.Option, bool) { res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Id: item.GetPublicId(), - Type: resource.HostSet, - Pin: item.GetCatalogId(), + ScopeId: authResults.Scope.Id, + ParentScopeId: authResults.Scope.ParentScopeId, + Id: item.GetPublicId(), + Type: resource.HostSet, + Pin: item.GetCatalogId(), } res.Id = item.GetPublicId() idActions := idActionsTypeMap[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype] diff --git a/internal/daemon/controller/handlers/hosts/grants_test.go b/internal/daemon/controller/handlers/hosts/grants_test.go new file mode 100644 index 000000000..6715878bf --- /dev/null +++ b/internal/daemon/controller/handlers/hosts/grants_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hosts_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/hosts" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + hostplugin "github.com/hashicorp/boundary/internal/host/plugin" + "github.com/hashicorp/boundary/internal/host/static" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/scheduler" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - proj level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project: this +// Scopes [resource]: +// - global +// - org1 +// - proj1 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + rw := db.New(conn) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + sche := scheduler.TestScheduler(t, conn, wrap) + pluginRepoFn := func() (*hostplugin.Repository, error) { + return hostplugin.NewRepository(ctx, rw, rw, kmsCache, sche, map[string]plgpb.HostPluginServiceClient{}) + } + repoFn := func() (*static.Repository, error) { + return static.NewRepository(ctx, rw, rw, kmsCache) + } + s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000) + require.NoError(t, err) + + org, proj := iam.TestScopes(t, iamRepo) + + hcs := static.TestCatalogs(t, conn, proj.GetPublicId(), 1) + hc := hcs[0] + + hset := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + var wantHs []string + testHosts := static.TestHosts(t, conn, hc.GetPublicId(), 10) + static.TestSetMembers(t, conn, hset.GetPublicId(), testHosts) + for _, h := range testHosts { + wantHs = append(wantHs, h.GetPublicId()) + } + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListHostsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created hosts", + input: &pbs.ListHostsRequest{ + HostCatalogId: hc.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=host;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: wantHs, + }, + { + name: "org role grant this returns all created hosts", + input: &pbs.ListHostsRequest{ + HostCatalogId: hc.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.PublicId, + GrantStrings: []string{"ids=*;type=host;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: wantHs, + }, + { + name: "project role grant this returns all created hosts", + input: &pbs.ListHostsRequest{ + HostCatalogId: hc.GetPublicId(), + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: proj.PublicId, + GrantStrings: []string{"ids=*;type=host;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: wantHs, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListHosts(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/hosts/host_service.go b/internal/daemon/controller/handlers/hosts/host_service.go index 8ff216926..9002c9929 100644 --- a/internal/daemon/controller/handlers/hosts/host_service.go +++ b/internal/daemon/controller/handlers/hosts/host_service.go @@ -622,10 +622,11 @@ func toPluginInfo(plg *plugin.Plugin) *plugins.PluginInfo { func newOutputOpts(ctx context.Context, item host.Host, plg *plugin.Plugin, authResults auth.VerifyResults) ([]handlers.Option, bool) { res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.Host, - Pin: item.GetCatalogId(), - Id: item.GetPublicId(), + ScopeId: authResults.Scope.Id, + ParentScopeId: authResults.Scope.ParentScopeId, + Type: resource.Host, + Pin: item.GetCatalogId(), + Id: item.GetPublicId(), } res.Id = item.GetPublicId() idActions := idActionsTypeMap[globals.ResourceInfoFromPrefix(res.Id).Subtype] diff --git a/internal/daemon/controller/handlers/managed_groups/grants_test.go b/internal/daemon/controller/handlers/managed_groups/grants_test.go new file mode 100644 index 000000000..f73ba91c9 --- /dev/null +++ b/internal/daemon/controller/handlers/managed_groups/grants_test.go @@ -0,0 +1,126 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package managed_groups_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/auth/ldap" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/managed_groups" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + rw := db.New(conn) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + oidcRepoFn := func() (*oidc.Repository, error) { + // Use a small limit to test that membership lookup is explicitly unlimited + return oidc.NewRepository(ctx, rw, rw, kmsCache, oidc.WithLimit(1)) + } + ldapRepoFn := func() (*ldap.Repository, error) { + // Use a small limit to test that membership lookup is explicitly unlimited + return ldap.NewRepository(ctx, rw, rw, kmsCache, ldap.WithLimit(ctx, 1)) + } + + s, err := managed_groups.NewService(ctx, oidcRepoFn, ldapRepoFn, 1000) + require.NoError(t, err) + + org, _ := iam.TestScopes(t, iamRepo) + + globalDBWrapper, err := kmsCache.GetWrapper(ctx, globals.GlobalPrefix, kms.KeyPurposeDatabase) + require.NoError(t, err) + globalOidcAm := oidc.TestAuthMethod( + t, conn, globalDBWrapper, globals.GlobalPrefix, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + _ = oidc.TestAccount(t, conn, globalOidcAm, "test-subject-1", oidc.WithName("global-1"), oidc.WithDescription("global-1")) + _ = oidc.TestAccount(t, conn, globalOidcAm, "test-subject-2", oidc.WithName("global-2"), oidc.WithDescription("global-2")) + globalMg1 := oidc.TestManagedGroup(t, conn, globalOidcAm, oidc.TestFakeManagedGroupFilter) + globalMg2 := oidc.TestManagedGroup(t, conn, globalOidcAm, oidc.TestFakeManagedGroupFilter) + + orgDBWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + orgLdapAm := ldap.TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap1"}, ldap.WithName(ctx, "global"), ldap.WithDescription(ctx, "global")) + _ = ldap.TestAccount(t, conn, orgLdapAm, "test-login-name-1", ldap.WithMemberOfGroups(ctx, "admin"), ldap.WithName(ctx, "org-1"), ldap.WithDescription(ctx, "org-1")) + _ = ldap.TestAccount(t, conn, orgLdapAm, "test-login-name-2", ldap.WithMemberOfGroups(ctx, "admin"), ldap.WithName(ctx, "org-2"), ldap.WithDescription(ctx, "org-2")) + orgMg := ldap.TestManagedGroup(t, conn, orgLdapAm, []string{"admin", "users"}) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListManagedGroupsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created oidc managed groups", + input: &pbs.ListManagedGroupsRequest{ + AuthMethodId: globalOidcAm.PublicId, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=managed-group;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants}, + }, + }, + wantErr: nil, + wantIDs: []string{globalMg1.PublicId, globalMg2.PublicId}, + }, + { + name: "org role grant this returns all created ldap managed groups", + input: &pbs.ListManagedGroupsRequest{ + AuthMethodId: orgLdapAm.PublicId, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org.PublicId, + GrantStrings: []string{"ids=*;type=managed-group;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + orgMg.PublicId, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListManagedGroups(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go index d8c03480d..3643345f0 100644 --- a/internal/daemon/controller/handlers/managed_groups/managed_group_service.go +++ b/internal/daemon/controller/handlers/managed_groups/managed_group_service.go @@ -998,9 +998,10 @@ func validateListRequest(ctx context.Context, req *pbs.ListManagedGroupsRequest) func newOutputOpts(ctx context.Context, item auth.ManagedGroup, authMethodId string, authResults requestauth.VerifyResults) ([]handlers.Option, bool) { res := perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.ManagedGroup, - Pin: authMethodId, + ScopeId: authResults.Scope.Id, + ParentScopeId: authResults.Scope.ParentScopeId, + Type: resource.ManagedGroup, + Pin: authMethodId, } res.Id = item.GetPublicId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions[globals.ResourceInfoFromPrefix(item.GetPublicId()).Subtype], requestauth.WithResource(&res)).Strings() diff --git a/internal/daemon/controller/handlers/roles/grants_test.go b/internal/daemon/controller/handlers/roles/grants_test.go new file mode 100644 index 000000000..e55c62121 --- /dev/null +++ b/internal/daemon/controller/handlers/roles/grants_test.go @@ -0,0 +1,181 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package roles_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/roles" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// - project level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// - project +// Scopes [resource]: +// - global [globalRole] +// - org1 [org1Role] +// - proj1 [proj1Role] +// - org2 [org2Role] +// - proj2 [proj2Role] +// - proj3 [proj3Role] +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + repoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + kmsCache := kms.TestKms(t, conn, wrap) + s, err := roles.NewService(ctx, repoFn, 1000) + require.NoError(t, err) + + org1, _ := iam.TestScopes(t, iamRepo) + org2, proj2 := iam.TestScopes(t, iamRepo) + proj3 := iam.TestProject(t, iamRepo, org2.PublicId) + + var defaultOrg1Roles []string + org1Roles, err := s.ListRoles(auth.DisabledAuthTestContext(repoFn, org1.GetPublicId()), &pbs.ListRolesRequest{ + ScopeId: org1.GetPublicId(), + }) + require.NoError(t, err) + for _, r := range org1Roles.Items { + defaultOrg1Roles = append(defaultOrg1Roles, r.GetId()) + } + + org2Roles, err := s.ListRoles(auth.DisabledAuthTestContext(repoFn, org2.GetPublicId()), &pbs.ListRolesRequest{ + ScopeId: org2.GetPublicId(), + }) + require.NoError(t, err) + var defaultOrg2Roles []string + for _, r := range org2Roles.Items { + defaultOrg2Roles = append(defaultOrg2Roles, r.GetId()) + } + + proj2Roles, err := s.ListRoles(auth.DisabledAuthTestContext(repoFn, proj2.GetPublicId()), &pbs.ListRolesRequest{ + ScopeId: proj2.GetPublicId(), + }) + require.NoError(t, err) + var defaultProj2Roles []string + for _, r := range proj2Roles.Items { + defaultProj2Roles = append(defaultProj2Roles, r.GetId()) + } + + proj3Roles, err := s.ListRoles(auth.DisabledAuthTestContext(repoFn, proj3.GetPublicId()), &pbs.ListRolesRequest{ + ScopeId: proj3.GetPublicId(), + }) + require.NoError(t, err) + var defaultProj3Roles []string + for _, r := range proj3Roles.Items { + defaultProj3Roles = append(defaultProj3Roles, r.GetId()) + } + + org1Role := iam.TestRole(t, conn, org1.GetPublicId(), iam.WithDescription("org1"), iam.WithName("org1")) + org2Role := iam.TestRole(t, conn, org2.GetPublicId(), iam.WithDescription("org2"), iam.WithName("org2")) + + proj2Role := iam.TestRole(t, conn, proj2.GetPublicId(), iam.WithDescription("proj2"), iam.WithName("proj2")) + proj3Role := iam.TestRole(t, conn, proj3.GetPublicId(), iam.WithDescription("proj3"), iam.WithName("proj3")) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListRolesRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + addRolesAtThisScope bool + wantIDs []string + }{ + { + name: "global role grant this and children returns global and org roles", + input: &pbs.ListRolesRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=role;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + addRolesAtThisScope: true, + wantErr: nil, + wantIDs: append(append([]string{ + org1Role.PublicId, + org2Role.PublicId, + }, defaultOrg1Roles...), defaultOrg2Roles...), + }, + { + name: "org role grant this and children returns org and project roles", + input: &pbs.ListRolesRequest{ + ScopeId: org2.PublicId, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org2.PublicId, + GrantStrings: []string{"ids=*;type=role;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + addRolesAtThisScope: true, + wantErr: nil, + wantIDs: append(append([]string{ + proj2Role.PublicId, + proj3Role.PublicId, + }, defaultProj2Roles...), defaultProj3Roles...), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + + // TestAuthTokenWithRoles creates a default role, so we need to add it to the expected list + // if the grant scope contains 'this' + // This will add the default roles to the expected list of roles + if tc.addRolesAtThisScope { + var rolesAtThisScope []string + rolesAtThisScopeList, err := s.ListRoles(auth.DisabledAuthTestContext(repoFn, tc.input.ScopeId), &pbs.ListRolesRequest{ + ScopeId: tc.input.ScopeId, + }) + require.NoError(t, err) + for _, r := range rolesAtThisScopeList.Items { + rolesAtThisScope = append(rolesAtThisScope, r.GetId()) + } + tc.wantIDs = append(tc.wantIDs, rolesAtThisScope...) + } + + got, finalErr := s.ListRoles(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/roles/role_service.go b/internal/daemon/controller/handlers/roles/role_service.go index 81c743bcf..96ce7a93a 100644 --- a/internal/daemon/controller/handlers/roles/role_service.go +++ b/internal/daemon/controller/handlers/roles/role_service.go @@ -1521,6 +1521,7 @@ func newOutputOpts(ctx context.Context, item *iam.Role, scopeInfoMap map[string] } res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { return nil, false diff --git a/internal/daemon/controller/handlers/scopes/grants_test.go b/internal/daemon/controller/handlers/scopes/grants_test.go new file mode 100644 index 000000000..b3f99944b --- /dev/null +++ b/internal/daemon/controller/handlers/scopes/grants_test.go @@ -0,0 +1,112 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package scopes_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// Scopes [resource]: +// - global [globalScope] +// - org1 +// - proj1 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + repoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000) + require.NoError(t, err) + + org1, proj1 := iam.TestScopes(t, iamRepo) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListScopesRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created scopes", + input: &pbs.ListScopesRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=scope;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + org1.PublicId, + proj1.PublicId, + }, + }, + { + name: "org role grant this returns all created scopes", + input: &pbs.ListScopesRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org1.PublicId, + GrantStrings: []string{"ids=*;type=scope;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + proj1.PublicId, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListScopes(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index 951015de3..a04a56df0 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -1138,9 +1138,10 @@ func validateDestroyKeyVersionRequest(req *pbs.DestroyKeyVersionRequest) error { func newOutputOpts(ctx context.Context, item *iam.Scope, authResults auth.VerifyResults, scopeInfoMap map[string]*pb.ScopeInfo) ([]handlers.Option, bool, error) { res := perms.Resource{ - Type: resource.Scope, - Id: item.GetPublicId(), - ScopeId: item.GetParentId(), + Type: resource.Scope, + Id: item.GetPublicId(), + ScopeId: item.GetParentId(), + ParentScopeId: scopeInfoMap[item.GetParentId()].GetParentScopeId(), } authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), idActionsById(item.GetPublicId()), auth.WithResource(&res)).Strings() diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index d934d2175..6d656b974 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -1983,6 +1983,7 @@ func newOutputOpts(ctx context.Context, item target.Target, authResults auth.Ver if outputFields.Has(globals.ScopeField) { outputOpts = append(outputOpts, handlers.WithScope(authzScopes[item.GetProjectId()])) } + pr.ParentScopeId = authzScopes[item.GetProjectId()].GetParentScopeId() if outputFields.Has(globals.AuthorizedActionsField) { authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&pr)).Strings() outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) diff --git a/internal/daemon/controller/handlers/targets/tcp/grants_test.go b/internal/daemon/controller/handlers/targets/tcp/grants_test.go new file mode 100644 index 000000000..cb9103592 --- /dev/null +++ b/internal/daemon/controller/handlers/targets/tcp/grants_test.go @@ -0,0 +1,107 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tcp_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/target" + "github.com/hashicorp/boundary/internal/target/tcp" + + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// Scopes [resource]: +// - org1 +// - proj1 +// - proj2 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + s, err := testService(t, ctx, conn, kms, wrapper) + require.NoError(t, err) + + org1, proj1 := iam.TestScopes(t, iamRepo) + _, proj2 := iam.TestScopes(t, iamRepo) + + target1 := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), "test address-1", target.WithAddress("8.8.8.8")) + target2 := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), "test address-2", target.WithAddress("8.8.8.8")) + target3 := tcp.TestTarget(ctx, t, conn, proj2.GetPublicId(), "test address-2", target.WithAddress("8.8.8.8")) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListTargetsRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "org role grant this returns all created targets", + input: &pbs.ListTargetsRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org1.GetPublicId(), + GrantStrings: []string{"ids=*;type=target;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{target1.GetPublicId(), target2.GetPublicId()}, + }, + { + name: "project role grant this returns all created targets", + input: &pbs.ListTargetsRequest{ + ScopeId: proj2.GetPublicId(), + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: proj2.GetPublicId(), + GrantStrings: []string{"ids=*;type=target;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{target3.GetPublicId()}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kms, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrapper, tok, iamRepo) + got, finalErr := s.ListTargets(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/users/grants_test.go b/internal/daemon/controller/handlers/users/grants_test.go new file mode 100644 index 000000000..a4bc80c48 --- /dev/null +++ b/internal/daemon/controller/handlers/users/grants_test.go @@ -0,0 +1,312 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package users_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + talias "github.com/hashicorp/boundary/internal/alias/target" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/users" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/require" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// - org level +// Grant - what IAM grant scope is set for the permission +// - global: descendant +// - org: children +// Scopes [resource]: +// - global [globalUser] +// - org1 [org1User] +// - org1User1 +// - org2 [org2User] +// - org2User1 +// - org2User2 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + repoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + aliasRepoFn := func() (*talias.Repository, error) { + return talias.NewRepository(context.Background(), rw, rw, kmsCache) + } + s, err := users.NewService(ctx, repoFn, aliasRepoFn, 1000) + require.NoError(t, err) + org1, _ := iam.TestScopes(t, iamRepo) + org2, _ := iam.TestScopes(t, iamRepo) + + globalUser := iam.TestUser(t, iamRepo, globals.GlobalPrefix, iam.WithDescription("global"), iam.WithName("global")) + org1User1 := iam.TestUser(t, iamRepo, org1.GetPublicId(), iam.WithDescription("org1"), iam.WithName("org1")) + org2User1 := iam.TestUser(t, iamRepo, org2.GetPublicId(), iam.WithDescription("org2-1"), iam.WithName("org2-1")) + org2User2 := iam.TestUser(t, iamRepo, org2.GetPublicId(), iam.WithDescription("org2-2"), iam.WithName("org2-2")) + + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListUsersRequest + // set this flag when the listing user has permission to list global + // AND the test attempts to list users at global scope + // users which gets created as a part of token generation + includeTestUsers bool + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this and children recursive list at global returns global and org users", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + includeTestUsers: true, + wantErr: nil, + wantIDs: []string{ + globals.AnyAuthenticatedUserId, + globals.AnonymousUserId, + globals.RecoveryUserId, + globalUser.PublicId, + org1User1.PublicId, + org2User1.PublicId, + org2User2.PublicId, + }, + }, + { + name: "org role grant this and children recursive list at org returns org user", + input: &pbs.ListUsersRequest{ + ScopeId: org2.PublicId, + Recursive: true, + }, + includeTestUsers: false, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org2.PublicId, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + org2User1.PublicId, + org2User2.PublicId, + }, + }, + { + name: "global role grant children recursive list at org returns org user", + input: &pbs.ListUsersRequest{ + ScopeId: org2.PublicId, + Recursive: true, + }, + includeTestUsers: false, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + org2User1.PublicId, + org2User2.PublicId, + }, + }, + { + name: "global role grant children recursive list at global returns all org users", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + includeTestUsers: false, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + wantErr: nil, + wantIDs: []string{ + org1User1.PublicId, + org2User1.PublicId, + org2User2.PublicId, + }, + }, + { + name: "org1 role grant this recursive list at global returns org1 users", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + includeTestUsers: false, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org1.PublicId, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{ + org1User1.PublicId, + }, + }, + { + name: "individual org grant this recursive list at global returns org user", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + includeTestUsers: false, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org1.PublicId, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + { + RoleScopeID: org2.PublicId, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{ + org1User1.PublicId, + org2User1.PublicId, + org2User2.PublicId, + }, + }, + { + name: "global role grant this recursive list at org returns no user", + input: &pbs.ListUsersRequest{ + ScopeId: org2.PublicId, + Recursive: true, + }, + includeTestUsers: false, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{}, + }, + { + name: "global role grant this recursive list at global returns only global user", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + includeTestUsers: true, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{ + globals.AnyAuthenticatedUserId, + globals.AnonymousUserId, + globals.RecoveryUserId, + globalUser.PublicId, + }, + }, + { + name: "global role grant children non-recursive list at global returns forbidden error", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: false, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + wantErr: handlers.ForbiddenError(), + wantIDs: nil, + }, + { + name: "org role grant children non-recursive list at global returns forbidden error", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: false, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: org2.PublicId, + GrantStrings: []string{"ids=*;type=user;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeChildren}, + }, + }, + wantErr: handlers.ForbiddenError(), + wantIDs: nil, + }, + { + name: "no grant recursive list returns forbidden error", + input: &pbs.ListUsersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{}, + wantErr: handlers.ForbiddenError(), + wantIDs: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate) + t.Cleanup(func() { + // deleting user to keep assertions clean since we're listing users over and over + _, _ = iamRepo.DeleteUser(ctx, tok.IamUserId) + }) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo) + got, finalErr := s.ListUsers(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + if tc.includeTestUsers { + tc.wantIDs = append(tc.wantIDs, tok.IamUserId) + } + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/users/user_service.go b/internal/daemon/controller/handlers/users/user_service.go index 83b09ed97..6b54dd819 100644 --- a/internal/daemon/controller/handlers/users/user_service.go +++ b/internal/daemon/controller/handlers/users/user_service.go @@ -1067,6 +1067,7 @@ func newOutputOpts(ctx context.Context, item *iam.User, scopeInfoMap map[string] } res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)) if len(authorizedActions) == 0 { return nil, false diff --git a/internal/daemon/controller/handlers/workers/grants_test.go b/internal/daemon/controller/handlers/workers/grants_test.go new file mode 100644 index 000000000..dc1aef0d2 --- /dev/null +++ b/internal/daemon/controller/handlers/workers/grants_test.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package workers_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/daemon/controller/auth" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers" + "github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers" + "github.com/hashicorp/boundary/internal/db" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/server" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" +) + +// TestGrants_ReadActions tests read actions to assert that grants are being applied properly +// +// Role - which scope the role is created in +// - global level +// Scopes [resource]: +// - globalWorker1 +// - globalWorker2 +func TestGrants_ReadActions(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + rw := db.New(conn) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + repoFn := func() (*server.Repository, error) { + return server.NewRepository(ctx, rw, rw, kms) + } + + workerAuthRepo, err := server.NewRepositoryStorage(ctx, rw, rw, kms) + require.NoError(t, err) + workerAuthRepoFn := func() (*server.WorkerAuthRepositoryStorage, error) { + return workerAuthRepo, nil + } + + s, err := workers.NewService(context.Background(), repoFn, iamRepoFn, workerAuthRepoFn, nil) + require.NoError(t, err) + globalWorker1 := server.TestPkiWorker(t, conn, wrapper, + server.WithName("worker-1"), + server.WithDescription("worker-1"), + server.WithNewIdFunc(func(ctx context.Context) (string, error) { + return server.NewWorkerIdFromScopeAndName(ctx, scope.Global.String(), "worker-1") + }), + ) + globalWorker2 := server.TestPkiWorker(t, conn, wrapper, + server.WithName("worker-2"), + server.WithDescription("worker-2"), + server.WithNewIdFunc(func(ctx context.Context) (string, error) { + return server.NewWorkerIdFromScopeAndName(ctx, scope.Global.String(), "worker-2") + }), + ) + t.Run("List", func(t *testing.T) { + testcases := []struct { + name string + input *pbs.ListWorkersRequest + rolesToCreate []authtoken.TestRoleGrantsForToken + wantErr error + wantIDs []string + }{ + { + name: "global role grant this returns all created workers", + input: &pbs.ListWorkersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=worker;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: nil, + wantIDs: []string{globalWorker1.PublicId, globalWorker2.PublicId}, + }, + { + name: "global role grant this with a non-applicable type throws an error", + input: &pbs.ListWorkersRequest{ + ScopeId: globals.GlobalPrefix, + Recursive: true, + }, + rolesToCreate: []authtoken.TestRoleGrantsForToken{ + { + RoleScopeID: globals.GlobalPrefix, + GrantStrings: []string{"ids=*;type=group;actions=list,read"}, + GrantScopes: []string{globals.GrantScopeThis}, + }, + }, + wantErr: handlers.ApiErrorWithCode(codes.PermissionDenied), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tok := authtoken.TestAuthTokenWithRoles(t, conn, kms, globals.GlobalPrefix, tc.rolesToCreate) + fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrapper, tok, iamRepo) + got, finalErr := s.ListWorkers(fullGrantAuthCtx, tc.input) + if tc.wantErr != nil { + require.ErrorIs(t, finalErr, tc.wantErr) + return + } + require.NoError(t, finalErr) + var gotIDs []string + for _, g := range got.Items { + gotIDs = append(gotIDs, g.GetId()) + } + require.ElementsMatch(t, tc.wantIDs, gotIDs) + }) + } + }) +} diff --git a/internal/daemon/controller/handlers/workers/worker_service.go b/internal/daemon/controller/handlers/workers/worker_service.go index db28d8582..b5163d118 100644 --- a/internal/daemon/controller/handlers/workers/worker_service.go +++ b/internal/daemon/controller/handlers/workers/worker_service.go @@ -167,6 +167,7 @@ func (s Service) ListWorkers(ctx context.Context, req *pbs.ListWorkersRequest) ( for _, item := range ul { res.Id = item.GetPublicId() res.ScopeId = item.GetScopeId() + res.ParentScopeId = scopeInfoMap[item.GetScopeId()].GetParentScopeId() authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), IdActions, auth.WithResource(&res)).Strings() if len(authorizedActions) == 0 { continue diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 6af5836df..970a920ac 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -223,6 +223,36 @@ func TestRole(t testing.TB, conn *db.DB, scopeId string, opt ...Option) *Role { return role } +// TestRoleWithGrants creates a role suitable for testing along with grants +// Functional options for GrantScopeIDs aren't used to express that +// this function does not provide any default grant scope unlike TestRole +func TestRoleWithGrants(t testing.TB, conn *db.DB, scopeId string, grantScopeIDs []string, grants []string) *Role { + t.Helper() + + ctx := context.Background() + require := require.New(t) + rw := db.New(conn) + + role, err := NewRole(ctx, scopeId) + require.NoError(err) + id, err := newRoleId(ctx) + require.NoError(err) + role.PublicId = id + require.NoError(rw.Create(ctx, role)) + require.NotEmpty(role.PublicId) + + for _, gsi := range grantScopeIDs { + gs, err := NewRoleGrantScope(ctx, id, gsi) + require.NoError(err) + require.NoError(rw.Create(ctx, gs)) + role.GrantScopes = append(role.GrantScopes, gs) + } + for _, g := range grants { + _ = TestRoleGrant(t, conn, role.PublicId, g) + } + return role +} + func TestRoleGrant(t testing.TB, conn *db.DB, roleId, grant string, opt ...Option) *RoleGrant { t.Helper() require := require.New(t)