Normalise grants and roles tables to improve grants query performance (#5846)

* Create function to define valid set of scopes for each resource (#5558)

* Create function to define valid set of scopes for each resource

* chore(iam): Update validScopeTypes() to use scope.AllowedIn()

* chore(scope): Initialize with iota

* feat(scope): Return an error instead scope.Unknown

* feat(iam): Replace interface method `validScopeTypes` with `getResourceType`

This allows us to call scope.AllowedIn() in one place vs in each implementation of `validScopeTypes`

* chore(resource): Refactor other package functions into methods on resource.Type

* fix(scope): Add defensive checks around invalid resource types

* docs(resource): Add AllowedIn() to the areas to update when adding a new resource type

* docs(resource): Improve error message when an invalid type is provided

* test(grants): GrantsForUsers tests for Group resource (#5443)

* test(grants): WIP: First stab at group associations

* test(grants): Add GrantsForUser test for groups

* chore(grants): Consolidate repetitive setup logic into functions

* test(grants): Add GrantsForUser test for managed groups

* test(grants): Add another user with different grants

Ensure that non-applicable grants should not be returned because they are not applicable to the user

* chore(grants): cleanup

* chore(grants): Move common setup steps into a helper function

* feat: Define new grants tables (#5486)

Create new tables for grants:

1. `iam_role_global`:
  Roles that are placed in the global scope will be persisted in the `iam_role_global` table. A global role has a `grant_scope` which must be one of:
  * descendants
  * children
  * individual

  This enforces that a global role's grants either apply to:
  *  All orgs and projects.
  *  All orgs.
  *  An individual set of orgs and/or projects.

  When the `grant_scope` is set to `individual`, entries for the specific set of orgs and/or projects can be added to the 
  `iam_role_global_individual_grant_scope` table.

  Separately, a global role can be set to also apply its grant to the global scope by setting `grant_this_role_scope` to true.

2. `iam_role_org`:
  Roles that are placed in an org scope will be persisted in the `iam_role_org` table. An org role has a `grant_scope` which must be one of:
  * children
  * individual

  This enforces that an org role's grants either apply to:
  * All projects in the org.
  * An individual set of projects in the org.

  When the `grant_scope` is set to `individual`, entries for the specific set of projects can be added to the 
  `iam_role_org_individual_grant_scope` table.
  **NOTE**: The projects must belong to the org's scope

  Separately, an org role can be set to also apply its grant to the org by setting `grant_this_role_scope` to true.

3. `resource_enm`: 
  Contains all boundary resources. This is used by `iam_grant` to set the resource from a canonical_grant.

4. `iam_grant`
Stores the canonical grant string and the resource for filtering on specific grants.

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>
Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* add name scope id unique constraints to all iam_role tables (#5623)

* Bosorawis domain iam role subtypes (#5626)

* add subtype storage definitions

* make gen

* add all subtype definitions

* add const for grant scope individual

* remove unnecessary baseRole subtype

* add new proto files to make target protobuild

* make gen to get protoc-go-inject-tag

* add clone, setTableName, and GetScope tests

* add ResourceType and Actions test

* add create and delete tests for globalROle

* finish create and delete tests

* add trigger for deleting base role

* add trigger to sync update_time back to base iam_role table

* add update tests

* fix missing err checks

* fix iam_role delete subtype trigger function name and use new.update_time instead of now()

* add struct documentation to role subtypes

* add version update check

* add todo comment:

* bosorawis sql split global grants scope table (#5638)

* split iam_role_global_individual_grant_scope to have separate tables for org and project

* small comment change

* small comment change

* WIP: add tests

* remove grant_scope as immutable column

* add trigger to delete individual grant scope when grant_scope changes

* add a test that covers changing grant_scope

* rename function and trigger in iam_role_global

* improve assertion in sqltest for iam_role_global

* update iam_role_org to delete redundant grants scope

* minor comment fix

* no longer handle individual grant scope deletion with triggers and rename some functions

* add trigger test for grant_scope

* rename delete_base_iam_role to delete_iam_role_subtype

* SQL formatting use now() instead of interval

* bosorawis domain iam implement getRoleScopeType (#5629)

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* rename test

* change error code to RecordNotFound

* Update internal/iam/repository_role.go

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* switch to slice instead of counter

---------

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* domain: iam: upgrade repository code to use new tables (#5643)

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* split iam_role_global_individual_grant_scope to have separate tables for org and project

* small comment change

* small comment change

* WIP: add tests

* remove grant_scope as immutable column

* add trigger to delete individual grant scope when grant_scope changes

* add a test that covers changing grant_scope

* rename function and trigger in iam_role_global

* improve assertion in sqltest for iam_role_global

* update iam_role_org to delete redundant grants scope

* minor comment fix

* no longer handle individual grant scope deletion with triggers and rename some functions

* rename test

* add all subtype definitions

* remove unnecessary baseRole subtype

* add clone, setTableName, and GetScope tests

* add ResourceType and Actions test

* add create and delete tests for globalROle

* finish create and delete tests

* add trigger for deleting base role

* add trigger to sync update_time back to base iam_role table

* add update tests

* fix missing err checks

* fix iam_role delete subtype trigger function name and use new.update_time instead of now()

* add struct documentation to role subtypes

* add version update check

* implement getRoleScopeId

* implement getRoleScopeId

* save

* remove struct embedding from iam.Role

* fix tests to use new iam.Role definition

* repository_role_test.go move to new iam.Role model

* repository_principal_role_test.go use new iam.Role model

* repository_role_grant_test.go use new iam.Role model in test

* add oplog info to sql schema

* internal/iam/testing.go use new role schema in TestRole

* add toRole helper function to all role subtype

* remove tests that are no longer relevant

* internal/iam/repository_scope.go use new iam model

* internal/iam/repository_role_grant.go use new iam model

* internal/iam/repository_principal_role.go use new iam model

* internal/iam/repository_role_test.go add test case for global scoped role

* internal/iam/repository_grant_scope.go use new iam model

* fix query

* make create and lookup role work and add tests

* add role id to getRoleScopeId error message

* make DeleteRole work with new model and add tests

* fix update

* ensure oplog.ReplayableMessage is implemented on all role subtypes

* internal/iam/repository_role_grant.go fix slugging version properly

* internal/iam/repository_role.go minor correction to error message saying org instead of scope

* internal/iam/repository_role_test.go add more update tests

* add immutable_fields tests

* fix rebase

* change error code to RecordNotFound

* refactor to use getScopeType

* fix delete test

* add getRoleScope utility function

* repository_principal_role.go: refactor to remove multiple switch statements

* repository_role_grant.go: refactor to reduce LOC

* repository_role.go small refactor to use alloc func

* repository_grant_scope.go refactor

* review comments

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* rename test

* change error code to RecordNotFound

* Update internal/iam/repository_role.go

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* switch to slice instead of counter

* fix merge mistakes

* handling special scopes in test function

* fix TestRoleWithGrants

* fix minor typo

* make gen

* fix comment typos

* Bosorawis domain iam role use new model list role (#5676)

* add and use new list roles query

* run make gen

* tweaked returned error

* replace tabs with spaces in query string

* missed one tab

* remove leading spaces

---------

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* Domain: iam: Repository: update list-grant-scope and test setup to use new model (#5679)

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* split iam_role_global_individual_grant_scope to have separate tables for org and project

* small comment change

* small comment change

* WIP: add tests

* remove grant_scope as immutable column

* add trigger to delete individual grant scope when grant_scope changes

* add a test that covers changing grant_scope

* rename function and trigger in iam_role_global

* improve assertion in sqltest for iam_role_global

* update iam_role_org to delete redundant grants scope

* minor comment fix

* no longer handle individual grant scope deletion with triggers and rename some functions

* rename test

* add all subtype definitions

* remove unnecessary baseRole subtype

* add clone, setTableName, and GetScope tests

* add ResourceType and Actions test

* add create and delete tests for globalROle

* finish create and delete tests

* add trigger for deleting base role

* add trigger to sync update_time back to base iam_role table

* add update tests

* fix missing err checks

* fix iam_role delete subtype trigger function name and use new.update_time instead of now()

* add struct documentation to role subtypes

* add version update check

* implement getRoleScopeId

* implement getRoleScopeId

* save

* remove struct embedding from iam.Role

* fix tests to use new iam.Role definition

* repository_role_test.go move to new iam.Role model

* repository_principal_role_test.go use new iam.Role model

* repository_role_grant_test.go use new iam.Role model in test

* add oplog info to sql schema

* internal/iam/testing.go use new role schema in TestRole

* add toRole helper function to all role subtype

* remove tests that are no longer relevant

* internal/iam/repository_scope.go use new iam model

* internal/iam/repository_role_grant.go use new iam model

* internal/iam/repository_principal_role.go use new iam model

* internal/iam/repository_role_test.go add test case for global scoped role

* internal/iam/repository_grant_scope.go use new iam model

* fix query

* make create and lookup role work and add tests

* add role id to getRoleScopeId error message

* make DeleteRole work with new model and add tests

* fix update

* ensure oplog.ReplayableMessage is implemented on all role subtypes

* internal/iam/repository_role_grant.go fix slugging version properly

* internal/iam/repository_role.go minor correction to error message saying org instead of scope

* internal/iam/repository_role_test.go add more update tests

* add immutable_fields tests

* fix rebase

* change error code to RecordNotFound

* refactor to use getScopeType

* fix delete test

* add getRoleScope utility function

* repository_principal_role.go: refactor to remove multiple switch statements

* repository_role_grant.go: refactor to reduce LOC

* repository_role.go small refactor to use alloc func

* repository_grant_scope.go refactor

* review comments

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* rename test

* change error code to RecordNotFound

* Update internal/iam/repository_role.go

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* switch to slice instead of counter

* fix merge mistakes

* handling special scopes in test function

* fix TestRoleWithGrants

* fix minor typo

* make gen

* fix comment typos

* Bosorawis domain iam role use new model list role (#5676)

* add and use new list roles query

* run make gen

* tweaked returned error

* replace tabs with spaces in query string

* missed one tab

* remove leading spaces

* move ListRoleGrantScopes to repository_grant_scope.go

* rename repository_grant_scope to repository_role_grant_scope

* add proto definition for global role individual grant scope tables

* fix test from removing embeded struct from RoleGrantScope

* add grant_scope to proto definition

* implement GlobalRoleIndividualOrgGrantScope and GlobalRoleIndividualProjectGrantScope

* update comment

* run make gen to update comment

* implement OrgRoleIndividualGrantScope and add tests

* implement part of ListRoleGrantScopes

* Add more test

* add more test cases and remove add-grants test

* unexport listRoleGrantScopes

* use reader from function parameter instead of struct method

* rename test to match actual function

* run make gen

* unexport individual grants structs

* unexport individual grants structs - missed one file

* change TestRole and TestRoleGrantScope function to support new model

* add validation for special scopes

* add role_org_individual_grant_scope.pb.go to protobuild make target

* remove dead code from listRoleGrantScopes

* fix testRoleGrantScopeSpecial not handling org role special scope properly

* add back query removed by rebase

---------

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* feat: grantsForUser for Global Resources (#5612)

* feat: grantsForUser for Global Resources

add query to fetch grants for a user for resources that are only globally scoped

* Update query based on change to bifurcate individual table

* Create subtests for different resources

* Return grant.grant_scope instead of the request scope

* Remove 'individual' subquery & unused reqScope parameter

* Use sql.Named for better readability

* Fix op function name

* Remove individual grant scope logic from global resource repo function

No need to handle individual grant scopes since global resources can only be queried via 'this' grant scope at the global scope.

* Fix row scan order

* Remove data gen function

* Adjust query formatting

Remove canonical_grant filter from query. `iam_grant.canonical_grant` is a primary key, so it can't be null anyway -- no need to filter out null canonical grants

* Use the consts for u_auth and u_anon

* Specify "empty" instead of "NULL" in struct field comment

* Build query args with `pq.Array` instead of `fmt.Sprintf`

* Fix TestGrantsForUserGlobalResources

No longer using a hard-coded value for roleVersion

* Refactor grantsForUserGlobalResources tests into testcases

* go mod tidy

* Update query comment for correctness

---------

Co-authored-by: dkanney <dkanney@terpmail.umd.edu>
Co-authored-by: dkanney <david.kanney@hashicorp.com>

* Bosorawis domain iam implement role grant scopes all (#5701)

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* split iam_role_global_individual_grant_scope to have separate tables for org and project

* small comment change

* small comment change

* WIP: add tests

* remove grant_scope as immutable column

* add trigger to delete individual grant scope when grant_scope changes

* add a test that covers changing grant_scope

* rename function and trigger in iam_role_global

* improve assertion in sqltest for iam_role_global

* update iam_role_org to delete redundant grants scope

* minor comment fix

* no longer handle individual grant scope deletion with triggers and rename some functions

* rename test

* add all subtype definitions

* remove unnecessary baseRole subtype

* add clone, setTableName, and GetScope tests

* add ResourceType and Actions test

* add create and delete tests for globalROle

* finish create and delete tests

* add trigger for deleting base role

* add trigger to sync update_time back to base iam_role table

* add update tests

* fix missing err checks

* fix iam_role delete subtype trigger function name and use new.update_time instead of now()

* add struct documentation to role subtypes

* add version update check

* implement getRoleScopeId

* implement getRoleScopeId

* save

* remove struct embedding from iam.Role

* fix tests to use new iam.Role definition

* repository_role_test.go move to new iam.Role model

* repository_principal_role_test.go use new iam.Role model

* repository_role_grant_test.go use new iam.Role model in test

* add oplog info to sql schema

* internal/iam/testing.go use new role schema in TestRole

* add toRole helper function to all role subtype

* remove tests that are no longer relevant

* internal/iam/repository_scope.go use new iam model

* internal/iam/repository_role_grant.go use new iam model

* internal/iam/repository_principal_role.go use new iam model

* internal/iam/repository_role_test.go add test case for global scoped role

* internal/iam/repository_grant_scope.go use new iam model

* fix query

* make create and lookup role work and add tests

* add role id to getRoleScopeId error message

* make DeleteRole work with new model and add tests

* fix update

* ensure oplog.ReplayableMessage is implemented on all role subtypes

* internal/iam/repository_role_grant.go fix slugging version properly

* internal/iam/repository_role.go minor correction to error message saying org instead of scope

* internal/iam/repository_role_test.go add more update tests

* add immutable_fields tests

* fix rebase

* change error code to RecordNotFound

* refactor to use getScopeType

* fix delete test

* add getRoleScope utility function

* repository_principal_role.go: refactor to remove multiple switch statements

* repository_role_grant.go: refactor to reduce LOC

* repository_role.go small refactor to use alloc func

* repository_grant_scope.go refactor

* review comments

* implement getRoleScopeId

* move query to query.go

* improve notfound err message

* improve other err messages

* use named parameter and move getRoleScopeId implementation

* moved getRoleScopeId test

* rename getRoleScopeId to getRoleScopeType

* fix public_id ambiguous error

* undo unintended change to getUserWithAccount

* fix the correct query

* rename test

* change error code to RecordNotFound

* Update internal/iam/repository_role.go

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* switch to slice instead of counter

* fix merge mistakes

* handling special scopes in test function

* fix TestRoleWithGrants

* fix minor typo

* make gen

* fix comment typos

* Bosorawis domain iam role use new model list role (#5676)

* add and use new list roles query

* run make gen

* tweaked returned error

* move ListRoleGrantScopes to repository_grant_scope.go

* rename repository_grant_scope to repository_role_grant_scope

* add proto definition for global role individual grant scope tables

* fix test from removing embeded struct from RoleGrantScope

* add grant_scope to proto definition

* implement GlobalRoleIndividualOrgGrantScope and GlobalRoleIndividualProjectGrantScope

* update comment

* run make gen to update comment

* implement OrgRoleIndividualGrantScope and add tests

* implement part of ListRoleGrantScopes

* Add more test

* add more test cases and remove add-grants test

* unexport listRoleGrantScopes

* use reader from function parameter instead of struct method

* rename test to match actual function

* run make gen

* unexport individual grants structs

* unexport individual grants structs - missed one file

* change TestRole and TestRoleGrantScope function to support new model

* add validation for special scopes

* add role_org_individual_grant_scope.pb.go to protobuild make target

* remove dead code from listRoleGrantScopes

* fix testRoleGrantScopeSpecial not handling org role special scope properly

* change proto grant_this default to true

* make TestRole readback the role to get updated version

* implement toRoleGrantScope function on the subtypes

* implement conversion function

* add tests and AddRoleGrantScope before refactor

* working delete grant scope before refactor

* remove unused functions

* refactor repository_role_grant_scope.go

* add tests for SetRoleGrantScopes

* all tests passing

* refactor repository_role_grant_scope.go again

* run make gen

* no longer embed Resource in roleScopeGranter interface and make interface all internal functions

* add additional test case

* fix minor typo

* add a constraint check for iam_role_org.grant_scope

* refactor and comment repository_role_grant_scope.go and add test cases

* remove unused code

* rename roleScopeGranter to roleGrantScopeUpdater

* interface and function rename

* address PR comments

* remove redundant constraint on iam_role_org

* address pr comments

* grant this scope to role by default when creating a role

* tweak comments and variable names

---------

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* feat: add grant_this_role_scope column to iam_role_project (#5738)

* add grant_this_role_scope and sqltes for iam_project_role

* update role_project.proto to add grant_this_role_scope field

* fix missing fields in role_project.proto

* support GrantThisRoleScope field in iam/testing.go

* update repository_scope to set GrantThisRoleScope to true

* add GrantThisRoleScope to projec role tests to role_test.go

* add grant_this_role_scope check to query.go

* update roleGrantScopeUpdater to split setGrantScope to setHierarchicalGrantScope and removeHierarchicalGrantScope

* update create_role to set default vaule for iam_role_project.grant_this_role_scope

* update set, add, delete, list role grant scopes to support grant_this_role_scope in project_role

* rename methods

* run make gen

* make TestRole not bump version when creating roles with default 'this' grants

* address pr comments

* fix variable names

* feat: grantsForUser for Org resources (#5663)

* Create grantsForUser query for Org resources

* Finish & format query

* Change name of field in query

From 'role_type' to 'role_parent_scope_id'

* Finish grantsForUserOrgResources test

- Refactor to match the usual testcase pattern
- Use test functions to create test resources

* Remove unnecessary check against GrantScopeThis

"this" now has its own field and no longer lives alongside the other grant scopes, so we should not check for it against grantScope

* Ignore children & descendant grant scopes when querying org resources

The only grant_scope that mattters when querying org resources is 'this'

* Refactor query to simplify repo function

Create separate CTEs for each case: global special (children or descendant), global individual, and org (this). This allows us to query directly into perms.GrantTuple; no additional logic required.

* feat: grantsForUser for Project resources (#5669)

* Reuse testInput

* Create grantsForUserProjectResources query

* Create grantsForUserProjectResources repo func

* Create tests for grantsForUserProjectResources

* Split up CTEs by grant scope and simplify repo function

* Add test cases for missing reqScope id

* Remove unnecessary join to iam_scope_org table

* Reuse 'roles_with_grants' CTE across the grantsForUser queriers

* Change reqScope parameter type from `Scope` to `string` (#5748)

* Change 'reqScope' parameter type to: string

* Simplify grantsForUserGlobalQuery & repo func

* Test: GrantsForUser for Accounts for various user to role relationships (#5631)

* test(iam): Refactor GrantsForUser tests into a single test

* test(iam): Extend TestGrantsForUser to include Account resource

Note: These tests will fail until GrantsForUser is refactored to return only grants whose scopes are applicable to this resource

* test(iam): test(iam): Add Target resource to TestGrantsForUser

* feat: grantsForUser for recursive Global or Org resources (#5747)

* Create query: grantsForUserGlobalAndOrgResourcesQuery

* Create repo function: grantsForUserGlobalAndOrgResourcesRecursive

* Create tests for grantsForUserGlobalAndOrgResources

* Restrict recursive list for Global/Org resources to global scope only

Return error for non-recursive scopes

* Remove unused option parameter

* Add additional scope and test cases

- Test for grants against a resource with no permission granted for it
- Test for grants against a specific resource id without an explicit type set in the grant string
- Add a project scope to ensure its grants aren't returned

* Return error when passed 'Unknown' or 'All' resource type

* Use constants for 'unknown' & '*' resources

* feat: grantsForUser for recursive Project resources (#5783)

* Create query: grantsForUserProjectResourcesGlobalScopeQuery

* Create query: grantsForUserProjectResourcesOrgScopeQuery

* Create repo function: grantsForUserProjectResourcesRecursiveScopes

* Add recursive testcases to existing grantsForUserProjectResource test function

* Address PR feedback

- Add testcases for Unknown & All resource types
- Change a grant string to a pinned resource. Its resource type changed from Target type to Unknown type
- Call through to grantsForUserProjectResources when reqScopeId is a project scope (i.e. a non-recursve scope)

* Add 'Recursive' to query var naming

* feat: migration hook grants refactor (#5733)

* scaffolding

* comments

* rename migration number

* implement FindIllegalAssociations for 97006 hook

* refactor findIllegalAssociation query

* implement RepairIllegalAssociations and refactor queries

* make gen

* fix missing err check

* pr feedback fix hook fix message wording

* sql formatting

* update comment

* sql formatting

* Apply suggestions from code review

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* change 'illegal' to 'invalid'

* pr comment feedback

* small comment update

---------

Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* feat: grantsForUser for Global/Org/Project resources (#5807)

* Change naming to be consistent with other grantsForUser functions

* Create grantsForUser query for global-scoped Global/Org/Proj resources

* Create grantsForUser repo function for Global/Org/Proj resources

* Create tests for grantsForUserGlobalOrOrgOrProjectResources

* Create grantsForUser query for org-scoped Globa/Org/Proj resources

* Add tests for grantsForUser Global/Org/Proj resource query's org & project request_scopes

* Add clarifying comments and shorter naming convention

* Add 'children' grant_scoped org roles to result set

* feat: grants: add resource type parameters to GrantsForUser (#5750)

* add resource.Type to auth.Verify callchain

* remove WithType option

* update internal/auth/additional_verification_test.go to no longer use WithType

* handle explicit resource type in aclAndGrantHashForUser

* update interceptor_test.go to use resource.Scope for testing auth.Verify

* use resource.Scope to test Audit events

* add godoc comment on GrantsForUser

* add recursive options to iam

* add recursive options to auth

* internal/auth/ldap/managed_group_role_grants_test.go fix GrantsForUser signature

* internal/daemon/controller/handlers/accounts/account_service_test.go fix GrantsForUser signature

* update GrantsForUser godoc

* replace GrantsForUser tests with a new version with scoped down grants

* add mix type assignment to TestGrantsForUser

* move GrantsForUser test to internal/iam/repository_role_grant_ext_test.go

* remove unused options

* rewrite GrantsForUser tests to match the new expectation

* handle recursive call in s.authResult

* fix missing import from rebase

* resolve ListResolvableAlias destination permissions properly

* use global scope for ListResolvableAlias aclAndGrantHashForUser

* make auth.WithRecursive take boolean input

* make iam.WithRecursive option takes a boolean input

* auth/options_test.go add assertion for default option values

* make gen

* add back ACL tests to grants for user

* Support previous grants use-cases  (#5823)

* rename parameter to TestManagedGroupMember

* make replace old GrantsForUser with new queries

* fix tests and bugs where global grants don't get resolved

* remove unnecessary grants from grantsForUserProjectResourcesOrgScopeRecursiveQuery

* return full grants for any recursive call

* handle this and role scope ID in test

* order list result by create time instead of update time

* add missing err check

* handle converting role scope ID to this in grant scope API

* fix role service tests

* correct default scope role name

* add special handling in grantsForUser for scope resource

* fix tests to match role creation that does not bump version

* creating alias in invalid scope now returns permissions denied instead of internal

* remove test which looks for account in an authmethod in project scope which is an invalid config

* fix TestLdapManagedGroupRoleGrants to use scope appropriate resource for the test

* fix TestFetchActionSetForId and split org/proj test cases

* Validate reqScopeId for grantsForUserRecursive()

* Update tests based on recursive grantsForUser changes

* Remove last recursive function in favor of grantsForUserRecursive()

* move Scope resource to be AllowedIn all scope types

* allow multiple resouce types when calling auth.Verify down to GrantsForUsers

* use append instead of ranging over slice

* minor fix to tests

* Skip default role creation to avoid returning default role in test

* Update wantErrMsg to accommodate multiple resource types

* Add new entries to TestGrantsForUser test results based on recursive query changes

* Update unneccessaryRoles in recursive global/org test case based on changes to recursive query

---------

Co-authored-by: dkanney <david.kanney@hashicorp.com>

* Migrate roles and grants to new table structure (#5814)

* Refactor migration files to create tables, functions, triggers, and complete the actual data migration in their own files.

update migrations and tests

clean up removed files

rebase onto bosorawis-sql-grants-migration-hook

* Add comments to migration files, clean up some formatting, and add 30k project roles to test migration efficiency

update migrations go test file

Delete duplicate test file

* Fix formatting in migration files

* Rename hook number to reflect new number of migration files
split migrations into hook files, large test file, and more specific test functions

* Include missed oss file

* Apply suggestions from code review

Co-authored-by: Sorawis Nilparuk (Bo) <sorawis.nilparuk@hashicorp.com>

---------

Co-authored-by: Sorawis Nilparuk (Bo) <sorawis.nilparuk@hashicorp.com>

* Refactor GrantsForUser subqueries to use aggregate functions (#5822)

* Use shorter var name

* Refactor grantsForUserGlobalResourcesQuery: Use aggregate function array_agg to reduce rows returned from DB

Adding mutliple grants to a role exposes the need to aggregate certain fields (canonical_grant).
- Before this change, a row is returned for each distinct canonical grant for each role.
- After this change, only one row is returned per role. Each row contains an array of all canonical_grants that apply to the role.

* Refactor grantsForUserOrgResourcesQuery: Use aggregate function array_agg to reduce rows returned from DB

Adding mutliple grants to a role exposes the need to aggregate certain fields (individual_grant_scopes, canonical_grant).
- Before this change, a row is returned for each distinct canonical grant for each role.
- After this change, only one row is returned per role. Each row contains an array of all canonical_grants (and individual_grant_scopes) that apply to the role.

* Refactor grantsForUserProjectResourcesQuery: Use aggregate function array_agg to reduce rows returned from DB

Adding mutliple grants to a role exposes the need to aggregate certain fields (individual_grant_scopes, canonical_grant).
- Before this change, a row is returned for each distinct canonical grant for each role.
- After this change, only one row is returned per role. Each row contains an array of all canonical_grants (and individual_grant_scopes) that apply to the role.

* Address PR feedback

* Finish PR feedback

* Remove unnecessary if-check

* Match test name to function name

* Refactor grantsForUserRecursiveQuery: Use aggregate function array_agg to reduce rows returned from DB

* fix broken test (#5842)

* update tests to not check a table that will be dropped in the migration (#5847)

* Fix global scope's parent scope id (#5844)

* Use variable over hard-coded string

* 'global' scope should have no parent scope

* Clean up migrated tables (#5837)

* Refactor migration files to create tables, functions, triggers, and complete the actual data migration in their own files.

update migrations and tests

clean up removed files

rebase onto bosorawis-sql-grants-migration-hook

* Add comments to migration files, clean up some formatting, and add 30k project roles to test migration efficiency

update migrations go test file

Delete duplicate test file

* Fix formatting in migration files

* Rename hook number to reflect new number of migration files
split migrations into hook files, large test file, and more specific test functions

* Drop unneeded iam_role_grant_scope table and unneeded name, description, and version columns from iam_role

* fix whitespace

* fix broken sql from removing iam role grant scope (#5860)

* Ensure table drop also cascades changes to cross-table dependencies

* fix broken pgtap from removing iam-role-grant-scope table

* drop trigger which deletes from iam-role-grant-scope

---------

Co-authored-by: Michael Milton <michael.milton@hashicorp.com>

* test(groups): add grants tests for groups API (#5403)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* move a test to _test package

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* reorganize tests

* make gen

* feat: add grants tests for billing resource (#5559)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* reorganize tests

* make gen

* feat: add grants tests for billing resource

add test coverage for testing grants with billing resource. This tests `monthly-active-users` action with billing.

billing does not support output_fields so there are no tests for that

* add negative test coverage for descendant scope and org scope

* move negative tests to the billing resource

* revert alias changes

* revert alias test name change

* resolve rebase conflict

---------

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>

* Add grants tests for accounts (#5566)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* small comment change

* refactor role grants out of authtoken package

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* undo merge mistakes

* fix merge mistakes

* Trigger CI checks

* refactor auth/iam grants test setup

* Trigger CI checks

* add CRUDL tests

* add change-password and set-password tests

* add UpdateAccount, ChangePassword, SetPassword tests

* add ListAccount output_fields test

* make gen

* fix error message

* rebased against llb

* make gen

* fix post rebase

* fix typo

* make some tests use type-specific grants

* fix rebase issue

* add negative test to read

* test(credentials): Add grants tests (#5608)

* test(credentials): List tests

* test(credentials): Get tests

* test(credentials): Add "attributes" output_field & one its subtypes to the Read tests

* test(credentials): Create tests

* test(credentials): Update tests

* test(credentials): Delete tests

* test(credentials): Add additional test cases for pinned cred store id

* feat: add grants tests for alias (#5550)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* reorganize tests

* make gen

* feat: add grants tests for alias

add test coverage for testing grants with alias resource. This tests all actions with aliases and different grant scopes

* add tests for output fields

* add more test cases for actions, id

* update output assert to use shared assert function

* rebase

* use hashicorp/go-uuid instead of google/uuid

---------

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>
Co-authored-by: Elim Tsiagbey <elim.tsiagbey@hashicorp.com>

* test: add grants tests for host_catalogs (#5573)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* move a test to _test package

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* reorganize tests

* make gen

* Trigger CI checks

* test: add grants tests for host_catalogs

* add test coverage for output fields and write actions

* add missing error check assertions

* fixup! address PR comments

* fixup! fix lint errors

* chore(host_catalogs_test): Remove duplicate import

* test(hostcatalogs): Add tests for actions w/o grants

* test(hostcatalogs): Use ldap managed group for all test cases

* feat: add grants tests for billing resource (#5559)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* reorganize tests

* make gen

* feat: add grants tests for billing resource

add test coverage for testing grants with billing resource. This tests `monthly-active-users` action with billing.

billing does not support output_fields so there are no tests for that

* add negative test coverage for descendant scope and org scope

* move negative tests to the billing resource

* revert alias changes

* revert alias test name change

* resolve rebase conflict

---------

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>

* Add grants tests for accounts (#5566)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* small comment change

* refactor role grants out of authtoken package

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* undo merge mistakes

* fix merge mistakes

* Trigger CI checks

* refactor auth/iam grants test setup

* Trigger CI checks

* add CRUDL tests

* add change-password and set-password tests

* add UpdateAccount, ChangePassword, SetPassword tests

* add ListAccount output_fields test

* make gen

* fix error message

* rebased against llb

* make gen

* fix post rebase

* fix typo

* make some tests use type-specific grants

* fix rebase issue

* add negative test to read

* test(credentials): Add grants tests (#5608)

* test(credentials): List tests

* test(credentials): Get tests

* test(credentials): Add "attributes" output_field & one its subtypes to the Read tests

* test(credentials): Create tests

* test(credentials): Update tests

* test(credentials): Delete tests

* test(credentials): Add additional test cases for pinned cred store id

* feat: add grants tests for alias (#5550)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* reorganize tests

* make gen

* feat: add grants tests for alias

add test coverage for testing grants with alias resource. This tests all actions with aliases and different grant scopes

* add tests for output fields

* add more test cases for actions, id

* update output assert to use shared assert function

* rebase

* use hashicorp/go-uuid instead of google/uuid

---------

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>
Co-authored-by: Elim Tsiagbey <elim.tsiagbey@hashicorp.com>

* test(hostcatalogs): Add an additional project to "create" test cases

---------

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>
Co-authored-by: dkanney <dkanney@terpmail.umd.edu>
Co-authored-by: David Kanney <david.kanney@hashicorp.com>

* Add grants tests for authmethods (#5569)

* test(authmethods): Simplify read actions test cases

* test(authmethods): Create grants tests for write actions

* test(authmethods): Create test cases for action: authenticate

* test(authmethods): Update tests to use userFunc & other changes from upstream branch

* test(authmethods): Add output_fields to ReadActions tests

* test(authmethods): Add output_fields to WriteActions tests

* chore(authmethods_test): Consolidate common output fields into a single struct

* test(authmethods): Add test cases for project roles

* test(authmethods): Fix authenticate testcases

* test(authmethods): Add tests granting permission to specific ids using multiple roles

* test(hostsets): add grants tests (#5591)

* test(hostsets): List tests

* test(hostsets): Get tests

* test(hostsets): Create tests

* test(hostsets): Update tests

* test(hostsets): Delete tests

* test(hostsets}: AddHostSetHosts tests

* test(hostsets): RemoveHostSetHosts tests

* test(hostsets): SetHostSetHosts tests

* test(hostsets): Use unique host-catalog names to avoid duplicate key DB errors

* test(hostsets): Add second project to enforce exclusivity when Listing host-sets

* test: credentials store grants (#5592)

* first test with all the required setup

* v1 of test

* add primitive func and more test

* refactor read tests into a single top level

* move token generation to a function

* add test for creates

* add delete tests

* add update test

* only check for version and update_time

* move setup resource into testcase to support grants with specific ID

* add member tests

* add group-member test example with multiple actions

* remove duplicate group membership tests

* ran make gen

* fix missing parentID bug

* fix typo

* fix test names and add test cases

* switch from google/uuid to hashicorp/go-uuid

* add comment to groupmember tests

* small comment change

* pull shared test utility code from PR #5418

* refactor role grants out of authtoken package

* unexport utility function

* Remove dead code

* lint and make gen

* fix role cration logic

* fix password TestAccountFunc implementation

* implement TestAccountFunc for LDAP

* implement TestAccountFunc for OIDC

* implement TestUserFunc for managed groups

* use managed groups in grants test

* undo removal of authtoken.TestAuthTokenWithRoles for future refactor

* switch from list to map based test case for create tests

* undo merge mistakes

* fix merge mistakes

* lint

* add setup examples

* add output fields tests for getgroup

* reimplement with reflect

* add test for CreateGroup

* add all single resource action tests

* add list test

* rename function argument

* move AssertOutputFields to handlers package

* fix lint

* make gen

* use proto.Message instead of custom interface

* switch to hashicorp/go-uuid

* fix typo

* fix error message

* id= to ids=

* make generating test accounts more randomized

* Trigger CI checks

* refactor auth/iam grants test setup

* lint

* minor comment fix

* use Id instead of ID

* make user/account setup in iam returns account instead of just account ID

* missed one change

* save

* add list tests

* add get test

* add create and delete test

* add delete and update tests

* more tests

* fix collection_authorized_actions grants not resolving

* complete output_fields tests

* fix import groups

* make gen

* fixed broken tests

* fix rebase

* switch all tests to TestUserGroupGrantsFunc

* remove duplicate test

* test: add grants tests for managed groups resource (#5642)

* test: add grants tests for managed groups resource

* PR reviews

* address PR comments

* test: add grants tests for auth tokens resource (#5644)

* test: add grants tests for auth tokens resource

* add authorized actions tests for resources with sub-resources (#5835)

* add authorized actions tests for resources with sub-resources

* make gen

* bosorawis remove TestRoleGrantsForToken (#5840)

* refactor auth method grants tests

* refactor credential libraries grants tests

* refactor hosts grants tests

* refactor roles grants tests

* refactor scopes grants tests

* refactor tcp targets grants tests

* refactor users grants tests

* refactor worker grants tests

* fix authmethod test broken during the refactor

* remove authtoken.TestRoleGrantsForToken

* add pinned ID test

* correct event name

* fix make gen and lint

* Remove old test Role creation code

Removing leftover logic from old grants data model because it caused Group-association tests to fail

* update go mod

* run make gen and move new migrations to new folder (#5862)

* run make gen and move new migrations to new folder

* make tools and make gen

* update hook number

* update prior migration

* move 97005 to 97001 for consistency

* test: add grants tests for session resource (#5855)

* add grants tests for sessions

* fix missing parentScopeId

* make gen

* Resolve GrantsForUser queries via resolveQuery() (#5836)

* Add validation for nil resource type

* Remove redundant recursive test

* Refactor GrantsForUser() to perform query resolution & data mapping

- Remove grantsForUser sub-functions
- Resolve grantsForUser queries via resolveQuery()
- resolveQuery() and map data to GrantTuples in GrantsForUser()

* remove dead code

---------

Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>

* add grants tests for scope resource (#5845) (#5865)

* 'List' tests for scopes

* 'Get' tests for scopes

* 'Create' tests for scopes

* 'List Key Version Destruction Jobs' tests for scopes

* Add output_fields testing to 'Get' test

* Add additional test case for list-key-version-destruction-jobs

* test: add grants tests for roles resource (#5864)

* add grants tests for roles

* make gen

* test: add grants tests for worker resource (#5841)

* add read createControllerLed createWorkerLed tests

# Conflicts:
#	internal/daemon/controller/handlers/workers/grants_test.go

* minor refactor

* add output fields tests for list

* add tests for addworkertags readcertificateauthority reinitializecertificateauthority

* finish all worker api tests

* fix make gen and lint

* fix unchecked error lint

* add missing output fields assertion

* Bosorawis fix set role grant scope not handle children grant already exist (#5868)

* add new test case where role already has children and attempt to set children and individual project

* handle case where role already has children grant scope attempt to set children and project

* test: add grants tests for target resource (#5861)

* additional tests to target resource

* add test coverage for SetTargetCredentialSources

* make gen

* review suggestions

* test: add grants test for user resource and fix ACL bug (#5869)

* add some list-resolvable-aliases tests

* fix missing fields

* add more test and found edge case with children grants

* more list-resolvable test cases

* fix edge case where resource ids not overlapping between parent and child are not considered

* resolve conflict

* remove name, version, description from expect output_fields

* remove name and description from toProto and add godoc comment

---------

Co-authored-by: David Kanney <david.kanney@hashicorp.com>
Co-authored-by: Elim Tsiagbey <elim.tsiagbey@hashicorp.com>
Co-authored-by: Sorawis Nilparuk <sorawis.nilparuk@hashicorp.com>
Co-authored-by: Elim Tsiagbey <elimty02@gmail.com>
Co-authored-by: dkanney <dkanney@terpmail.umd.edu>
This commit is contained in:
authored and GitHub committed 2025-06-20 15:31:57 -04:00
1 parent 1f64c1202e
commit 1f17313a39
126 files changed
+97018 -4310

No files matched your search

+6
View File
@@ -161,6 +161,12 @@ protobuild:
@protoc-go-inject-tag -input=./internal/oplog/oplog_test/oplog_test.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/group_member.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_global_individual_org_grant_scope.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_global_individual_project_grant_scope.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_org_individual_grant_scope.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_global.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_org.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_project.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/principal_role.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_grant.pb.go
@protoc-go-inject-tag -input=./internal/iam/store/role_grant_scope.pb.go
+1
View File
@@ -18,6 +18,7 @@ const (
GrantScopeThis = "this"
GrantScopeChildren = "children"
GrantScopeDescendants = "descendants"
GrantScopeIndividual = "individual"
// CorrelationIdKey defines the http header and grpc metadata key used for specifying a
// correlation id. When getting the correlationId (from the http header or grpc metadata)
+1 -1
View File
@@ -66,6 +66,7 @@ require (
github.com/kelseyhightower/envconfig v1.4.0
github.com/kr/pretty v0.3.1
github.com/kr/text v0.2.0
github.com/lib/pq v1.10.9
github.com/mattn/go-colorable v0.1.14
github.com/miekg/dns v1.1.66
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
@@ -172,7 +173,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
+113 -5
View File
@@ -29,7 +29,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestFetchActionSetForId(t *testing.T) {
func TestFetchActionSetForId_orgRole(t *testing.T) {
tc := controller.NewTestController(t, nil)
defer tc.Shutdown()
@@ -73,7 +73,116 @@ func TestFetchActionSetForId(t *testing.T) {
iam.TestUserRole(t, conn, orgRole.PublicId, token.UserId)
iam.TestRoleGrant(t, conn, orgRole.PublicId, "ids=ttcp_foo;actions=read,update")
iam.TestRoleGrant(t, conn, orgRole.PublicId, "ids=ttcp_bar;actions=read,update,delete,authorize-session")
iam.TestRoleGrant(t, conn, orgRole.PublicId, "ids=*;type=role;actions=add-grants,remove-grants")
iam.TestRoleGrant(t, conn, orgRole.PublicId, "ids=*;type=policy;actions=add-grants,remove-grants")
cases := []struct {
name string
id string
avail action.ActionSet
allowed action.ActionSet
typeOverride resource.Type
}{
{
name: "base",
},
{
name: "no match",
id: "zip",
avail: action.NewActionSet(action.Read, action.Update),
},
{
name: "disjoint match",
id: "ttcp_bar",
avail: action.NewActionSet(action.Delete, action.AddGrants, action.Read, action.RemoveHostSets),
allowed: action.NewActionSet(action.Delete, action.Read),
},
{
name: "different type",
id: "anything",
typeOverride: resource.Scope,
},
{
name: "type match",
id: "anything",
typeOverride: resource.Policy,
avail: action.NewActionSet(action.Read, action.AddGrants),
allowed: action.NewActionSet(action.AddGrants),
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
req := require.New(t)
ctx := auth.NewVerifierContext(
context.Background(),
iamRepoFn,
authTokenRepoFn,
serversRepoFn,
tc.Kms(),
&authpb.RequestInfo{
PublicId: token.Id,
EncryptedToken: strings.Split(token.Token, "_")[2],
TokenFormat: uint32(auth.AuthTokenTypeBearer),
})
typ := resource.Role
if tt.typeOverride != resource.Unknown {
typ = tt.typeOverride
}
res := auth.Verify(ctx, typ, []auth.Option{
auth.WithId("ttcp_foo"),
auth.WithAction(action.Read),
auth.WithScopeId(org.PublicId),
}...)
req.NoError(res.Error)
assert.Equal(t, tt.allowed, res.FetchActionSetForId(ctx, tt.id, tt.avail))
})
}
}
func TestFetchActionSetForId_projRole(t *testing.T) {
tc := controller.NewTestController(t, nil)
defer tc.Shutdown()
conn := tc.DbConn()
client := tc.Client()
token := tc.Token()
client.SetToken(token.Token)
_, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
iamRepoFn := func() (*iam.Repository, error) {
return tc.IamRepo(), nil
}
serversRepoFn := func() (*server.Repository, error) {
return tc.ServersRepo(), nil
}
authTokenRepoFn := func() (*authtoken.Repository, error) {
return tc.AuthTokenRepo(), nil
}
// Delete the global default role so it doesn't interfere with the
// permissions we're testing here
rolesClient := roles.NewClient(tc.Client())
rolesResp, err := rolesClient.List(tc.Context(), scope.Global.String())
require.NoError(t, err)
require.NotNil(t, rolesResp)
assert.Len(t, rolesResp.GetItems(), 3)
var adminRoleId string
for _, item := range rolesResp.GetItems() {
if strings.Contains(item.Name, "Authenticated User") ||
strings.Contains(item.Name, "Login") {
_, err := rolesClient.Delete(tc.Context(), item.Id)
require.NoError(t, err)
} else {
adminRoleId = item.Id
}
}
_, err = rolesClient.Delete(tc.Context(), adminRoleId)
require.NoError(t, err)
projRole := iam.TestRole(t, conn, proj.GetPublicId())
iam.TestUserRole(t, conn, projRole.PublicId, token.UserId)
iam.TestRoleGrant(t, conn, projRole.PublicId, "ids=ttcp_foo;actions=read,update")
iam.TestRoleGrant(t, conn, projRole.PublicId, "ids=ttcp_bar;actions=read,update,delete,authorize-session")
iam.TestRoleGrant(t, conn, projRole.PublicId, "ids=*;type=role;actions=add-grants,remove-grants")
cases := []struct {
name string
@@ -127,11 +236,10 @@ func TestFetchActionSetForId(t *testing.T) {
if tt.typeOverride != resource.Unknown {
typ = tt.typeOverride
}
res := auth.Verify(ctx, []auth.Option{
res := auth.Verify(ctx, typ, []auth.Option{
auth.WithId("ttcp_foo"),
auth.WithAction(action.Read),
auth.WithScopeId(org.PublicId),
auth.WithType(typ),
auth.WithScopeId(proj.PublicId),
}...)
req.NoError(res.Error)
assert.Equal(t, tt.allowed, res.FetchActionSetForId(ctx, tt.id, tt.avail))
+1 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package auth
package auth_test
import (
"context"
@@ -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/boundary/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -73,7 +74,8 @@ func TestLdapManagedGroupRoleGrants(t *testing.T) {
// okay, let's try the CTE and make sure the user has the grants given via
// the ldap managed group
tuples, err := iamRepo.GrantsForUser(testCtx, testUser.PublicId)
// resource type does not matter here since testGrants has type=*
tuples, err := iamRepo.GrantsForUser(testCtx, testUser.PublicId, []resource.Type{resource.Scope}, testScopeId)
require.NoError(t, err)
// De-dupe role IDs
roleIds := make(map[string]bool, len(tuples))
@@ -86,7 +88,7 @@ func TestLdapManagedGroupRoleGrants(t *testing.T) {
// make sure a user without the appropriate managed group doesn't have grants
testUserWithoutManagedGroupRole := iam.TestUser(t, iamRepo, testScopeId)
tuples, err = iamRepo.GrantsForUser(testCtx, testUserWithoutManagedGroupRole.PublicId)
tuples, err = iamRepo.GrantsForUser(testCtx, testUserWithoutManagedGroupRole.PublicId, []resource.Type{resource.Scope}, testScopeId)
require.NoError(t, err)
assert.Equal(t, 0, len(tuples))
@@ -111,7 +113,7 @@ func TestLdapManagedGroupRoleGrants(t *testing.T) {
_ = iam.TestRoleGrant(t, testConn, testRole2.GetPublicId(), testGrant)
iam.TestManagedGroupRole(t, testConn, testRole2.GetPublicId(), testOidcManagedGrp.GetPublicId())
tuples, err = iamRepo.GrantsForUser(testCtx, testUser.GetPublicId())
tuples, err = iamRepo.GrantsForUser(testCtx, testUser.GetPublicId(), []resource.Type{resource.SessionRecording}, testScopeId)
require.NoError(t, err)
assert.Equal(t, 2, len(tuples))
t.Log("tuples:", tuples)
+19
View File
@@ -12,6 +12,7 @@ import (
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/url"
@@ -19,9 +20,12 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/kms"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
)
@@ -176,6 +180,21 @@ func TestAccount(t testing.TB, conn *db.DB, am *AuthMethod, loginName string, op
return a
}
// TestAuthMethodWithAccountInManagedGroup creates an authMethod, and an account within that authmethod, an
// LDAP managed group, and add the newly created account as a member of the LDAP managed group.
func TestAuthMethodWithAccountInManagedGroup(t *testing.T, conn *db.DB, kmsCache *kms.Kms, scopeId string) (auth.AuthMethod, auth.Account, auth.ManagedGroup) {
t.Helper()
uuid, err := uuid.GenerateUUID()
require.NoError(t, err)
ctx := context.Background()
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), scopeId, kms.KeyPurposeDatabase)
require.NoError(t, err)
am := TestAuthMethod(t, conn, databaseWrapper, scopeId, []string{fmt.Sprintf("ldap://%s", uuid)})
managedGroup := TestManagedGroup(t, conn, am, []string{uuid})
acct := TestAccount(t, conn, am, "testacct", WithMemberOfGroups(ctx, uuid))
return am, acct, managedGroup
}
// TestManagedGroup creates a test ldap managed group.
func TestManagedGroup(t testing.TB, conn *db.DB, am *AuthMethod, grpNames []string, opt ...Option) *ManagedGroup {
t.Helper()
+23 -2
View File
@@ -24,6 +24,7 @@ import (
"testing"
"time"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/oidc/request"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/db"
@@ -32,6 +33,7 @@ import (
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/cap/oidc"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -192,6 +194,25 @@ func TestAccount(t testing.TB, conn *db.DB, am *AuthMethod, subject string, opt
return a
}
// TestAuthMethodWithAccountInManagedGroup creates an authMethod, and an account within that authmethod, an
// OIDC managed group, and add the newly created account as a member of the OIDC managed group.
func TestAuthMethodWithAccountInManagedGroup(t *testing.T, conn *db.DB, kmsCache *kms.Kms, scopeId string) (auth.AuthMethod, auth.Account, auth.ManagedGroup) {
t.Helper()
uuid, err := uuid.GenerateUUID()
require.NoError(t, err)
databaseWrapper, err := kmsCache.GetWrapper(context.Background(), scopeId, kms.KeyPurposeDatabase)
require.NoError(t, err)
testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, scopeId, ActivePublicState,
"alice-rp", "fido",
WithIssuer(TestConvertToUrls(t, fmt.Sprintf("https://%s.com", uuid))[0]),
WithSigningAlgs(Alg(oidc.RS256)),
WithApiUrl(TestConvertToUrls(t, fmt.Sprintf("https://%s.com/callback", uuid))[0]))
account := TestAccount(t, conn, testAuthMethod, "testacct")
managedGroup := TestManagedGroup(t, conn, testAuthMethod, `"/token/sub" matches ".*"`)
TestManagedGroupMember(t, conn, managedGroup.PublicId, account.PublicId)
return testAuthMethod, account, managedGroup
}
// TestManagedGroup creates a test oidc managed group.
func TestManagedGroup(t testing.TB, conn *db.DB, am *AuthMethod, filter string, opt ...Option) *ManagedGroup {
t.Helper()
@@ -211,13 +232,13 @@ func TestManagedGroup(t testing.TB, conn *db.DB, am *AuthMethod, filter string,
}
// TestManagedGroupMember adds given account IDs to a managed group
func TestManagedGroupMember(t testing.TB, conn *db.DB, managedGroupId, memberId string, opt ...Option) *ManagedGroupMemberAccount {
func TestManagedGroupMember(t testing.TB, conn *db.DB, managedGroupId, accountId string, opt ...Option) *ManagedGroupMemberAccount {
t.Helper()
require := require.New(t)
rw := db.New(conn)
ctx := context.Background()
mg, err := NewManagedGroupMemberAccount(ctx, managedGroupId, memberId, opt...)
mg, err := NewManagedGroupMemberAccount(ctx, managedGroupId, accountId, opt...)
require.NoError(err)
require.NoError(rw.Create(ctx, mg))
+13
View File
@@ -8,7 +8,10 @@ import (
"fmt"
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -71,6 +74,16 @@ func TestMultipleAccounts(t testing.TB, conn *db.DB, authMethodId string, count
return auts
}
// TestAuthMethodWithAccount creates an authMethod and an account within that authmethod
// returing both the AM and the account
func TestAuthMethodWithAccount(t *testing.T, conn *db.DB) (auth.AuthMethod, auth.Account) {
authMethod := TestAuthMethod(t, conn, globals.GlobalPrefix)
loginName, err := uuid.GenerateUUID()
require.NoError(t, err)
acct := TestAccount(t, conn, authMethod.GetPublicId(), loginName)
return authMethod, acct
}
// TestAccount creates a password account to the provided DB with the provided
// auth method id and loginName. The auth method must have been created
// previously. See password.NewAccount(...) for a list of supported options.
+6
View File
@@ -10,9 +10,15 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/kms"
"github.com/stretchr/testify/require"
)
type (
TestAuthMethodWithAccountFunc func(t *testing.T, conn *db.DB) (AuthMethod, Account)
TestAuthMethodWithAccountInManagedGroup func(t *testing.T, conn *db.DB, kmsCache *kms.Kms, scopeId string) (AuthMethod, Account, ManagedGroup)
)
// ManagedGroupMemberAccount represents an entry from
// auth_managed_group_member_account. These are used to determine the account
// ids where are a member of managed groups. See: oidc and ldap managed groups
+2 -2
View File
@@ -51,7 +51,7 @@ func TestAuthToken(t testing.TB, conn *db.DB, kms *kms.Kms, scopeId string, opt
// TestRoleGrantsForToken contains information used by TestAuthTokenWithRoles to create
// roles and their associated grants (with grant scopes)
type TestRoleGrantsForToken struct {
RoleScopeID string
RoleScopeId string
GrantStrings []string
GrantScopes []string
}
@@ -75,7 +75,7 @@ func TestAuthTokenWithRoles(t testing.TB, conn *db.DB, kms *kms.Kms, scopeId str
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)
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())
+9 -6
View File
@@ -173,7 +173,7 @@ func NewVerifierContext(ctx context.Context,
// may come from the URL and may come from the token) and whether or not to
// proceed, e.g. whether the authn/authz check resulted in failure. If an error
// occurs it's logged to the system log.
func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
func Verify(ctx context.Context, resourceType resource.Type, opt ...Option) (ret VerifyResults) {
const op = "auth.Verify"
ret.Error = handlers.ForbiddenError()
v, ok := ctx.Value(verifierKey).(*verifier)
@@ -272,11 +272,11 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
ScopeId: opts.withScopeId,
Id: opts.withId,
Pin: opts.withPin,
Type: opts.withType,
Type: resourceType,
// Parent Scope ID will be filled in via performAuthCheck
}
// Global scope has no parent ID; account for this
if opts.withId == scope.Global.String() && opts.withType == resource.Scope {
if opts.withId == scope.Global.String() && resourceType == resource.Scope {
v.res.ScopeId = scope.Global.String()
}
@@ -284,10 +284,12 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
v.decryptToken(ctx)
}
resourcesToFetchGrants := append([]resource.Type{resourceType}, opts.withFetchAdditionalResourceGrants...)
var authResults perms.ACLResults
var userData template.Data
var err error
authResults, ret.UserData, ret.Scope, v.acl, ret.grants, err = v.performAuthCheck(ctx)
authResults, ret.UserData, ret.Scope, v.acl, ret.grants, err = v.performAuthCheck(ctx, resourcesToFetchGrants, opts.withRecursive)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error performing authn/authz check"))
return
@@ -491,7 +493,7 @@ func (v *verifier) decryptToken(ctx context.Context) {
}
}
func (v verifier) performAuthCheck(ctx context.Context) (
func (v verifier) performAuthCheck(ctx context.Context, resourceType []resource.Type, isRecursiveRequest bool) (
aclResults perms.ACLResults,
userData template.Data,
scopeInfo *scopes.ScopeInfo,
@@ -651,7 +653,8 @@ func (v verifier) performAuthCheck(ctx context.Context) (
// Fetch and parse grants for this user ID (which may include grants for
// u_anon and u_auth)
grantTuples, err = iamRepo.GrantsForUser(v.ctx, *userData.User.Id)
iamOpt := []iam.Option{iam.WithRecursive(isRecursiveRequest)}
grantTuples, err = iamRepo.GrantsForUser(v.ctx, *userData.User.Id, resourceType, scopeInfo.Id, iamOpt...)
if err != nil {
retErr = errors.Wrap(ctx, err, op)
return
+6 -4
View File
@@ -23,6 +23,7 @@ import (
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/tests/api"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/eventlogger/filters/encrypt"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/assert"
@@ -243,7 +244,7 @@ func TestVerify_AuditEvent(t *testing.T) {
ctx := NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, testKms, &requestInfo)
_ = os.WriteFile(eventConfig.AuditEvents.Name(), nil, 0o666) // clean out audit events from previous calls
_ = Verify(ctx, tt.opt...)
_ = Verify(ctx, resource.Scope, tt.opt...)
got := api.CloudEventFromFile(t, eventConfig.AuditEvents.Name())
if tt.wantAuthAuditData {
@@ -285,6 +286,7 @@ func TestGrantsHash(t *testing.T) {
}
o, _ := iam.TestScopes(t, iamRepo)
resourceType := resource.Scope
req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/"+o.GetPublicId(), nil)
at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
@@ -302,7 +304,7 @@ func TestGrantsHash(t *testing.T) {
verifierCtx := NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo)
// Create auth result from token
res := Verify(verifierCtx, WithScopeId(o.GetPublicId()))
res := Verify(verifierCtx, resourceType, WithScopeId(o.GetPublicId()))
hash1, err := res.GrantsHash(ctx)
require.NoError(t, err)
@@ -325,7 +327,7 @@ func TestGrantsHash(t *testing.T) {
verifierCtx = NewVerifierContext(ctx, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo)
// Create auth result from token
res = Verify(verifierCtx, WithScopeId(o.GetPublicId()))
res = Verify(verifierCtx, resourceType, WithScopeId(o.GetPublicId()))
hash2, err := res.GrantsHash(ctx)
require.NoError(t, err)
@@ -337,7 +339,7 @@ func TestGrantsHash(t *testing.T) {
_ = iam.TestUserRole(t, conn, newRole.GetPublicId(), user.GetPublicId())
// Recreate auth result with new grants, should have a new hash
res = Verify(verifierCtx, WithScopeId(o.GetPublicId()))
res = Verify(verifierCtx, resourceType, WithScopeId(o.GetPublicId()))
hash3, err := res.GrantsHash(ctx)
require.NoError(t, err)
assert.False(t, bytes.Equal(hash1, hash3))
+28 -17
View File
@@ -23,23 +23,40 @@ type Option func(*options)
// options = how options are represented
type options struct {
withScopeId string
withPin string
withId string
withAction action.Type
withType resource.Type
withUserId string
withKms *kms.Kms
withRecoveryTokenNotAllowed bool
withAnonymousUserNotAllowed bool
withResource *perms.Resource
withActions []string
withScopeId string
withPin string
withId string
withAction action.Type
withUserId string
withKms *kms.Kms
withRecursive bool
withRecoveryTokenNotAllowed bool
withAnonymousUserNotAllowed bool
withResource *perms.Resource
withActions []string
withFetchAdditionalResourceGrants []resource.Type
}
func getDefaultOptions() options {
return options{}
}
func WithRecursive(isRecursive bool) Option {
return func(o *options) {
o.withRecursive = isRecursive
}
}
// WithFetchAdditionalResourceGrants allows auth.Verify to fetch grants for additional resources to build a more
// complete GrantTuples of the requesting identity. This ensures that we can accurately determine
// authorized_action and authorized_collection_action for sub-resources
// E.g. Reading 'host-catalog' should also fetch authorized actions for 'hosts'
func WithFetchAdditionalResourceGrants(resources ...resource.Type) Option {
return func(o *options) {
o.withFetchAdditionalResourceGrants = append(o.withFetchAdditionalResourceGrants, resources...)
}
}
func WithScopeId(id string) Option {
return func(o *options) {
o.withScopeId = id
@@ -64,12 +81,6 @@ func WithAction(action action.Type) Option {
}
}
func WithType(rt resource.Type) Option {
return func(o *options) {
o.withType = rt
}
}
func WithUserId(id string) Option {
return func(o *options) {
o.withUserId = id
@@ -9,7 +9,6 @@ import (
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/stretchr/testify/assert"
)
@@ -20,16 +19,20 @@ func Test_GetOpts(t *testing.T) {
withKms := new(kms.Kms)
res := new(perms.Resource)
// test default values
defaultOpts := getDefaultOptions()
assert.Equal(t, options{}, defaultOpts)
opts := getOpts(
WithScopeId("foo"),
WithPin("bar"),
WithId("zip"),
WithAction(action.AddHosts),
WithType(resource.Group),
WithUserId("user"),
WithKms(withKms),
WithRecoveryTokenNotAllowed(true),
WithAnonymousUserNotAllowed(true),
WithRecursive(true),
WithResource(res),
WithActions([]string{"callback"}),
)
@@ -38,11 +41,11 @@ func Test_GetOpts(t *testing.T) {
withPin: "bar",
withId: "zip",
withAction: action.AddHosts,
withType: resource.Group,
withUserId: "user",
withKms: withKms,
withRecoveryTokenNotAllowed: true,
withAnonymousUserNotAllowed: true,
withRecursive: true,
withResource: res,
withActions: []string{"callback"},
}
@@ -150,7 +150,7 @@ func (s Service) ListAccounts(ctx context.Context, req *pbs.ListAccountsRequest)
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
_, authResults := s.parentAndAuthResult(ctx, req.GetAuthMethodId(), action.List)
_, authResults := s.parentAndAuthResult(ctx, req.GetAuthMethodId(), action.List, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -368,7 +368,7 @@ func (s Service) GetAccount(ctx context.Context, req *pbs.GetAccountRequest) (*p
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -410,7 +410,7 @@ func (s Service) CreateAccount(ctx context.Context, req *pbs.CreateAccountReques
return nil, err
}
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetAuthMethodId(), action.Create)
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetAuthMethodId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -449,7 +449,7 @@ func (s Service) UpdateAccount(ctx context.Context, req *pbs.UpdateAccountReques
return nil, err
}
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update)
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -485,7 +485,7 @@ func (s Service) DeleteAccount(ctx context.Context, req *pbs.DeleteAccountReques
if err := validateDeleteRequest(ctx, req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -504,7 +504,7 @@ func (s Service) ChangePassword(ctx context.Context, req *pbs.ChangePasswordRequ
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.ChangePassword)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.ChangePassword, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -543,7 +543,7 @@ func (s Service) SetPassword(ctx context.Context, req *pbs.SetPasswordRequest) (
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.SetPassword)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.SetPassword, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -1030,7 +1030,7 @@ func (s Service) setPasswordInRepo(ctx context.Context, scopeId, id string, vers
return out, nil
}
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (auth.AuthMethod, requestauth.VerifyResults) {
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type, isRecursive bool) (auth.AuthMethod, requestauth.VerifyResults) {
res := requestauth.VerifyResults{}
pwRepo, err := s.pwRepoFn()
if err != nil {
@@ -1049,7 +1049,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
}
var parentId string
opts := []requestauth.Option{requestauth.WithType(resource.Account), requestauth.WithAction(a)}
opts := []requestauth.Option{requestauth.WithAction(a), requestauth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -1129,7 +1129,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
authMeth = am
}
opts = append(opts, requestauth.WithScopeId(authMeth.GetScopeId()), requestauth.WithPin(parentId))
return authMeth, requestauth.Verify(ctx, opts...)
return authMeth, requestauth.Verify(ctx, resource.Account, opts...)
}
func toProto(ctx context.Context, in auth.Account, opt ...handlers.Option) (*pb.Account, error) {
@@ -33,6 +33,7 @@ import (
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/accounts"
scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
@@ -4453,7 +4454,7 @@ func TestGrantsAcrossManagedGroups(t *testing.T) {
wrap := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrap)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
require.NoError(t, err)
@@ -4496,7 +4497,9 @@ func TestGrantsAcrossManagedGroups(t *testing.T) {
iam.TestManagedGroupRole(t, conn, ldapRole.GetPublicId(), ldapMg.GetPublicId())
iam.TestRoleGrant(t, conn, ldapRole.GetPublicId(), "ids=ttcp_ldap;actions=read")
grants, err := iamRepo.GrantsForUser(ctx, user.GetPublicId())
// targets must be in a project scope so we're passing in a scope ID which we expect the user to have access to
// which is the project under the role's org
grants, err := iamRepo.GrantsForUser(ctx, user.GetPublicId(), []resource.Type{resource.Target}, proj.PublicId)
require.NoError(t, err)
// Verify we see both grants
@@ -4520,7 +4523,7 @@ func TestGrantsAcrossManagedGroups(t *testing.T) {
assert.Equal(t, 1, numDeleted)
// Verify we don't see the ldap grant anymore
grants, err = iamRepo.GrantsForUser(ctx, user.GetPublicId())
grants, err = iamRepo.GrantsForUser(ctx, user.GetPublicId(), []resource.Type{resource.Target}, proj.PublicId)
require.NoError(t, err)
foundOidc = false
foundLdap = false
@@ -4543,7 +4546,7 @@ func TestGrantsAcrossManagedGroups(t *testing.T) {
assert.Equal(t, 1, numDeleted)
// Verify we don't see the oidc grant anymore
grants, err = iamRepo.GrantsForUser(ctx, user.GetPublicId())
grants, err = iamRepo.GrantsForUser(ctx, user.GetPublicId(), []resource.Type{resource.Target}, proj.PublicId)
require.NoError(t, err)
foundOidc = false
foundLdap = false
File diff suppressed because it is too large. Load diff
@@ -98,7 +98,7 @@ func (s Service) ListAliases(ctx context.Context, req *pbs.ListAliasesRequest) (
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -240,7 +240,7 @@ func (s Service) GetAlias(ctx context.Context, req *pbs.GetAliasRequest) (*pbs.G
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -278,7 +278,7 @@ func (s Service) CreateAlias(ctx context.Context, req *pbs.CreateAliasRequest) (
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -316,7 +316,7 @@ func (s Service) UpdateAlias(ctx context.Context, req *pbs.UpdateAliasRequest) (
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -352,7 +352,7 @@ func (s Service) DeleteAlias(ctx context.Context, req *pbs.DeleteAliasRequest) (
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -467,7 +467,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
return rows > 0, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
iamRepo, err := s.iamRepoFn()
if err != nil {
@@ -476,7 +476,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Alias), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -508,7 +508,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Alias, opts...)
}
func toProto(ctx context.Context, in *target.Alias, opt ...handlers.Option) (*pb.Alias, error) {
File diff suppressed because it is too large. Load diff
@@ -78,6 +78,11 @@ var (
resource.Account: accounts.CollectionActions,
resource.ManagedGroup: managed_groups.CollectionActions,
}
additionalResourceGrants = []resource.Type{
resource.Account,
resource.ManagedGroup,
}
)
// Service handles request as described by the pbs.AuthMethodServiceServer interface.
@@ -155,7 +160,7 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -320,7 +325,7 @@ func (s Service) GetAuthMethod(ctx context.Context, req *pbs.GetAuthMethodReques
if err := validateGetRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -365,7 +370,7 @@ func (s Service) CreateAuthMethod(ctx context.Context, req *pbs.CreateAuthMethod
if err := validateCreateRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -410,7 +415,7 @@ func (s Service) UpdateAuthMethod(ctx context.Context, req *pbs.UpdateAuthMethod
if err := validateUpdateRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -464,7 +469,7 @@ func (s Service) ChangeState(ctx context.Context, req *pbs.ChangeStateRequest) (
if err := validateChangeStateRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.ChangeState)
authResults := s.authResult(ctx, req.GetId(), action.ChangeState, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -512,7 +517,7 @@ func (s Service) DeleteAuthMethod(ctx context.Context, req *pbs.DeleteAuthMethod
if err := validateDeleteRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -546,7 +551,7 @@ func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest)
}
}
authResults := s.authResult(ctx, req.GetAuthMethodId(), action.Authenticate)
authResults := s.authResult(ctx, req.GetAuthMethodId(), action.Authenticate, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -765,12 +770,17 @@ func (s Service) changeStateInRepo(ctx context.Context, req *pbs.ChangeStateRequ
return nil, errors.New(ctx, errors.InvalidParameter, op, "Given auth method type does not support changing state")
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) requestauth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) requestauth.VerifyResults {
const op = "authmethods.(Service).authResult"
res := requestauth.VerifyResults{}
var parentId string
opts := []requestauth.Option{requestauth.WithType(resource.AuthMethod), requestauth.WithAction(a)}
opts := []requestauth.Option{
requestauth.WithAction(a),
requestauth.WithRecursive(isRecursive),
requestauth.WithFetchAdditionalResourceGrants(additionalResourceGrants...),
}
switch a {
case action.List, action.Create:
parentId = id
@@ -847,7 +857,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) reque
opts = append(opts, requestauth.WithId(id))
}
opts = append(opts, requestauth.WithScopeId(parentId))
return requestauth.Verify(ctx, opts...)
return requestauth.Verify(ctx, resource.AuthMethod, opts...)
}
func toAuthMethodProto(ctx context.Context, in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMethod, error) {
File diff suppressed because it is too large. Load diff
@@ -84,7 +84,7 @@ func (s Service) ListAuthTokens(ctx context.Context, req *pbs.ListAuthTokensRequ
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -224,7 +224,7 @@ func (s Service) GetAuthToken(ctx context.Context, req *pbs.GetAuthTokenRequest)
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.ReadSelf)
authResults := s.authResult(ctx, req.GetId(), action.ReadSelf, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -276,7 +276,7 @@ func (s Service) DeleteAuthToken(ctx context.Context, req *pbs.DeleteAuthTokenRe
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.DeleteSelf)
authResults := s.authResult(ctx, req.GetId(), action.DeleteSelf, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -334,11 +334,11 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
return rows > 0, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
var parentId string
opts := []auth.Option{auth.WithType(resource.AuthToken), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -375,7 +375,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.AuthToken, opts...)
}
func toProto(ctx context.Context, in *authtoken.AuthToken, opt ...handlers.Option) (*pb.AuthToken, error) {
@@ -9,13 +9,17 @@ import (
"testing"
"github.com/hashicorp/boundary/globals"
a "github.com/hashicorp/boundary/internal/auth"
"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"
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/assert"
"github.com/stretchr/testify/require"
)
@@ -65,49 +69,258 @@ func TestGrants_ReadActions(t *testing.T) {
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListAuthTokensRequest
rolesToCreate []authtoken.TestRoleGrantsForToken
wantErr error
wantIDs []string
name string
input *pbs.ListAuthTokensRequest
userFunc func() (*iam.User, a.Account)
wantErr error
wantIDs []string
expectOutfields []string
}{
{
name: "global role grant this and children returns global and org groups",
name: "global role grant this returns global token",
input: &pbs.ListAuthTokensRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=auth-token;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,scope_id,scope,user_id"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: []string{globalAT.PublicId, org1AT.PublicId, org2AT.PublicId},
wantIDs: []string{globalAT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
},
},
{
name: "org role grant this and children returns org groups",
name: "global role grant this and children returns global and org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=auth-token;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: nil,
wantIDs: []string{globalAT.PublicId, org1AT.PublicId, org2AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
globals.AuthMethodIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
},
{
name: "global role grant this and descendants returns global and org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=auth-token;actions=list,read;output_fields=id,approximate_last_used_time,expiration_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
wantErr: nil,
wantIDs: []string{globalAT.PublicId, org1AT.PublicId, org2AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
},
{
name: "global role grant descendants with recursive returns global and org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=auth-token;actions=list,read;output_fields=id,approximate_last_used_time,expiration_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantErr: nil,
wantIDs: []string{org1AT.PublicId, org2AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
},
{
name: "global role grant descendants without recursive returns error",
input: &pbs.ListAuthTokensRequest{
ScopeId: globals.GlobalPrefix,
Recursive: false,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=auth-token;actions=list,read;output_fields=id,approximate_last_used_time,expiration_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantErr: handlers.ForbiddenError(),
wantIDs: nil,
},
{
name: "org role grant this returns org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: org1.PublicId,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org1.PublicId,
GrantStrings: []string{"ids=*;type=auth-token;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=auth-token;actions=list,read;output_fields=id,auth_method_id,account_id,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: []string{org1AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.AuthMethodIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
},
},
{
name: "org role grant this and children returns org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: org1.PublicId,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: nil,
wantIDs: []string{org1AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
globals.AuthMethodIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
},
{
name: "global role grant this and children returns org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: org1.PublicId,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: nil,
wantIDs: []string{org1AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
globals.AuthMethodIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
},
{
name: "org role grant this pinned id returns org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: org1.PublicId,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{fmt.Sprintf("ids=%s;type=auth-token;actions=*", org1AT.PublicId)},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
wantIDs: []string{org1AT.PublicId},
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
globals.AuthMethodIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
},
{
name: "org role grant this does not list other org tokens",
input: &pbs.ListAuthTokensRequest{
ScopeId: org2.PublicId,
Recursive: true,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
wantIDs: nil,
expectOutfields: nil,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tok := authtoken.TestAuthTokenWithRoles(t, conn, kmsCache, globals.GlobalPrefix, tc.rolesToCreate)
user, acct := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.ListAuthTokens(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
@@ -124,6 +337,304 @@ func TestGrants_ReadActions(t *testing.T) {
for _, id := range tc.wantIDs {
require.Contains(t, gotIDs, id)
}
for _, item := range got.Items {
handlers.TestAssertOutputFields(t, item, tc.expectOutfields)
}
})
}
})
t.Run("Get", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.GetAuthTokenRequest
userFunc func() (*iam.User, a.Account)
wantErr error
wantId string
expectOutfields []string
}{
{
name: "global role grant this returns global token",
input: &pbs.GetAuthTokenRequest{
Id: globalAT.PublicId,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=read;output_fields=id,scope_id,scope,user_id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
},
wantId: globalAT.PublicId,
},
{
name: "org role grant this returns org token",
input: &pbs.GetAuthTokenRequest{
Id: org1AT.PublicId,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=auth-token;actions=read;output_fields=id,auth_method_id,account_id,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
expectOutfields: []string{
globals.IdField,
globals.AuthMethodIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
},
wantId: org1AT.PublicId,
},
{
name: "org role grant this cannot read global token",
input: &pbs.GetAuthTokenRequest{
Id: globalAT.PublicId,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: handlers.ForbiddenError(),
expectOutfields: nil,
},
{
name: "global role grant this and children can read org token",
input: &pbs.GetAuthTokenRequest{
Id: org1AT.PublicId,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,scope_id,scope,user_id"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: nil,
expectOutfields: []string{
globals.IdField,
globals.AuthMethodIdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
globals.AccountIdField,
globals.CreatedTimeField,
globals.UpdatedTimeField,
globals.ApproximateLastUsedTimeField,
globals.ExpirationTimeField,
globals.AuthorizedActionsField,
},
wantId: org1AT.PublicId,
},
{
name: "org role grant this pinned id returns org token",
input: &pbs.GetAuthTokenRequest{
Id: org1AT.PublicId,
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{fmt.Sprintf("ids=%s;type=auth-token;actions=read;output_fields=id,scope_id,scope,user_id", org1AT.PublicId)},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
expectOutfields: []string{
globals.IdField,
globals.ScopeIdField,
globals.ScopeField,
globals.UserIdField,
},
wantId: org1AT.PublicId,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, acct := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.GetAuthToken(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
return
}
require.NoError(t, finalErr)
assert.Equal(t, tc.wantId, got.GetItem().Id)
handlers.TestAssertOutputFields(t, got.GetItem(), tc.expectOutfields)
})
}
})
}
func TestGrants_CreateActions(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)
org3, _ := iam.TestScopes(t, iamRepo)
pinnedId := authtoken.TestAuthToken(t, conn, kmsCache, org2.GetPublicId())
selfToken := authtoken.TestAuthToken(t, conn, kmsCache, org3.GetPublicId())
t.Run("Delete", func(t *testing.T) {
testcases := []struct {
name string
createTokenFn func() *authtoken.AuthToken
userFunc func() (*iam.User, a.Account)
ctxAuthToken func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error)
wantErr error
}{
{
name: "global role grant this deletes global token",
createTokenFn: func() *authtoken.AuthToken {
return authtoken.TestAuthToken(t, conn, kmsCache, globals.GlobalPrefix)
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=delete"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
ctxAuthToken: func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error) {
return atRepo.CreateAuthToken(ctx, user, acctId)
},
wantErr: nil,
},
{
name: "global role grant this and children can delete org tokens",
createTokenFn: func() *authtoken.AuthToken {
return authtoken.TestAuthToken(t, conn, kmsCache, org1.GetPublicId())
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=auth-token;actions=delete"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
ctxAuthToken: func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error) {
return atRepo.CreateAuthToken(ctx, user, acctId)
},
wantErr: nil,
},
{
name: "global role grant this and descendants can delete org tokens",
createTokenFn: func() *authtoken.AuthToken {
return authtoken.TestAuthToken(t, conn, kmsCache, org2.GetPublicId())
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
ctxAuthToken: func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error) {
return atRepo.CreateAuthToken(ctx, user, acctId)
},
wantErr: nil,
},
{
name: "global role grant this can delete pinned org token",
createTokenFn: func() *authtoken.AuthToken {
return pinnedId
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{fmt.Sprintf("ids=%s;type=auth-token;actions=*", pinnedId.PublicId)},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
ctxAuthToken: func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error) {
return atRepo.CreateAuthToken(ctx, user, acctId)
},
wantErr: nil,
},
{
name: "org role grant this can delete self",
createTokenFn: func() *authtoken.AuthToken {
return selfToken
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=delete:self"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
ctxAuthToken: func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error) {
return selfToken, nil
},
wantErr: nil,
},
{
name: "org role grant this can't delete global",
createTokenFn: func() *authtoken.AuthToken {
return authtoken.TestAuthToken(t, conn, kmsCache, globals.GlobalPrefix)
},
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{org2.PublicId},
},
}),
ctxAuthToken: func(ctx context.Context, user *iam.User, acctId string) (*authtoken.AuthToken, error) {
return atRepo.CreateAuthToken(ctx, user, acctId)
},
wantErr: handlers.ForbiddenError(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, acct := tc.userFunc()
inputToken := tc.createTokenFn()
input := &pbs.DeleteAuthTokenRequest{
Id: inputToken.PublicId,
}
authToken, err := tc.ctxAuthToken(ctx, user, acct.GetPublicId())
require.NoError(t, err)
// authToken, err := atRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
fullGrantAuthCtx := auth.TestAuthContextFromToken(t, conn, wrap, authToken, iamRepo)
_, finalErr := s.DeleteAuthToken(fullGrantAuthCtx, input)
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
return
}
require.NoError(t, finalErr)
})
}
})
@@ -63,7 +63,7 @@ func NewService(
func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveUsersRequest) (*pbs.MonthlyActiveUsersResponse, error) {
const op = "billing.(Service).MonthlyActiveUsers"
authResults := s.authResult(ctx, action.MonthlyActiveUsers)
authResults := s.authResult(ctx, action.MonthlyActiveUsers, false)
if authResults.Error != nil {
return nil, errors.Wrap(ctx, authResults.Error, op)
}
@@ -115,11 +115,11 @@ func (s Service) MonthlyActiveUsers(ctx context.Context, req *pbs.MonthlyActiveU
return &pbs.MonthlyActiveUsersResponse{Items: activeUsers}, nil
}
func (s Service) authResult(ctx context.Context, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, a action.Type, isRecursive bool) auth.VerifyResults {
opts := []auth.Option{
auth.WithType(resource.Billing),
auth.WithAction(a),
auth.WithScopeId(scope.Global.String()),
auth.WithRecursive(isRecursive),
}
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Billing, opts...)
}
@@ -0,0 +1,256 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package billing_test
import (
"context"
"testing"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/billing"
cauth "github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
billingservice "github.com/hashicorp/boundary/internal/daemon/controller/handlers/billing"
"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_MonthlyActiveUsers tests read actions to assert that grants are being applied properly
//
// Role - which scope the role is created in
// - global level
func TestGrants_MonthlyActiveUsers(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)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap))
repoFn := func() (*billing.Repository, error) {
return billing.TestRepo(t, conn), nil
}
billing.TestGenerateActiveUsers(t, conn)
today := time.Now().UTC()
monthStart := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, time.UTC)
threeMonthsAgo := time.Date(monthStart.AddDate(0, -3, 0).Year(), monthStart.AddDate(0, -3, 0).Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01")
oneMonthAgo := time.Date(monthStart.AddDate(0, -1, 0).Year(), monthStart.AddDate(0, -1, 0).Month(), 1, 0, 0, 0, 0, time.UTC).Format("2006-01")
s, err := billingservice.NewService(ctx, repoFn)
require.NoError(t, err)
t.Run("MonthlyActiveUsers", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.MonthlyActiveUsersRequest
userFunc func() (*iam.User, auth.Account)
wantErr error
expectedItemsReturned int
}{
{
name: "global role with wildcard returns monthly active users",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
expectedItemsReturned: 2,
},
{
name: "global role + direct association with type billing returns monthly active users",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
expectedItemsReturned: 2,
},
{
name: "global role + direct association with multiple roles returns monthly active users",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=group;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=no-op"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=monthly-active-users"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
expectedItemsReturned: 2,
},
{
name: "global role + group association with type billing returns monthly active users",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
expectedItemsReturned: 2,
},
{
name: "global role + managed group association with type billing returns monthly active users",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
expectedItemsReturned: 2,
},
{
name: "global role with type billing and actions monthly-active-users returns monthly active users",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=monthly-active-users"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
expectedItemsReturned: 2,
},
{
name: "global role with non-applicable type returns an error",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=group;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
wantErr: handlers.ForbiddenError(),
},
{
name: "global role with billing type and non-applicable actions returns an error",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=billing;actions=no-op"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
wantErr: handlers.ForbiddenError(),
},
{
name: "global role grant this with a non-applicable type returns a permission error",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=group;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
wantErr: handlers.ForbiddenError(),
},
{
name: "global role grant descendant returns no monthly active users",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=billing;actions=*"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
wantErr: handlers.ForbiddenError(),
},
{
name: "global role grant descendant returns no monthly active users",
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=billing;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
input: &pbs.MonthlyActiveUsersRequest{
StartTime: threeMonthsAgo,
EndTime: oneMonthAgo,
},
wantErr: handlers.ForbiddenError(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, accountID := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, accountID.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := cauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.MonthlyActiveUsers(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
return
}
require.NoError(t, finalErr)
require.Len(t, got.Items, tc.expectedItemsReturned)
})
}
})
}
@@ -151,7 +151,7 @@ func (s Service) ListCredentialLibraries(ctx context.Context, req *pbs.ListCrede
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetCredentialStoreId(), action.List)
authResults := s.authResult(ctx, req.GetCredentialStoreId(), action.List, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -281,7 +281,7 @@ func (s Service) GetCredentialLibrary(ctx context.Context, req *pbs.GetCredentia
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -319,7 +319,7 @@ func (s Service) CreateCredentialLibrary(ctx context.Context, req *pbs.CreateCre
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetCredentialStoreId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetCredentialStoreId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -382,7 +382,7 @@ func (s Service) UpdateCredentialLibrary(ctx context.Context, req *pbs.UpdateCre
if err := validateUpdateRequest(req, currentCredentialType); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -418,7 +418,7 @@ func (s Service) DeleteCredentialLibrary(ctx context.Context, req *pbs.DeleteCre
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -598,7 +598,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
return rows > 0, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
const op = "credentiallibraries.(Service).authResult"
res := auth.VerifyResults{}
repo, err := s.repoFn()
@@ -608,7 +608,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.CredentialLibrary), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -667,8 +667,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
res.Error = errors.New(ctx, errors.InvalidParameter, op, "unrecognized credential store subtype from id")
return res
}
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.CredentialLibrary, opts...)
}
func newOutputOpts(
@@ -8,9 +8,11 @@ import (
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/credential/vault"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
controllerauth "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"
@@ -31,6 +33,8 @@ func TestGrants_ReadActions(t *testing.T) {
}
kmsCache := kms.TestKms(t, conn, wrap)
sche := scheduler.TestScheduler(t, conn, wrap)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
vaultRepoFn := func() (*vault.Repository, error) {
return vault.NewRepository(ctx, rw, rw, kmsCache, sche)
@@ -45,24 +49,24 @@ func TestGrants_ReadActions(t *testing.T) {
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListCredentialLibrariesRequest
rolesToCreate []authtoken.TestRoleGrantsForToken
wantErr error
wantIDs []string
name string
input *pbs.ListCredentialLibrariesRequest
userFunc func() (*iam.User, auth.Account)
wantErr error
wantIDs []string
}{
{
name: "global role grant descendant returns all credentials library",
input: &pbs.ListCredentialLibrariesRequest{
CredentialStoreId: proj1CredStore[0].GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=credential-library;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
RoleScopeId: globals.GlobalPrefix,
Grants: []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()},
},
@@ -71,13 +75,13 @@ func TestGrants_ReadActions(t *testing.T) {
input: &pbs.ListCredentialLibrariesRequest{
CredentialStoreId: proj1CredStore[0].GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org.GetPublicId(),
GrantStrings: []string{"ids=*;type=credential-library;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org.GetPublicId(),
Grants: []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()},
},
@@ -85,8 +89,10 @@ func TestGrants_ReadActions(t *testing.T) {
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)
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.ListCredentialLibraries(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
@@ -130,7 +130,7 @@ func (s Service) ListCredentials(ctx context.Context, req *pbs.ListCredentialsRe
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetCredentialStoreId(), action.List)
authResults := s.authResult(ctx, req.GetCredentialStoreId(), action.List, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -260,7 +260,7 @@ func (s Service) GetCredential(ctx context.Context, req *pbs.GetCredentialReques
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -298,7 +298,7 @@ func (s Service) CreateCredential(ctx context.Context, req *pbs.CreateCredential
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetCredentialStoreId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetCredentialStoreId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -345,7 +345,7 @@ func (s Service) UpdateCredential(ctx context.Context, req *pbs.UpdateCredential
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -381,7 +381,7 @@ func (s Service) DeleteCredential(ctx context.Context, req *pbs.DeleteCredential
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -584,7 +584,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
return rows > 0, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
const op = "credentials.(Service).authResult"
res := auth.VerifyResults{}
repo, err := s.repoFn()
@@ -594,7 +594,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Credential), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -629,8 +629,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
return res
}
opts = append(opts, auth.WithScopeId(cs.GetProjectId()))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Credential, opts...)
}
func newOutputOpts(
@@ -6,20 +6,43 @@ package credentials_test
import (
"context"
"fmt"
"maps"
"slices"
"testing"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/credential"
"github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
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/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"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentials"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
// UsernamePasswordAttributesField is the field name for the UsernamePassword subtype of the `Credential.Attrs` attributes field
//
// When the "attributes" field is specified as an output_field and can be one of many sub-types, the expected output field must be the corresponding sub-type and not `globals.AttributesField`
UsernamePasswordAttributesField = "username_password_attributes"
)
// expectedOutput consolidates common output fields for the test cases
type expectedOutput struct {
err error
outputFields []string
}
// TestGrants_ReadActions tests read actions to assert that grants are being applied properly
//
// Role - which scope the role is created in
@@ -39,9 +62,8 @@ func TestGrants_ReadActions(t *testing.T) {
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)
iamRepo := iam.TestRepo(t, conn, wrap)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
@@ -51,88 +73,803 @@ func TestGrants_ReadActions(t *testing.T) {
s, err := credentials.NewService(ctx, iamRepoFn, staticRepoFn, 1000)
require.NoError(t, err)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
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++ {
var credIds []string
for i := range 5 {
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())
c := static.TestUsernamePasswordCredential(t, conn, wrap, user, pass, credStore.GetPublicId(), proj.GetPublicId(), static.WithName(fmt.Sprintf("cred-%d", i)), static.WithDescription(fmt.Sprintf("desc-%d", i)))
credIds = append(credIds, c.GetPublicId())
}
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListCredentialsRequest
rolesToCreate []authtoken.TestRoleGrantsForToken
userFunc func() (*iam.User, auth.Account)
wantErr error
wantIDs []string
wantOutfields map[string][]string
}{
{
name: "global role grant this returns all created credentials",
input: &pbs.ListCredentialsRequest{
CredentialStoreId: credStore.GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
name: "global role grant this returns 403 because credentials live on projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=credential;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
wantErr: nil,
wantIDs: wantCreds,
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "org role grant this returns all created credentials",
input: &pbs.ListCredentialsRequest{
CredentialStoreId: credStore.GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
name: "org role grant this returns 403 because credentials live on projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org.PublicId,
GrantStrings: []string{"ids=*;type=credential;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org.GetPublicId(),
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
wantErr: nil,
wantIDs: wantCreds,
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "project role grant this returns all created credentials",
input: &pbs.ListCredentialsRequest{
CredentialStoreId: credStore.GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
name: "project role grant this returns all credentials on the project",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: proj.PublicId,
GrantStrings: []string{"ids=*;type=credential;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField},
credIds[1]: {globals.IdField},
credIds[2]: {globals.IdField},
credIds[3]: {globals.IdField},
credIds[4]: {globals.IdField},
},
wantErr: nil,
wantIDs: wantCreds,
},
{
name: "global role grant this & children returns 403 because credentials live on projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "org role grant children returns all credentials on child projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,credential_store_id,scope,name,description,created_time,updated_time,version,type,authorized_actions,attributes"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[1]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[2]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[3]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[4]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
},
},
{
name: "global role grant this & descendants returns all credentials on descendant projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,scope,type,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField},
credIds[1]: {globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField},
credIds[2]: {globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField},
credIds[3]: {globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField},
credIds[4]: {globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField},
},
},
{
name: "project role grant this returns all credentials on the project",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,credential_store_id,scope,name,description,created_time,updated_time,version,type,authorized_actions,attributes"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[1]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[2]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[3]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
credIds[4]: {globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField},
},
},
{
name: "global role grant with credential-store id, list action, credential type, and descendant scope returns all credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=list,no-op;output_fields=id,name,description,credential_store_id"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.CredentialStoreIdField},
credIds[1]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.CredentialStoreIdField},
credIds[2]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.CredentialStoreIdField},
credIds[3]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.CredentialStoreIdField},
credIds[4]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.CredentialStoreIdField},
},
},
{
name: "org role grant with credential-store id, list action, credential type, and children scope returns all credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=list,no-op;output_fields=id,name,description,version,credential_store_id"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[1]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[2]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[3]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[4]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
},
},
{
name: "project role grant with credential-store id, list action, credential type, and this scope returns all credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=list,no-op;output_fields=id,name,description,version,credential_store_id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantOutfields: map[string][]string{
credIds[0]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[1]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[2]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[3]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
credIds[4]: {globals.IdField, globals.NameField, globals.DescriptionField, globals.VersionField, globals.CredentialStoreIdField},
},
},
{
name: "incorrect grants can't list credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=credential;actions=read,create,update,delete"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "no grants returns 403",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, nil),
wantErr: 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 := auth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.ListCredentials(fullGrantAuthCtx, tc.input)
// Call function to create direct/indirect relationship
user, account := tc.userFunc()
// Create auth token for the user
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
// Create auth context from the auth token
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.ListCredentials(fullGrantAuthCtx, &pbs.ListCredentialsRequest{CredentialStoreId: credStore.GetPublicId()})
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
return
}
require.NoError(t, finalErr)
var gotIDs []string
var gotIds []string
for _, g := range got.Items {
gotIDs = append(gotIDs, g.GetId())
gotIds = append(gotIds, g.GetId())
// check if the output fields are as expected
if tc.wantOutfields[g.Id] != nil {
handlers.TestAssertOutputFields(t, g, tc.wantOutfields[g.Id])
}
}
wantIds := slices.Collect(maps.Keys(tc.wantOutfields))
require.ElementsMatch(t, wantIds, gotIds)
})
}
})
t.Run("Get", func(t *testing.T) {
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
canGetCredential map[string]expectedOutput
}{
{
name: "global role grant this returns 403",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {err: handlers.ForbiddenError()},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {err: handlers.ForbiddenError()},
},
},
{
name: "org role grant this returns 403",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {err: handlers.ForbiddenError()},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {err: handlers.ForbiddenError()},
},
},
{
name: "project role grant this returns all credentials on the project",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {outputFields: []string{globals.IdField}},
credIds[1]: {outputFields: []string{globals.IdField}},
credIds[2]: {outputFields: []string{globals.IdField}},
credIds[3]: {outputFields: []string{globals.IdField}},
credIds[4]: {outputFields: []string{globals.IdField}},
},
},
{
name: "global role grant this & children returns 403 because credentials live on projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {err: handlers.ForbiddenError()},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {err: handlers.ForbiddenError()},
},
},
{
name: "org role grant children returns all credentials on child projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {outputFields: []string{globals.IdField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[1]: {outputFields: []string{globals.IdField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[2]: {outputFields: []string{globals.IdField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[3]: {outputFields: []string{globals.IdField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[4]: {outputFields: []string{globals.IdField, globals.CreatedTimeField, globals.UpdatedTimeField}},
},
},
{
name: "global role grant this & descendants returns all credentials on descendant projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,scope,type,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[1]: {outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[2]: {outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[3]: {outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
credIds[4]: {outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
},
},
{
name: "project role grant this returns all credentials on the project",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id,credential_store_id,scope,name,description,created_time,updated_time,version,type,authorized_actions,attributes"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
credIds[1]: {outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
credIds[2]: {outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
credIds[3]: {outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
credIds[4]: {outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
},
},
{
name: "incorrect grants returns no credential",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=credential;actions=list,create,update,delete;output_fields=id,name,description,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {err: handlers.ForbiddenError()},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {err: handlers.ForbiddenError()},
},
},
{
name: "no grants returns no credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, nil),
canGetCredential: map[string]expectedOutput{
credIds[0]: {err: handlers.ForbiddenError()},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {err: handlers.ForbiddenError()},
},
},
{
name: "project role grant with credential id, get action, and this scope returns the id'd credential",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=" + credIds[0] + ";actions=read;output_fields=id,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {outputFields: []string{globals.IdField, globals.AuthorizedActionsField}},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {err: handlers.ForbiddenError()},
},
},
{
name: "org role grant with credential id, get action, and this & children scope returns the id'd credential",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=" + credIds[4] + ";actions=read;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
canGetCredential: map[string]expectedOutput{
credIds[0]: {err: handlers.ForbiddenError()},
credIds[1]: {err: handlers.ForbiddenError()},
credIds[2]: {err: handlers.ForbiddenError()},
credIds[3]: {err: handlers.ForbiddenError()},
credIds[4]: {outputFields: []string{globals.IdField}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for id, expected := range tc.canGetCredential {
input := &pbs.GetCredentialRequest{Id: id}
got, err := s.GetCredential(fullGrantAuthCtx, input)
if expected.err != nil {
require.ErrorIs(t, err, expected.err)
continue
}
// check if the output fields are as expected
if tc.canGetCredential[id].outputFields != nil {
handlers.TestAssertOutputFields(t, got.Item, tc.canGetCredential[id].outputFields)
}
require.NoError(t, err)
}
require.ElementsMatch(t, tc.wantIDs, gotIDs)
})
}
})
}
func TestGrants_WriteActions(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
rw := db.New(conn)
kmsCache := kms.TestKms(t, conn, wrap)
iamRepo := iam.TestRepo(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)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org, proj := iam.TestScopes(t, iamRepo)
t.Run("Create", func(t *testing.T) {
credStore := static.TestCredentialStore(t, conn, wrap, proj.GetPublicId())
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
expected expectedOutput
}{
{
name: "global role grant this can't create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
{
name: "org role grant this can't create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
{
name: "project role grant this can create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField}},
},
{
name: "global role grant this & descendants can create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=credential;actions=create,read,update;output_fields=id,credential_store_id,scope,name,description,created_time,updated_time,version,type,authorized_actions,attributes"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
},
{
name: "org role grant this & children can create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=credential;actions=create;output_fields=id,scope,type,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
},
{
name: "global role grant pinned to credential-store id, credential type, and descendant scope can create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=create;output_fields=id,credential_store_id,name,scope,description,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.NameField, globals.ScopeField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
},
{
name: "global role grant pinned to credential-store id, credential type, and children scope cannot create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=create;output_fields=id,credential_store_id,name,scope,description,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
{
name: "org role grant pinned to credential-store id, credential type, and children scope can create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=create;output_fields=id,credential_store_id,name,scope,description,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.NameField, globals.ScopeField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
},
{
name: "project role grant pinned to credential-store id, credential type, and this scope can create credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=" + credStore.PublicId + ";type=credential;actions=create;output_fields=id,credential_store_id,name,scope,description,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.NameField, globals.ScopeField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, err := s.CreateCredential(fullGrantAuthCtx, &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: credStore.GetPublicId(),
Name: &wrappers.StringValue{Value: "test credential - " + tc.name},
Description: &wrappers.StringValue{Value: "test desc"},
Type: credential.UsernamePasswordSubtype.String(),
Attrs: &pb.Credential_UsernamePasswordAttributes{
UsernamePasswordAttributes: &pb.UsernamePasswordAttributes{
Username: wrapperspb.String("static-username"),
Password: wrapperspb.String("static-password"),
},
},
}})
if tc.expected.err != nil {
require.ErrorIs(t, err, tc.expected.err)
return
}
// check if the output fields are as expected
require.NoError(t, err)
handlers.TestAssertOutputFields(t, got.Item, tc.expected.outputFields)
})
}
})
t.Run("Update", func(t *testing.T) {
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
expected expectedOutput
}{
{
name: "global role grant this can't update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
{
name: "org role grant this can't update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
{
name: "project role grant this can update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField}},
},
{
name: "global role grant this & descendants can update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=credential;actions=update;output_fields=id,credential_store_id,scope,name,description,created_time,updated_time,version,type,authorized_actions,attributes"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.CredentialStoreIdField, globals.ScopeField, globals.NameField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, UsernamePasswordAttributesField}},
},
{
name: "org role grant this & children can update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=credential;actions=update;output_fields=id,scope,type,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
expected: expectedOutput{outputFields: []string{globals.IdField, globals.ScopeField, globals.TypeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
},
{
name: "incorrect grants can't update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=credential;actions=list,create,delete;output_fields=id,name,description,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
{
name: "no grants can't update credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, nil),
expected: expectedOutput{err: handlers.ForbiddenError()},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
credStore := static.TestCredentialStore(t, conn, wrap, proj.GetPublicId())
cred := static.TestUsernamePasswordCredential(t, conn, wrap, "user", "pass", credStore.GetPublicId(), proj.GetPublicId(), static.WithName("cred"), static.WithDescription("desc"))
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, err := s.UpdateCredential(fullGrantAuthCtx, &pbs.UpdateCredentialRequest{
Id: cred.PublicId,
Item: &pb.Credential{
Name: &wrappers.StringValue{Value: "updated credential name (" + tc.name + ")"},
Description: &wrappers.StringValue{Value: "test desc"},
Version: 1,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "description"}},
})
if tc.expected.err != nil {
require.ErrorIs(t, err, tc.expected.err)
return
}
// check if the output fields are as expected
require.NoError(t, err)
handlers.TestAssertOutputFields(t, got.Item, tc.expected.outputFields)
})
}
})
t.Run("Delete", func(t *testing.T) {
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
wantErr error
}{
{
name: "global role grant this can't delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "org role grant this can't delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "project role grant this can delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
},
{
name: "global role grant this & descendants can delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=credential;actions=delete"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
},
{
name: "org role grant this & children can delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=credential;actions=delete"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
},
{
name: "incorrect grants can't delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=credential;actions=list,create,update"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "no grants can't delete credentials",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, nil),
wantErr: handlers.ForbiddenError(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
credStore := static.TestCredentialStore(t, conn, wrap, proj.GetPublicId())
cred := static.TestUsernamePasswordCredential(t, conn, wrap, "user", "pass", credStore.GetPublicId(), proj.GetPublicId(), static.WithName("cred"), static.WithDescription("desc"))
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
_, err = s.DeleteCredential(fullGrantAuthCtx, &pbs.DeleteCredentialRequest{Id: cred.PublicId})
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return
}
require.NoError(t, err)
})
}
})
@@ -73,6 +73,12 @@ var (
staticCollectionTypeMap = map[resource.Type]action.ActionSet{
resource.Credential: credentials.CollectionActions,
}
additionalResourceGrants = []resource.Type{
resource.Credential,
resource.CredentialLibrary,
}
validateVaultWorkerFilterFn = vaultWorkerFilterUnsupported
vaultWorkerFilterToProto = false
)
@@ -149,7 +155,7 @@ func (s Service) ListCredentialStores(ctx context.Context, req *pbs.ListCredenti
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -301,7 +307,7 @@ func (s Service) GetCredentialStore(ctx context.Context, req *pbs.GetCredentialS
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -324,7 +330,7 @@ func (s Service) GetCredentialStore(ctx context.Context, req *pbs.GetCredentialS
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, cs.GetPublicId(), IdActions).Strings()))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, cs.GetPublicId())
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, authResults.Scope, cs.GetPublicId())
if err != nil {
return nil, err
}
@@ -346,7 +352,7 @@ func (s Service) CreateCredentialStore(ctx context.Context, req *pbs.CreateCrede
if err := validateCreateRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -369,7 +375,7 @@ func (s Service) CreateCredentialStore(ctx context.Context, req *pbs.CreateCrede
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, cs.GetPublicId(), IdActions).Strings()))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, cs.GetPublicId())
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, authResults.Scope, cs.GetPublicId())
if err != nil {
return nil, err
}
@@ -394,7 +400,7 @@ func (s Service) UpdateCredentialStore(ctx context.Context, req *pbs.UpdateCrede
if err := validateUpdateRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -417,7 +423,7 @@ func (s Service) UpdateCredentialStore(ctx context.Context, req *pbs.UpdateCrede
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, cs.GetPublicId(), IdActions).Strings()))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, cs.GetPublicId())
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, authResults.Scope, cs.GetPublicId())
if err != nil {
return nil, err
}
@@ -437,7 +443,7 @@ func (s Service) DeleteCredentialStore(ctx context.Context, req *pbs.DeleteCrede
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -605,7 +611,7 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
return rows > 0, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
iamRepo, err := s.iamRepoFn()
if err != nil {
@@ -624,7 +630,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.CredentialStore), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive), auth.WithFetchAdditionalResourceGrants(additionalResourceGrants...)}
switch a {
case action.List, action.Create:
parentId = id
@@ -667,7 +673,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.CredentialStore, opts...)
}
func newOutputOpts(
@@ -697,7 +703,7 @@ func newOutputOpts(
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, item.GetPublicId())
collectionActions, err := calculateAuthorizedCollectionActions(ctx, authResults, authzScopes[item.GetProjectId()], item.GetPublicId())
if err != nil {
return nil, false, err
}
@@ -1000,15 +1006,15 @@ func validateListRequest(ctx context.Context, req *pbs.ListCredentialStoresReque
return nil
}
func calculateAuthorizedCollectionActions(ctx context.Context, authResults auth.VerifyResults, id string) (map[string]*structpb.ListValue, error) {
func calculateAuthorizedCollectionActions(ctx context.Context, authResults auth.VerifyResults, itemScopeInfo *scopes.ScopeInfo, itemId string) (map[string]*structpb.ListValue, error) {
var collectionActions map[string]*structpb.ListValue
var err error
switch globals.ResourceInfoFromPrefix(id).Subtype {
switch globals.ResourceInfoFromPrefix(itemId).Subtype {
case vault.Subtype:
collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, vaultCollectionTypeMap, authResults.Scope, id)
collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, vaultCollectionTypeMap, itemScopeInfo, itemId)
case static.Subtype:
collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, staticCollectionTypeMap, authResults.Scope, id)
collectionActions, err = auth.CalculateAuthorizedCollectionActions(ctx, authResults, staticCollectionTypeMap, itemScopeInfo, itemId)
}
if err != nil {
return nil, err
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -95,7 +95,7 @@ func (s Service) ListGroups(ctx context.Context, req *pbs.ListGroupsRequest) (*p
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -233,7 +233,7 @@ func (s Service) GetGroup(ctx context.Context, req *pbs.GetGroupRequest) (*pbs.G
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -271,7 +271,7 @@ func (s Service) CreateGroup(ctx context.Context, req *pbs.CreateGroupRequest) (
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -309,7 +309,7 @@ func (s Service) UpdateGroup(ctx context.Context, req *pbs.UpdateGroupRequest) (
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -345,7 +345,7 @@ func (s Service) DeleteGroup(ctx context.Context, req *pbs.DeleteGroupRequest) (
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -363,7 +363,7 @@ func (s Service) AddGroupMembers(ctx context.Context, req *pbs.AddGroupMembersRe
if err := validateAddGroupMembersRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddMembers)
authResults := s.authResult(ctx, req.GetId(), action.AddMembers, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -401,7 +401,7 @@ func (s Service) SetGroupMembers(ctx context.Context, req *pbs.SetGroupMembersRe
if err := validateSetGroupMembersRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetMembers)
authResults := s.authResult(ctx, req.GetId(), action.SetMembers, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -439,7 +439,7 @@ func (s Service) RemoveGroupMembers(ctx context.Context, req *pbs.RemoveGroupMem
if err := validateRemoveGroupMembersRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveMembers)
authResults := s.authResult(ctx, req.GetId(), action.RemoveMembers, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -625,7 +625,7 @@ func (s Service) removeMembersInRepo(ctx context.Context, groupId string, userId
return out, m, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
repo, err := s.repoFn()
if err != nil {
@@ -634,7 +634,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Group), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -661,7 +661,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Group, opts...)
}
func toProto(ctx context.Context, in *iam.Group, members []*iam.GroupMember, opt ...handlers.Option) (*pb.Group, error) {
File diff suppressed because it is too large. Load diff
@@ -73,6 +73,11 @@ var (
},
}
additionalResourceGrants = []resource.Type{
resource.HostSet,
resource.Host,
}
validateWorkerFilterFn = validateWorkerFilterUnsupported
workerFilterToProto = false
)
@@ -162,7 +167,7 @@ func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogs
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -318,7 +323,7 @@ func (s Service) GetHostCatalog(ctx context.Context, req *pbs.GetHostCatalogRequ
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -375,7 +380,7 @@ func (s Service) CreateHostCatalog(ctx context.Context, req *pbs.CreateHostCatal
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -435,7 +440,7 @@ func (s Service) UpdateHostCatalog(ctx context.Context, req *pbs.UpdateHostCatal
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -489,7 +494,7 @@ func (s Service) DeleteHostCatalog(ctx context.Context, req *pbs.DeleteHostCatal
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -692,11 +697,11 @@ func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
return rows > 0, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
var parentId string
opts := []auth.Option{auth.WithType(resource.HostCatalog), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -753,7 +758,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.HostCatalog, opts...)
}
func toPluginInfo(plg *plugin.Plugin) *plugins.PluginInfo {
File diff suppressed because it is too large. Load diff
@@ -124,7 +124,7 @@ func (s Service) ListHostSetsWithOptions(ctx context.Context, req *pbs.ListHostS
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
_, authResults := s.parentAndAuthResult(ctx, req.GetHostCatalogId(), action.List)
_, authResults := s.parentAndAuthResult(ctx, req.GetHostCatalogId(), action.List, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -304,7 +304,7 @@ func (s Service) GetHostSet(ctx context.Context, req *pbs.GetHostSetRequest) (*p
if err := validateGetRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -346,7 +346,7 @@ func (s Service) CreateHostSet(ctx context.Context, req *pbs.CreateHostSetReques
if err := validateCreateRequest(ctx, req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetHostCatalogId(), action.Create)
_, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetHostCatalogId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -391,7 +391,7 @@ func (s Service) UpdateHostSet(ctx context.Context, req *pbs.UpdateHostSetReques
if err := validateUpdateRequest(ctx, req); err != nil {
return nil, err
}
cat, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update)
cat, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -431,7 +431,7 @@ func (s Service) DeleteHostSet(ctx context.Context, req *pbs.DeleteHostSetReques
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -449,7 +449,7 @@ func (s Service) AddHostSetHosts(ctx context.Context, req *pbs.AddHostSetHostsRe
if err := validateAddRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.AddHosts)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.AddHosts, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -488,7 +488,7 @@ func (s Service) SetHostSetHosts(ctx context.Context, req *pbs.SetHostSetHostsRe
if err := validateSetRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.SetHosts)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.SetHosts, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -526,7 +526,7 @@ func (s Service) RemoveHostSetHosts(ctx context.Context, req *pbs.RemoveHostSetH
if err := validateRemoveRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.RemoveHosts)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.RemoveHosts, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -854,7 +854,7 @@ func (s Service) removeInRepo(ctx context.Context, projectId, setId string, host
return out, hl, nil
}
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (host.Catalog, auth.VerifyResults) {
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type, isRecursive bool) (host.Catalog, auth.VerifyResults) {
res := auth.VerifyResults{}
staticRepo, err := s.staticRepoFn()
@@ -869,7 +869,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
}
var parentId string
opts := []auth.Option{auth.WithType(resource.HostSet), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -929,7 +929,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
cat = pc
}
opts = append(opts, auth.WithScopeId(cat.GetProjectId()), auth.WithPin(parentId))
return cat, auth.Verify(ctx, opts...)
return cat, auth.Verify(ctx, resource.HostSet, opts...)
}
func toPluginInfo(plg *plugin.Plugin) *plugins.PluginInfo {
@@ -8,8 +8,10 @@ import (
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
controllerauth "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"
@@ -52,6 +54,8 @@ func TestGrants_ReadActions(t *testing.T) {
}
s, err := hosts.NewService(ctx, repoFn, pluginRepoFn, 1000)
require.NoError(t, err)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org, proj := iam.TestScopes(t, iamRepo)
@@ -68,24 +72,24 @@ func TestGrants_ReadActions(t *testing.T) {
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListHostsRequest
rolesToCreate []authtoken.TestRoleGrantsForToken
wantErr error
wantIDs []string
name string
input *pbs.ListHostsRequest
userFunc func() (*iam.User, auth.Account)
wantErr error
wantIDs []string
}{
{
name: "global role grant this returns all created hosts",
input: &pbs.ListHostsRequest{
HostCatalogId: hc.GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=host;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=host;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
},
}),
wantErr: nil,
wantIDs: wantHs,
},
@@ -94,13 +98,13 @@ func TestGrants_ReadActions(t *testing.T) {
input: &pbs.ListHostsRequest{
HostCatalogId: hc.GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org.PublicId,
GrantStrings: []string{"ids=*;type=host;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org.PublicId,
Grants: []string{"ids=*;type=host;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
},
}),
wantErr: nil,
wantIDs: wantHs,
},
@@ -109,13 +113,13 @@ func TestGrants_ReadActions(t *testing.T) {
input: &pbs.ListHostsRequest{
HostCatalogId: hc.GetPublicId(),
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: proj.PublicId,
GrantStrings: []string{"ids=*;type=host;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: proj.PublicId,
Grants: []string{"ids=*;type=host;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: wantHs,
},
@@ -123,8 +127,10 @@ func TestGrants_ReadActions(t *testing.T) {
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)
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.ListHosts(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
@@ -105,7 +105,7 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
_, authResults := s.parentAndAuthResult(ctx, req.GetHostCatalogId(), action.List)
_, authResults := s.parentAndAuthResult(ctx, req.GetHostCatalogId(), action.List, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -285,7 +285,7 @@ func (s Service) GetHost(ctx context.Context, req *pbs.GetHostRequest) (*pbs.Get
if err := validateGetRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -327,7 +327,7 @@ func (s Service) CreateHost(ctx context.Context, req *pbs.CreateHostRequest) (*p
if err := validateCreateRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetHostCatalogId(), action.Create)
_, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetHostCatalogId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -369,7 +369,7 @@ func (s Service) UpdateHost(ctx context.Context, req *pbs.UpdateHostRequest) (*p
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
cat, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update)
cat, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -407,7 +407,7 @@ func (s Service) DeleteHost(ctx context.Context, req *pbs.DeleteHostRequest) (*p
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -533,7 +533,7 @@ func (s Service) deleteFromRepo(ctx context.Context, projectId, id string) (bool
return rows > 0, nil
}
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (host.Catalog, auth.VerifyResults) {
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type, isRecursive bool) (host.Catalog, auth.VerifyResults) {
res := auth.VerifyResults{}
staticRepo, err := s.staticRepoFn()
if err != nil {
@@ -547,7 +547,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Host), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -605,7 +605,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
cat = plcat
}
opts = append(opts, auth.WithScopeId(cat.GetProjectId()), auth.WithPin(parentId))
return cat, auth.Verify(ctx, opts...)
return cat, auth.Verify(ctx, resource.Host, opts...)
}
func toPluginInfo(plg *plugin.Plugin) *plugins.PluginInfo {
File diff suppressed because it is too large. Load diff
@@ -123,7 +123,7 @@ func (s Service) ListManagedGroups(ctx context.Context, req *pbs.ListManagedGrou
if err := validateListRequest(ctx, req); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
_, authResults := s.parentAndAuthResult(ctx, req.GetAuthMethodId(), action.List)
_, authResults := s.parentAndAuthResult(ctx, req.GetAuthMethodId(), action.List, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -302,7 +302,7 @@ func (s Service) GetManagedGroup(ctx context.Context, req *pbs.GetManagedGroupRe
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -344,7 +344,7 @@ func (s Service) CreateManagedGroup(ctx context.Context, req *pbs.CreateManagedG
return nil, err
}
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetAuthMethodId(), action.Create)
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetItem().GetAuthMethodId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -383,7 +383,7 @@ func (s Service) UpdateManagedGroup(ctx context.Context, req *pbs.UpdateManagedG
return nil, err
}
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update)
authMeth, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -419,7 +419,7 @@ func (s Service) DeleteManagedGroup(ctx context.Context, req *pbs.DeleteManagedG
if err := validateDeleteRequest(ctx, req); err != nil {
return nil, err
}
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete)
_, authResults := s.parentAndAuthResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -708,7 +708,7 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
return rows > 0, nil
}
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (auth.AuthMethod, requestauth.VerifyResults) {
func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type, isRecursive bool) (auth.AuthMethod, requestauth.VerifyResults) {
const op = "managed_groups.(Service)."
res := requestauth.VerifyResults{}
oidcRepo, err := s.oidcRepoFn()
@@ -723,7 +723,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
}
var parentId string
opts := []requestauth.Option{requestauth.WithType(resource.ManagedGroup), requestauth.WithAction(a)}
opts := []requestauth.Option{requestauth.WithAction(a), requestauth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -789,7 +789,7 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
return nil, res
}
opts = append(opts, requestauth.WithPin(parentId))
return authMeth, requestauth.Verify(ctx, opts...)
return authMeth, requestauth.Verify(ctx, resource.ManagedGroup, opts...)
}
func toProto(ctx context.Context, in auth.ManagedGroup, opt ...handlers.Option) (*pb.ManagedGroup, error) {
@@ -5,37 +5,39 @@ package roles_test
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
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/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"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/roles"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// 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]
type expect struct {
wantErr error
wantFields []string
}
func TestGrants_ReadActions(t *testing.T) {
const (
// Set role description to this value for every role that should be included in the result set.
// This is used to work around a non-deterministic behavior from roles being created as a part of
// the test setup.
// This also means that `description` must also be included in the output_fields
roleDescription = "test role"
)
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
@@ -46,81 +48,114 @@ func TestGrants_ReadActions(t *testing.T) {
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(),
})
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
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())
}
org1, noRoleProj := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj3 := iam.TestProject(t, iamRepo, org2.PublicId, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
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"))
globalRole := iam.TestRole(t, conn, globals.GlobalPrefix, iam.WithDescription(roleDescription), iam.WithName("glob"))
iam.TestRoleGrant(t, conn, globalRole.PublicId, "ids=*;type=*;actions=*")
org1Role := iam.TestRole(t, conn, org1.GetPublicId(), iam.WithDescription(roleDescription), iam.WithName("org1"))
iam.TestRoleGrant(t, conn, org1Role.PublicId, "ids=*;type=*;actions=*")
org2Role := iam.TestRole(t, conn, org2.GetPublicId(), iam.WithDescription(roleDescription), iam.WithName("org2"))
iam.TestRoleGrant(t, conn, org2Role.PublicId, "ids=*;type=*;actions=*")
proj2Role := iam.TestRole(t, conn, proj2.GetPublicId(), iam.WithDescription(roleDescription), iam.WithName("proj2"))
iam.TestRoleGrant(t, conn, proj2Role.PublicId, "ids=*;type=*;actions=*")
proj3Role := iam.TestRole(t, conn, proj3.GetPublicId(), iam.WithDescription(roleDescription), iam.WithName("proj3"))
iam.TestRoleGrant(t, conn, proj3Role.PublicId, "ids=*;type=*;actions=*")
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListRolesRequest
rolesToCreate []authtoken.TestRoleGrantsForToken
wantErr error
addRolesAtThisScope bool
wantIDs []string
name string
input *pbs.ListRolesRequest
userFunc func() (*iam.User, auth.Account)
wantErr error
idOutputFieldsMap map[string][]string
}{
{
name: "global role grant this and descendants returns global org and project roles",
input: &pbs.ListRolesRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,name,description,scope_id,authorized_actions,principal_ids"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
wantErr: nil,
idOutputFieldsMap: map[string][]string{
globalRole.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
org1Role.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
org2Role.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
proj2Role.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
proj3Role.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
},
},
{
name: "project grant scope list recursive returns no result and no error",
input: &pbs.ListRolesRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: noRoleProj.PublicId,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,name,description,scope_id,authorized_actions,principal_ids"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
idOutputFieldsMap: map[string][]string{},
},
{
name: "project grant scope list non-recursive returns error",
input: &pbs.ListRolesRequest{
ScopeId: globals.GlobalPrefix,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: noRoleProj.PublicId,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,name,description,scope_id,authorized_actions,principal_ids"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: handlers.ForbiddenError(),
idOutputFieldsMap: map[string][]string{},
},
{
name: "global role grant this and children returns global and org roles",
input: &pbs.ListRolesRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=role;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,name,description,scope_id,authorized_actions,principal_ids"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: nil,
idOutputFieldsMap: map[string][]string{
globalRole.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
org1Role.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
org2Role.PublicId: {globals.IdField, globals.NameField, globals.DescriptionField, globals.ScopeIdField, globals.AuthorizedActionsField},
},
addRolesAtThisScope: true,
wantErr: nil,
wantIDs: append(append([]string{
org1Role.PublicId,
org2Role.PublicId,
}, defaultOrg1Roles...), defaultOrg2Roles...),
},
{
name: "no role recursive list returns error",
input: &pbs.ListRolesRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{}),
wantErr: handlers.ForbiddenError(),
},
{
name: "org role grant this and children returns org and project roles",
@@ -128,54 +163,563 @@ func TestGrants_ReadActions(t *testing.T) {
ScopeId: org2.PublicId,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org2.PublicId,
GrantStrings: []string{"ids=*;type=role;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,description,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantErr: nil,
idOutputFieldsMap: map[string][]string{
org2Role.PublicId: {globals.IdField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField},
proj2Role.PublicId: {globals.IdField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField},
proj3Role.PublicId: {globals.IdField, globals.DescriptionField, globals.CreatedTimeField, globals.UpdatedTimeField},
},
},
{
name: "org role grant this and individual project returns org and granted project roles",
input: &pbs.ListRolesRequest{
ScopeId: org2.PublicId,
Recursive: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,description,updated_time"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=role;actions=list,read;output_fields=id,description,created_time"},
GrantScopes: []string{proj3.PublicId},
},
}),
wantErr: nil,
idOutputFieldsMap: map[string][]string{
org2Role.PublicId: {globals.IdField, globals.DescriptionField, globals.UpdatedTimeField},
proj3Role.PublicId: {globals.IdField, globals.DescriptionField, globals.CreatedTimeField},
},
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...)
}
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
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())
var filteredRoles []*pb.Role
for _, r := range got.Items {
// only include roles with description that indicates that the roles are created as a part of
// the test setup
if r.GetDescription().GetValue() != roleDescription {
continue
}
filteredRoles = append(filteredRoles, r)
}
require.Len(t, filteredRoles, len(tc.idOutputFieldsMap))
for _, item := range filteredRoles {
wantFields, ok := tc.idOutputFieldsMap[item.GetId()]
require.True(t, ok)
handlers.TestAssertOutputFields(t, item, wantFields)
}
})
}
})
t.Run("Read", func(t *testing.T) {
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
inputExpectMap map[*pbs.GetRoleRequest]expect
}{
{
name: "global role grant this and descendants can read all roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=read;output_fields=id,name,created_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
inputExpectMap: map[*pbs.GetRoleRequest]expect{
{Id: globalRole.PublicId}: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
{Id: org1Role.PublicId}: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
{Id: org2Role.PublicId}: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
{Id: proj2Role.PublicId}: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
{Id: proj3Role.PublicId}: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
},
},
{
name: "global role grant this and children can read global and org roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,updated_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
inputExpectMap: map[*pbs.GetRoleRequest]expect{
{Id: globalRole.PublicId}: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
{Id: org1Role.PublicId}: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
{Id: org2Role.PublicId}: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
{Id: proj2Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj3Role.PublicId}: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "org role grant this and children can read self and projects roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,updated_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
inputExpectMap: map[*pbs.GetRoleRequest]expect{
{Id: globalRole.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org1Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org2Role.PublicId}: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
{Id: proj2Role.PublicId}: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
{Id: proj3Role.PublicId}: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
},
},
{
name: "project role grant this can read self",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputExpectMap: map[*pbs.GetRoleRequest]expect{
{Id: globalRole.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org1Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org2Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj2Role.PublicId}: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
{Id: proj3Role.PublicId}: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "individually granted scopes can read granted scopes roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
}),
inputExpectMap: map[*pbs.GetRoleRequest]expect{
{Id: globalRole.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org1Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org2Role.PublicId}: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
{Id: proj2Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj3Role.PublicId}: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
{
name: "multiple roles can read granted scopes roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputExpectMap: map[*pbs.GetRoleRequest]expect{
{Id: globalRole.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org1Role.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: org2Role.PublicId}: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
{Id: proj2Role.PublicId}: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
{Id: proj3Role.PublicId}: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for input, expected := range tc.inputExpectMap {
got, finalErr := s.GetRole(fullGrantAuthCtx, input)
if expected.wantErr != nil {
require.ErrorIs(t, finalErr, expected.wantErr)
continue
}
require.NoError(t, finalErr)
handlers.TestAssertOutputFields(t, got.Item, expected.wantFields)
}
require.ElementsMatch(t, tc.wantIDs, gotIDs)
})
}
})
}
func TestGrants_CreateRole(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)
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, _ := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj3 := iam.TestProject(t, iamRepo, org2.PublicId, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
inputScopeExpectMap map[string]expect
}{
{
name: "global role grant this and descendants can create all roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=create;output_fields=id,name,created_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
inputScopeExpectMap: map[string]expect{
globals.GlobalPrefix: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
org1.PublicId: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
org2.PublicId: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
proj2.PublicId: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
proj3.PublicId: {wantFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField}},
},
},
{
name: "global role grant this and children can create global and org roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,updated_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
inputScopeExpectMap: map[string]expect{
globals.GlobalPrefix: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
org1.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
org2.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
proj2.PublicId: {wantErr: handlers.ForbiddenError()},
proj3.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "org role grant this and children can create self and projects roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,updated_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
inputScopeExpectMap: map[string]expect{
globals.GlobalPrefix: {wantErr: handlers.ForbiddenError()},
org1.PublicId: {wantErr: handlers.ForbiddenError()},
org2.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
proj2.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
proj3.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
},
},
{
name: "project role grant this can create self",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputScopeExpectMap: map[string]expect{
globals.GlobalPrefix: {wantErr: handlers.ForbiddenError()},
org1.PublicId: {wantErr: handlers.ForbiddenError()},
org2.PublicId: {wantErr: handlers.ForbiddenError()},
proj2.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj3.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "individually granted scopes can create granted scopes roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
}),
inputScopeExpectMap: map[string]expect{
globals.GlobalPrefix: {wantErr: handlers.ForbiddenError()},
org1.PublicId: {wantErr: handlers.ForbiddenError()},
org2.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj2.PublicId: {wantErr: handlers.ForbiddenError()},
proj3.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
{
name: "multiple roles can create granted scopes roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputScopeExpectMap: map[string]expect{
globals.GlobalPrefix: {wantErr: handlers.ForbiddenError()},
org1.PublicId: {wantErr: handlers.ForbiddenError()},
org2.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj2.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj3.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for inputScopeId, expected := range tc.inputScopeExpectMap {
randomId, err := uuid.GenerateUUID()
require.NoError(t, err)
got, finalErr := s.CreateRole(fullGrantAuthCtx, &pbs.CreateRoleRequest{
Item: &pb.Role{
ScopeId: inputScopeId,
Name: wrapperspb.String(randomId),
Description: wrapperspb.String(randomId),
},
})
if expected.wantErr != nil {
require.ErrorIs(t, finalErr, expected.wantErr)
continue
}
require.NoError(t, finalErr)
handlers.TestAssertOutputFields(t, got.Item, expected.wantFields)
}
})
}
}
func TestGrants_SetRoleGrants(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)
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, _ := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj3 := iam.TestProject(t, iamRepo, org2.PublicId, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
globalRole := iam.TestRole(t, conn, globals.GlobalPrefix)
iam.TestRoleGrant(t, conn, globalRole.PublicId, "ids=*;type=*;actions=*")
org1Role := iam.TestRole(t, conn, org1.GetPublicId())
iam.TestRoleGrant(t, conn, org1Role.PublicId, "ids=*;type=*;actions=*")
org2Role := iam.TestRole(t, conn, org2.GetPublicId())
iam.TestRoleGrant(t, conn, org2Role.PublicId, "ids=*;type=*;actions=*")
proj2Role := iam.TestRole(t, conn, proj2.GetPublicId())
iam.TestRoleGrant(t, conn, proj2Role.PublicId, "ids=*;type=*;actions=*")
proj3Role := iam.TestRole(t, conn, proj3.GetPublicId())
iam.TestRoleGrant(t, conn, proj3Role.PublicId, "ids=*;type=*;actions=*")
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
inputRoleIdExpectMap map[string]expect
}{
{
name: "global role grant this and descendants can set role grants in all roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=set-grants;output_fields=id,updated_time,created_time"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.CreatedTimeField}},
org1Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.CreatedTimeField}},
org2Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.CreatedTimeField}},
proj2Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.CreatedTimeField}},
proj3Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.CreatedTimeField}},
},
},
{
name: "global role grant this and children can set role grants in global and org roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,updated_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
org1Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
org2Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
proj2Role.PublicId: {wantErr: handlers.ForbiddenError()},
proj3Role.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "org role grant this and children can set role grants in self and projects roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,updated_time,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantErr: handlers.ForbiddenError()},
org1Role.PublicId: {wantErr: handlers.ForbiddenError()},
org2Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
proj2Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
proj3Role.PublicId: {wantFields: []string{globals.IdField, globals.UpdatedTimeField, globals.AuthorizedActionsField}},
},
},
{
name: "project role grant this can set role grants in self",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantErr: handlers.ForbiddenError()},
org1Role.PublicId: {wantErr: handlers.ForbiddenError()},
org2Role.PublicId: {wantErr: handlers.ForbiddenError()},
proj2Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj3Role.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "individually granted scopes can set role grants in granted scopes roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantErr: handlers.ForbiddenError()},
org1Role.PublicId: {wantErr: handlers.ForbiddenError()},
org2Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj2Role.PublicId: {wantErr: handlers.ForbiddenError()},
proj3Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
{
name: "multiple roles can set role grants in granted scopes roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=role;actions=*;output_fields=id,version,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantErr: handlers.ForbiddenError()},
org1Role.PublicId: {wantErr: handlers.ForbiddenError()},
org2Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj2Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj3Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
{
name: "pinned id and action set role grants",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, oidc.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{fmt.Sprintf("ids=%s,%s;actions=read,set-grants;output_fields=id,version,authorized_actions", org2Role.PublicId, proj3Role.PublicId)},
GrantScopes: []string{org2.PublicId, proj3.PublicId},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{fmt.Sprintf("ids=%s;actions=read,set-grants;output_fields=id,version,authorized_actions", proj2Role.PublicId)},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputRoleIdExpectMap: map[string]expect{
globalRole.PublicId: {wantErr: handlers.ForbiddenError()},
org1Role.PublicId: {wantErr: handlers.ForbiddenError()},
org2Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj2Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
proj3Role.PublicId: {wantFields: []string{globals.IdField, globals.VersionField, globals.AuthorizedActionsField}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for inputRoleId, expected := range tc.inputRoleIdExpectMap {
readbackRole, _, _, _, err := iamRepo.LookupRole(ctx, inputRoleId)
require.NoError(t, err)
randomId, err := uuid.GenerateUUID()
require.NoError(t, err)
got, finalErr := s.SetRoleGrants(fullGrantAuthCtx, &pbs.SetRoleGrantsRequest{
Id: inputRoleId,
Version: readbackRole.Version,
// use any randomized grants string
GrantStrings: []string{fmt.Sprintf("ids=ttcp_%s;actions=read", randomId[:6])},
})
if expected.wantErr != nil {
require.ErrorIs(t, finalErr, expected.wantErr)
continue
}
require.NoError(t, finalErr)
handlers.TestAssertOutputFields(t, got.Item, expected.wantFields)
}
})
}
}
@@ -104,7 +104,7 @@ func (s Service) ListRoles(ctx context.Context, req *pbs.ListRolesRequest) (*pbs
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -244,7 +244,7 @@ func (s Service) GetRole(ctx context.Context, req *pbs.GetRoleRequest) (*pbs.Get
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -282,7 +282,7 @@ func (s Service) CreateRole(ctx context.Context, req *pbs.CreateRoleRequest) (*p
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -320,7 +320,7 @@ func (s Service) UpdateRole(ctx context.Context, req *pbs.UpdateRoleRequest) (*p
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -356,7 +356,7 @@ func (s Service) DeleteRole(ctx context.Context, req *pbs.DeleteRoleRequest) (*p
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -374,7 +374,7 @@ func (s Service) AddRolePrincipals(ctx context.Context, req *pbs.AddRolePrincipa
if err := validateAddRolePrincipalsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddPrincipals)
authResults := s.authResult(ctx, req.GetId(), action.AddPrincipals, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -412,7 +412,7 @@ func (s Service) SetRolePrincipals(ctx context.Context, req *pbs.SetRolePrincipa
if err := validateSetRolePrincipalsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetPrincipals)
authResults := s.authResult(ctx, req.GetId(), action.SetPrincipals, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -450,7 +450,7 @@ func (s Service) RemoveRolePrincipals(ctx context.Context, req *pbs.RemoveRolePr
if err := validateRemoveRolePrincipalsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemovePrincipals)
authResults := s.authResult(ctx, req.GetId(), action.RemovePrincipals, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -488,7 +488,7 @@ func (s Service) AddRoleGrants(ctx context.Context, req *pbs.AddRoleGrantsReques
if err := validateAddRoleGrantsRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddGrants)
authResults := s.authResult(ctx, req.GetId(), action.AddGrants, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -526,7 +526,7 @@ func (s Service) SetRoleGrants(ctx context.Context, req *pbs.SetRoleGrantsReques
if err := validateSetRoleGrantsRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetGrants)
authResults := s.authResult(ctx, req.GetId(), action.SetGrants, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -564,7 +564,7 @@ func (s Service) RemoveRoleGrants(ctx context.Context, req *pbs.RemoveRoleGrants
if err := validateRemoveRoleGrantsRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveGrants)
authResults := s.authResult(ctx, req.GetId(), action.RemoveGrants, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -602,7 +602,7 @@ func (s Service) AddRoleGrantScopes(ctx context.Context, req *pbs.AddRoleGrantSc
if err := validateRoleGrantScopesRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddGrantScopes)
authResults := s.authResult(ctx, req.GetId(), action.AddGrantScopes, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -641,7 +641,7 @@ func (s Service) SetRoleGrantScopes(ctx context.Context, req *pbs.SetRoleGrantSc
if err := validateRoleGrantScopesRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetGrantScopes)
authResults := s.authResult(ctx, req.GetId(), action.SetGrantScopes, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -680,7 +680,7 @@ func (s Service) RemoveRoleGrantScopes(ctx context.Context, req *pbs.RemoveRoleG
if err := validateRoleGrantScopesRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveGrants)
authResults := s.authResult(ctx, req.GetId(), action.RemoveGrants, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -952,7 +952,7 @@ func (s Service) addGrantScopesInRepo(ctx context.Context, req grantScopeRequest
deduped := strutil.RemoveDuplicates(req.GetGrantScopeIds(), false)
if err := validateRoleGrantScopesHierarchy(ctx, repo, req.GetId(), deduped); err != nil {
if err := validateAndCleanRoleGrantScopesHierarchy(ctx, repo, req.GetId(), deduped); err != nil {
return nil, nil, nil, nil, err
}
@@ -980,7 +980,7 @@ func (s Service) setGrantScopesInRepo(ctx context.Context, req grantScopeRequest
deduped := strutil.RemoveDuplicates(req.GetGrantScopeIds(), false)
if err := validateRoleGrantScopesHierarchy(ctx, repo, req.GetId(), deduped); err != nil {
if err := validateAndCleanRoleGrantScopesHierarchy(ctx, repo, req.GetId(), deduped); err != nil {
return nil, nil, nil, nil, err
}
@@ -1005,7 +1005,6 @@ func (s Service) removeGrantScopesInRepo(ctx context.Context, req grantScopeRequ
if err != nil {
return nil, nil, nil, nil, err
}
_, err = repo.DeleteRoleGrantScopes(ctx, req.GetId(), req.GetVersion(), strutil.RemoveDuplicates(req.GetGrantScopeIds(), false))
if err != nil {
// TODO: Figure out a way to surface more helpful error info beyond the Internal error.
@@ -1021,7 +1020,7 @@ func (s Service) removeGrantScopesInRepo(ctx context.Context, req grantScopeRequ
return out, pr, roleGrants, grantScopes, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isResursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
repo, err := s.repoFn()
if err != nil {
@@ -1030,7 +1029,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Role), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isResursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -1057,7 +1056,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Role, opts...)
}
func toProto(ctx context.Context, in *iam.Role, principals []*iam.PrincipalRole, grants []*iam.RoleGrant, grantScopes []*iam.RoleGrantScope, opt ...handlers.Option) (*pb.Role, error) {
@@ -1458,13 +1457,15 @@ func validateRoleGrantScopesRequest(ctx context.Context, req grantScopeRequest)
return nil
}
// validateRoleGrantScopesHierarchy is the companion to the domain-side logic to
// validateAndCleanRoleGrantScopesHierarchy is the companion to the domain-side logic to
// validate scopes. It doesn't do all of the same checking but will allow for
// better error messages when possible. We perform this check after
// authentication to limit the possibility of an anonymous user causing DB load
// due to this lookup, which is not a cheap one.
func validateRoleGrantScopesHierarchy(ctx context.Context, repo *iam.Repository, roleId string, grantScopes []string) error {
const op = "service.(Service).validateRoleGrantScopesHierarchy"
// This function also converts grant scope that is the role's scope ID to 'this'
// by mutating grantScopes input
func validateAndCleanRoleGrantScopesHierarchy(ctx context.Context, repo *iam.Repository, roleId string, grantScopes []string) error {
const op = "service.(Service).validateAndCleanRoleGrantScopesHierarchy"
// We want to ensure that the values being passed in make sense to whatever
// extent we can right now, so we can provide nice errors back instead of DB
// errors.
@@ -1474,12 +1475,18 @@ func validateRoleGrantScopesHierarchy(ctx context.Context, repo *iam.Repository,
}
switch {
case role.ScopeId == scope.Global.String():
// Nothing, any grant scope is allowed for global
for i, grantScope := range grantScopes {
if grantScope == scope.Global.String() {
grantScopes[i] = globals.GrantScopeThis
}
}
case strings.HasPrefix(role.ScopeId, scope.Project.Prefix()):
// In this case only "this" or the same project scope is allowed
for _, grantScope := range grantScopes {
for i, grantScope := range grantScopes {
switch grantScope {
case globals.GrantScopeThis, role.ScopeId:
case globals.GrantScopeThis:
case role.ScopeId:
grantScopes[i] = globals.GrantScopeThis
default:
return handlers.InvalidArgumentErrorf(
"Invalid grant scope.",
@@ -1490,10 +1497,11 @@ func validateRoleGrantScopesHierarchy(ctx context.Context, repo *iam.Repository,
}
case strings.HasPrefix(role.ScopeId, scope.Org.Prefix()):
// Orgs can have "this", its own scope, a project scope, or "children"
for _, grantScope := range grantScopes {
for i, grantScope := range grantScopes {
switch {
case grantScope == role.ScopeId,
grantScope == globals.GrantScopeThis,
case grantScope == role.ScopeId:
grantScopes[i] = globals.GrantScopeThis
case grantScope == globals.GrantScopeThis,
grantScope == globals.GrantScopeChildren,
strings.HasPrefix(grantScope, scope.Project.Prefix()):
default:
@@ -219,8 +219,7 @@ func TestList(t *testing.T) {
var wantProjRoles []*pb.Role
var totalRoles []*pb.Role
for i := 0; i < 10; i++ {
or := iam.TestRole(t, conn, oWithRoles.GetPublicId())
_ = iam.TestRoleGrantScope(t, conn, or.GetPublicId(), globals.GrantScopeChildren)
or := iam.TestRole(t, conn, oWithRoles.GetPublicId(), iam.WithGrantScopeIds([]string{globals.GrantScopeThis, globals.GrantScopeChildren}))
wantOrgRoles = append(wantOrgRoles, &pb.Role{
Id: or.GetPublicId(),
ScopeId: or.GetScopeId(),
@@ -908,7 +907,7 @@ func TestCreate(t *testing.T) {
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
GrantScopeIds: []string{globals.GrantScopeThis},
Version: 2,
Version: 1,
AuthorizedActions: testAuthorizedActions,
},
},
@@ -928,7 +927,7 @@ func TestCreate(t *testing.T) {
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
GrantScopeIds: []string{globals.GrantScopeThis},
Version: 2,
Version: 1,
AuthorizedActions: testAuthorizedActions,
},
},
@@ -950,7 +949,7 @@ func TestCreate(t *testing.T) {
Name: &wrapperspb.StringValue{Value: "name"},
Description: &wrapperspb.StringValue{Value: "desc"},
GrantScopeIds: []string{globals.GrantScopeThis},
Version: 2,
Version: 1,
AuthorizedActions: testAuthorizedActions,
},
},
@@ -2602,9 +2601,9 @@ func TestAddGrantScopes(t *testing.T) {
{
name: "Add grant scopes on role with grant scopes - global",
scopeId: scope.Global.String(),
existing: []string{"global", o.PublicId},
existing: []string{"global"},
add: []string{"children", p.PublicId},
result: []string{"global", o.PublicId, p.PublicId, "children"},
result: []string{"this", p.PublicId, "children"},
},
{
name: "Add duplicate grant on role with grant scopes - global",
@@ -2624,8 +2623,8 @@ func TestAddGrantScopes(t *testing.T) {
name: "Add grant scope matching existing grant scopes - global",
scopeId: scope.Global.String(),
existing: []string{"this"},
add: []string{"this", "children"},
wantErr: true,
add: []string{"this"},
result: []string{"this"},
},
{
name: "Add invalid grant scope - global",
@@ -2639,14 +2638,16 @@ func TestAddGrantScopes(t *testing.T) {
scopeId: scope.Global.String(),
existing: []string{scope.Global.String()},
add: []string{"this"},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Add scope to this grant scope - global",
scopeId: scope.Global.String(),
existing: []string{"this"},
add: []string{scope.Global.String()},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Add children when descendants exists - global",
@@ -2674,6 +2675,7 @@ func TestAddGrantScopes(t *testing.T) {
existing: []string{o.PublicId, p.PublicId},
add: []string{"children"},
result: []string{o.PublicId, p.PublicId, "children"},
wantErr: true,
},
{
name: "Add duplicate grant on role with grant scopes - org",
@@ -2687,7 +2689,8 @@ func TestAddGrantScopes(t *testing.T) {
scopeId: o.PublicId,
existing: []string{"this"},
add: []string{"this", "children"},
wantErr: true,
result: []string{"this", "children"},
wantErr: false,
},
{
name: "Add invalid grant scope - org",
@@ -2708,14 +2711,16 @@ func TestAddGrantScopes(t *testing.T) {
scopeId: o.PublicId,
existing: []string{o.PublicId},
add: []string{"this"},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Add scope to this grant scope - org",
scopeId: o.PublicId,
existing: []string{"this"},
add: []string{o.PublicId},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Add other org/proj scopes - org",
@@ -2728,7 +2733,7 @@ func TestAddGrantScopes(t *testing.T) {
name: "Add grant scopes on empty role - proj with id",
scopeId: p.PublicId,
add: []string{p.PublicId},
result: []string{p.PublicId},
result: []string{"this"},
},
{
name: "Add grant scopes on empty role - proj with this",
@@ -2768,14 +2773,16 @@ func TestAddGrantScopes(t *testing.T) {
scopeId: p.PublicId,
existing: []string{p.PublicId},
add: []string{"this"},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Add scope to this grant scope - proj",
scopeId: p.PublicId,
existing: []string{"this"},
add: []string{p.PublicId},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Add other org/proj scopes - proj",
@@ -2791,14 +2798,20 @@ func TestAddGrantScopes(t *testing.T) {
assert, require := assert.New(t), require.New(t)
role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"}))
for _, e := range tc.existing {
_ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e)
_ = iam.TestRoleGrantScope(t, conn, role, e)
}
noAuthCtx := auth.DisabledAuthTestContext(repoFn, tc.scopeId)
readbackRole, err := s.GetRole(noAuthCtx, &pbs.GetRoleRequest{
Id: role.PublicId,
})
require.NoError(err)
req := &pbs.AddRoleGrantScopesRequest{
Id: role.GetPublicId(),
Version: role.GetVersion(),
Version: readbackRole.GetItem().GetVersion(),
}
req.GrantScopeIds = tc.add
got, err := s.AddRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req)
got, err := s.AddRoleGrantScopes(noAuthCtx, req)
if tc.wantErr {
assert.Error(err)
if tc.wantErrContains != "" {
@@ -2928,7 +2941,7 @@ func TestSetGrantScopes(t *testing.T) {
scopeId: scope.Global.String(),
existing: []string{"this"},
set: []string{scope.Global.String(), "children"},
result: []string{scope.Global.String(), "children"},
result: []string{"this", "children"},
},
{
name: "Set other org/proj scopes - global",
@@ -2949,14 +2962,16 @@ func TestSetGrantScopes(t *testing.T) {
scopeId: scope.Global.String(),
existing: []string{},
set: []string{scope.Global.String(), "children", "this"},
wantErr: true,
result: []string{"this", "children"},
wantErr: false,
},
{
name: "Set both grant scope - global",
scopeId: scope.Global.String(),
existing: []string{},
set: []string{"this", scope.Global.String(), "children"},
wantErr: true,
result: []string{"this", "children"},
wantErr: false,
},
{
name: "Set children and descendants - global",
@@ -2989,8 +3004,8 @@ func TestSetGrantScopes(t *testing.T) {
name: "Set duplicate grant on role with grant scopes - org",
scopeId: o.PublicId,
existing: []string{"this"},
set: []string{p.PublicId, "children", "children"},
result: []string{p.PublicId, "children"},
set: []string{"children", "children"},
result: []string{"children"},
},
{
name: "Set grant scope matching existing grant scopes - org",
@@ -3011,7 +3026,7 @@ func TestSetGrantScopes(t *testing.T) {
scopeId: o.PublicId,
existing: []string{"this"},
set: []string{o.PublicId, "children"},
result: []string{o.PublicId, "children"},
result: []string{"this", "children"},
},
{
name: "Set invalid grant scope - org",
@@ -3032,20 +3047,22 @@ func TestSetGrantScopes(t *testing.T) {
scopeId: o.PublicId,
existing: []string{},
set: []string{o.PublicId, "children", "this"},
wantErr: true,
result: []string{"this", "children"},
wantErr: false,
},
{
name: "Set both grant scope - org",
scopeId: o.PublicId,
existing: []string{},
set: []string{"this", o.PublicId, "children"},
wantErr: true,
result: []string{"this", "children"},
wantErr: false,
},
{
name: "Set grant scopes on empty role - proj with id",
scopeId: p.PublicId,
set: []string{p.PublicId},
result: []string{p.PublicId},
result: []string{"this"},
},
{
name: "Set grant scopes on empty role - proj with this",
@@ -3071,7 +3088,7 @@ func TestSetGrantScopes(t *testing.T) {
scopeId: p.PublicId,
existing: []string{"this"},
set: []string{p.PublicId},
result: []string{p.PublicId},
result: []string{"this"},
},
{
name: "Set other org/proj scopes - org",
@@ -3106,14 +3123,16 @@ func TestSetGrantScopes(t *testing.T) {
scopeId: p.PublicId,
existing: []string{},
set: []string{p.PublicId, "this"},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Set both grant scope - proj",
scopeId: p.PublicId,
existing: []string{},
set: []string{"this", p.PublicId},
wantErr: true,
result: []string{"this"},
wantErr: false,
},
{
name: "Set other org/proj scopes - proj",
@@ -3129,14 +3148,18 @@ func TestSetGrantScopes(t *testing.T) {
assert, require := assert.New(t), require.New(t)
role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"}))
for _, e := range tc.existing {
_ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e)
_ = iam.TestRoleGrantScope(t, conn, role, e)
}
req := &pbs.SetRoleGrantScopesRequest{
Id: role.GetPublicId(),
Version: role.GetVersion(),
}
req.GrantScopeIds = tc.set
got, err := s.SetRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req)
noAuthCtx := auth.DisabledAuthTestContext(repoFn, tc.scopeId)
readbackRole, err := s.GetRole(noAuthCtx, &pbs.GetRoleRequest{
Id: role.PublicId,
})
require.NoError(err)
got, err := s.SetRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), &pbs.SetRoleGrantScopesRequest{
Id: role.GetPublicId(),
Version: readbackRole.GetItem().GetVersion(),
GrantScopeIds: tc.set,
})
if tc.wantErr {
assert.Error(err)
if tc.wantErrContains != "" {
@@ -3233,15 +3256,15 @@ func TestRemoveGrantScopes(t *testing.T) {
{
name: "Remove partial - global",
scopeId: scope.Global.String(),
existing: []string{"this", o.PublicId, "descendants", p.PublicId},
remove: []string{o.PublicId, p.PublicId},
result: []string{"this", "descendants"},
existing: []string{"this", "descendants"},
remove: []string{"descendants"},
result: []string{"this"},
},
{
name: "Remove duplicate - global",
scopeId: scope.Global.String(),
existing: []string{"this", "children", o.PublicId},
remove: []string{"children", o.PublicId, "children"},
existing: []string{"this", "children", p.PublicId},
remove: []string{"children", p.PublicId, "children"},
result: []string{"this"},
},
{
@@ -3267,15 +3290,15 @@ func TestRemoveGrantScopes(t *testing.T) {
{
name: "Remove partial - org",
scopeId: o.PublicId,
existing: []string{"children", o.PublicId, p.PublicId},
remove: []string{"children"},
result: []string{o.PublicId, p.PublicId},
existing: []string{"this", p.PublicId},
remove: []string{"this"},
result: []string{p.PublicId},
},
{
name: "Remove duplicate - org",
scopeId: o.PublicId,
existing: []string{"this", p.PublicId, "children"},
remove: []string{"children", p.PublicId, "children"},
existing: []string{"this", "children"},
remove: []string{"children", "children"},
result: []string{"this"},
},
{
@@ -3295,8 +3318,8 @@ func TestRemoveGrantScopes(t *testing.T) {
{
name: "Remove all - proj",
scopeId: p.PublicId,
existing: []string{p.PublicId},
remove: []string{p.PublicId},
existing: []string{"this"},
remove: []string{"this"},
},
{
name: "Remove duplicate - proj",
@@ -3326,14 +3349,20 @@ func TestRemoveGrantScopes(t *testing.T) {
assert, require := assert.New(t), require.New(t)
role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"}))
for _, e := range tc.existing {
_ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e)
_ = iam.TestRoleGrantScope(t, conn, role, e)
}
noAuthCtx := auth.DisabledAuthTestContext(repoFn, tc.scopeId)
// have to read back the role to get the correct role version
readbackRole, err := s.GetRole(noAuthCtx, &pbs.GetRoleRequest{
Id: role.PublicId,
})
require.NoError(err)
req := &pbs.RemoveRoleGrantScopesRequest{
Id: role.GetPublicId(),
Version: role.GetVersion(),
Version: readbackRole.GetItem().GetVersion(),
}
req.GrantScopeIds = tc.remove
got, err := s.RemoveRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req)
got, err := s.RemoveRoleGrantScopes(noAuthCtx, req)
if tc.wantErr {
assert.Error(err)
return
@@ -3345,12 +3374,11 @@ func TestRemoveGrantScopes(t *testing.T) {
})
}
role := iam.TestRole(t, conn, p.GetPublicId(), iam.WithGrantScopeIds([]string{"testing-none"}))
role := iam.TestRole(t, conn, p.GetPublicId(), iam.WithGrantScopeIds([]string{"this"}))
failCases := []struct {
name string
req *pbs.RemoveRoleGrantScopesRequest
err error
err error
}{
{
name: "Bad Version",
@@ -5,32 +5,29 @@ package scopes_test
import (
"context"
"fmt"
"maps"
"slices"
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
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/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/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
"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) {
func TestGrants_ListScopes(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrap := db.TestWrapper(t)
@@ -41,72 +38,756 @@ func TestGrants_ReadActions(t *testing.T) {
}
s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
require.NoError(t, err)
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo)
org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj1 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithName("proj1"), iam.WithDescription("test project 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj2a := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2a"), iam.WithDescription("test project 2a"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj2b := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2b"), iam.WithDescription("test project 2b"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
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,
testcases := []struct {
name string
input *pbs.ListScopesRequest
userFunc func() (*iam.User, auth.Account)
wantOutfields map[string][]string
wantErr error
}{
{
name: "no grants returns no scopes",
input: &pbs.ListScopesRequest{ScopeId: globals.GlobalPrefix},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
wantErr: handlers.ForbiddenError(),
},
{
name: "global role grant this returns all org scopes",
input: &pbs.ListScopesRequest{ScopeId: globals.GlobalPrefix},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list,no-op;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantOutfields: map[string][]string{
org1.PublicId: {globals.IdField},
org2.PublicId: {globals.IdField},
},
{
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,
},
},
{
name: "global role grant this & children returns org scopes",
input: &pbs.ListScopesRequest{
ScopeId: globals.GlobalPrefix,
},
}
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list,read;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantOutfields: map[string][]string{
org1.PublicId: {globals.IdField},
org2.PublicId: {globals.IdField},
},
},
{
name: "global role grant this & children recursive returns org and project scopes",
input: &pbs.ListScopesRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list,read;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
wantOutfields: map[string][]string{
org1.PublicId: {globals.IdField},
org2.PublicId: {globals.IdField},
proj1.PublicId: {globals.IdField},
proj2a.PublicId: {globals.IdField},
proj2b.PublicId: {globals.IdField},
},
},
{
name: "global role grant this & descendants recursive returns org and project scopes",
input: &pbs.ListScopesRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list,read;output_fields=id,name"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
wantOutfields: map[string][]string{
org1.PublicId: {globals.IdField, globals.NameField},
org2.PublicId: {globals.IdField, globals.NameField},
proj1.PublicId: {globals.IdField, globals.NameField},
proj2a.PublicId: {globals.IdField, globals.NameField},
proj2b.PublicId: {globals.IdField, globals.NameField},
},
},
}
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
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
// Call function to create direct/indirect relationship
user, account := tc.userFunc()
// Create auth token for the user
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
// Create auth context from the auth token
fullGrantAuthCtx := controllerauth.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())
// check if the output fields are as expected
if tc.wantOutfields[g.Id] != nil {
handlers.TestAssertOutputFields(t, g, tc.wantOutfields[g.Id])
}
require.NoError(t, finalErr)
var gotIDs []string
for _, g := range got.Items {
gotIDs = append(gotIDs, g.GetId())
}
require.ElementsMatch(t, tc.wantIDs, gotIDs)
})
}
})
}
wantIds := slices.Collect(maps.Keys(tc.wantOutfields))
require.ElementsMatch(t, wantIds, gotIds)
})
}
}
// expectedOutput consolidates common output fields for the test cases
type expectedOutput struct {
err error
outputFields []string
}
func TestGrants_GetScope(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)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
require.NoError(t, err)
org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj1 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithName("proj1"), iam.WithDescription("test project 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj2 := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2"), iam.WithDescription("test project 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
expected map[*pbs.GetScopeRequest]expectedOutput
}{
{
name: "no grants returns no scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "global role grant this returns scopes under the global scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {outputFields: []string{globals.IdField}},
{Id: org1.PublicId}: {outputFields: []string{globals.IdField}},
{Id: org2.PublicId}: {outputFields: []string{globals.IdField}},
{Id: proj1.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "global role grant this and children returns scopes under the global and org scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id,name"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {outputFields: []string{globals.IdField, globals.NameField}},
{Id: org1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField}},
{Id: org2.PublicId}: {outputFields: []string{globals.IdField, globals.NameField}},
{Id: proj1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField}},
{Id: proj2.PublicId}: {outputFields: []string{globals.IdField, globals.NameField}},
},
},
{
name: "global role grant children returns scopes under org scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField}},
{Id: proj2.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField}},
},
},
{
name: "global role grant descendants returns scopes under org scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField}},
{Id: proj2.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField}},
},
},
{
name: "org1 role grant this returns scopes under the org1 scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description,version"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField}},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "org1 role grant children returns scopes under project scopes (nothing)",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=scope;actions=read"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "org1 role grant this and children returns scopes under the org1 scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description,version,type"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField, globals.TypeField}},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "proj1 role grant this returns scopes under proj1 scope (nothing)",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.PublicId,
Grants: []string{"ids=*;type=scope;actions=read"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "global role grant this scope pinned to org1 scope returns org1 scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{fmt.Sprintf("ids=%s;types=scope;actions=read;output_fields=id,name,scope_id,description,version,type,authorized_actions", org1.PublicId)},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField}},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
{
name: "global role grant individual scopes (org1) returns org1 scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description,version,type,authorized_actions,created_time,updated_time"},
GrantScopes: []string{org1.PublicId},
},
}),
expected: map[*pbs.GetScopeRequest]expectedOutput{
{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
{Id: org1.PublicId}: {err: handlers.ForbiddenError()},
{Id: org2.PublicId}: {err: handlers.ForbiddenError()},
{Id: proj1.PublicId}: {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, globals.CreatedTimeField, globals.UpdatedTimeField}},
{Id: proj2.PublicId}: {err: handlers.ForbiddenError()},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for input, expectedOutput := range tc.expected {
got, err := s.GetScope(fullGrantAuthCtx, input)
// not found means expect error
if expectedOutput.err != nil {
require.ErrorIs(t, err, expectedOutput.err)
continue
}
// check if the output fields are as expected
if expectedOutput.outputFields != nil {
handlers.TestAssertOutputFields(t, got.Item, expectedOutput.outputFields)
}
require.NoError(t, err)
}
})
}
}
func TestGrants_CreateScopes(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)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
require.NoError(t, err)
org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
canCreateInScopes map[*pbs.CreateScopeRequest]error
}{
{
name: "no grants returns no creatable scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
canCreateInScopes: map[*pbs.CreateScopeRequest]error{
{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
{Item: &pb.Scope{ScopeId: org1.PublicId}}: handlers.ForbiddenError(),
{Item: &pb.Scope{ScopeId: org2.PublicId}}: handlers.ForbiddenError(),
},
},
{
name: "global role grant this can create scopes in the global scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=create"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canCreateInScopes: map[*pbs.CreateScopeRequest]error{
{Item: &pb.Scope{ScopeId: scope.Global.String()}}: nil,
{Item: &pb.Scope{ScopeId: org1.PublicId}}: handlers.ForbiddenError(),
{Item: &pb.Scope{ScopeId: org2.PublicId}}: handlers.ForbiddenError(),
},
},
{
name: "global role grant children can create scopes in org scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=create"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
canCreateInScopes: map[*pbs.CreateScopeRequest]error{
{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
{Item: &pb.Scope{ScopeId: org1.PublicId}}: nil,
{Item: &pb.Scope{ScopeId: org2.PublicId}}: nil,
},
},
{
name: "global role grant descendants can create scopes in org scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=create"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
canCreateInScopes: map[*pbs.CreateScopeRequest]error{
{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
{Item: &pb.Scope{ScopeId: org1.PublicId}}: nil,
{Item: &pb.Scope{ScopeId: org2.PublicId}}: nil,
},
},
{
name: "global role individual grant to org1 can create scopes in org1 scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=create"},
GrantScopes: []string{org1.PublicId},
},
}),
canCreateInScopes: map[*pbs.CreateScopeRequest]error{
{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
{Item: &pb.Scope{ScopeId: org1.PublicId}}: nil,
{Item: &pb.Scope{ScopeId: org2.PublicId}}: handlers.ForbiddenError(),
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for req, wantErr := range tc.canCreateInScopes {
_, err := s.CreateScope(fullGrantAuthCtx, req)
if wantErr != nil {
require.ErrorIs(t, err, wantErr)
continue
}
require.NoError(t, err)
}
})
}
}
func TestGrants_ListKeyVersionDestructionJobs(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)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
require.NoError(t, err)
org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj1 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithName("proj1"), iam.WithDescription("test project 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj2 := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2"), iam.WithDescription("test project 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
canListInScopes map[string]error
}{
{
name: "no grants cannot list key version destruction jobs",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: handlers.ForbiddenError(),
org2.PublicId: handlers.ForbiddenError(),
proj1.PublicId: handlers.ForbiddenError(),
proj2.PublicId: handlers.ForbiddenError(),
},
},
{
name: "global role grant this can list key version destruction jobs in the global scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): nil,
org1.PublicId: handlers.ForbiddenError(),
org2.PublicId: handlers.ForbiddenError(),
proj1.PublicId: handlers.ForbiddenError(),
proj2.PublicId: handlers.ForbiddenError(),
},
},
{
name: "global role grant children can list key version destruction jobs in org scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: nil,
org2.PublicId: nil,
proj1.PublicId: handlers.ForbiddenError(),
proj2.PublicId: handlers.ForbiddenError(),
},
},
{
name: "org1 role grant this can list key version destruction jobs in the org1 scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: nil,
org2.PublicId: handlers.ForbiddenError(),
proj1.PublicId: handlers.ForbiddenError(),
proj2.PublicId: handlers.ForbiddenError(),
},
},
{
name: "org1 role grant children can list key version destruction jobs in its project scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: handlers.ForbiddenError(),
org2.PublicId: handlers.ForbiddenError(),
proj1.PublicId: nil,
proj2.PublicId: handlers.ForbiddenError(),
},
},
{
name: "global role grant descendants can list key version destruction jobs in its project scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: nil,
org2.PublicId: nil,
proj1.PublicId: nil,
proj2.PublicId: nil,
},
},
{
name: "global role grant org1 scope can list key version destruction jobs in the org1 scope",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{org1.PublicId},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: nil,
org2.PublicId: handlers.ForbiddenError(),
proj1.PublicId: handlers.ForbiddenError(),
proj2.PublicId: handlers.ForbiddenError(),
},
},
{
name: "global role grant to proj1 and proj2 role grant this can list key version destruction in granted projects",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
password.TestAuthMethodWithAccount,
[]iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{proj1.PublicId},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
canListInScopes: map[string]error{
scope.Global.String(): handlers.ForbiddenError(),
org1.PublicId: handlers.ForbiddenError(),
org2.PublicId: handlers.ForbiddenError(),
proj1.PublicId: nil,
proj2.PublicId: nil,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for scopeId, wantErr := range tc.canListInScopes {
_, err = s.ListKeyVersionDestructionJobs(fullGrantAuthCtx, &pbs.ListKeyVersionDestructionJobsRequest{
ScopeId: scopeId,
})
if wantErr != nil {
require.ErrorIs(t, err, wantErr)
continue
}
require.NoError(t, err)
}
})
}
}
func TestGrants_List_AuthorizedAction(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)
_, _ = iam.TestScopes(t, iamRepo)
_, _ = iam.TestScopes(t, iamRepo)
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
require.NoError(t, err)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
user, account := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=*;actions=*"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
})()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, err := s.ListScopes(fullGrantAuthCtx, &pbs.ListScopesRequest{ScopeId: globals.GlobalPrefix, Recursive: true})
require.NoError(t, err)
for _, item := range got.Items {
switch item.GetType() {
case "global":
require.ElementsMatch(t, item.AuthorizedActions, []string{"no-op", "read", "update", "delete", "attach-storage-policy", "detach-storage-policy"})
require.Len(t, item.AuthorizedCollectionActions, 11)
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Alias.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthMethod.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.StorageBucket.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthToken.PluralString()].AsSlice(), []string{"list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Group.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Role.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Scope.PluralString()].AsSlice(), []string{"create", "list", "list-keys", "rotate-keys", "list-key-version-destruction-jobs", "destroy-key-version"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.User.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Worker.PluralString()].AsSlice(), []string{"read-certificate-authority", "create-controller-led", "create-worker-led", "reinitialize-certificate-authority", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.SessionRecording.PluralString()].AsSlice(), []string{"list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Policy.PluralString()].AsSlice(), []string{"create", "list"})
case "org":
require.ElementsMatch(t, item.AuthorizedActions, []string{"no-op", "read", "update", "delete", "attach-storage-policy", "detach-storage-policy"})
require.Len(t, item.AuthorizedCollectionActions, 9)
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthMethod.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.StorageBucket.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthToken.PluralString()].AsSlice(), []string{"list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Group.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Role.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Scope.PluralString()].AsSlice(), []string{"create", "list", "list-keys", "rotate-keys", "list-key-version-destruction-jobs", "destroy-key-version"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.User.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.SessionRecording.PluralString()].AsSlice(), []string{"list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Policy.PluralString()].AsSlice(), []string{"create", "list"})
case "project":
require.ElementsMatch(t, item.AuthorizedActions, []string{"no-op", "read", "update", "delete"})
require.Len(t, item.AuthorizedCollectionActions, 7)
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.CredentialStore.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Group.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.HostCatalog.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Role.PluralString()].AsSlice(), []string{"create", "list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Scope.PluralString()].AsSlice(), []string{"list-keys", "rotate-keys", "list-key-version-destruction-jobs", "destroy-key-version"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Session.PluralString()].AsSlice(), []string{"list"})
require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Target.PluralString()].AsSlice(), []string{"create", "list"})
default:
t.Fatalf("unknown item type: %s", item.GetType())
}
}
}
@@ -117,6 +117,23 @@ var (
resource.Target: targets.CollectionActions,
},
}
additionalResourceGrants = []resource.Type{
resource.Alias,
resource.AuthMethod,
resource.AuthToken,
resource.StorageBucket,
resource.Group,
resource.Role,
resource.Scope,
resource.User,
resource.SessionRecording,
resource.Policy,
resource.CredentialStore,
resource.HostCatalog,
resource.Worker,
resource.Session,
resource.Target,
}
)
func init() {
@@ -168,7 +185,10 @@ func (s *Service) ListScopes(ctx context.Context, req *pbs.ListScopesRequest) (*
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
// Hard-coding 'isRecursive' to true because list scope returns child scopes which requires
// additional grants for those child-scopes (recursive) to calculate authorized_actions on the returned scope
authResults := s.authResult(ctx, req.GetScopeId(), action.List, true)
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -314,7 +334,7 @@ func (s *Service) GetScope(ctx context.Context, req *pbs.GetScopeRequest) (*pbs.
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -366,7 +386,7 @@ func (s *Service) CreateScope(ctx context.Context, req *pbs.CreateScopeRequest)
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -418,7 +438,7 @@ func (s *Service) UpdateScope(ctx context.Context, req *pbs.UpdateScopeRequest)
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -468,7 +488,7 @@ func (s *Service) DeleteScope(ctx context.Context, req *pbs.DeleteScopeRequest)
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -487,7 +507,7 @@ func (s *Service) ListKeys(ctx context.Context, req *pbs.ListKeysRequest) (*pbs.
if err := validateListKeysRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.ListScopeKeys)
authResults := s.authResult(ctx, req.GetId(), action.ListScopeKeys, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -533,7 +553,7 @@ func (s *Service) RotateKeys(ctx context.Context, req *pbs.RotateKeysRequest) (*
if err := validateRotateKeysRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.RotateScopeKeys)
authResults := s.authResult(ctx, req.GetScopeId(), action.RotateScopeKeys, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -553,7 +573,7 @@ func (s *Service) ListKeyVersionDestructionJobs(ctx context.Context, req *pbs.Li
if err := validateListKeyVersionDestructionJobsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.ListScopeKeyVersionDestructionJobs)
authResults := s.authResult(ctx, req.GetScopeId(), action.ListScopeKeyVersionDestructionJobs, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -595,7 +615,7 @@ func (s *Service) DestroyKeyVersion(ctx context.Context, req *pbs.DestroyKeyVers
if err := validateDestroyKeyVersionRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.DestroyScopeKeyVersion)
authResults := s.authResult(ctx, req.GetScopeId(), action.DestroyScopeKeyVersion, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -767,7 +787,7 @@ func sortKeys(keys []*pb.Key) {
}
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
repo, err := s.repoFn()
if err != nil {
@@ -776,7 +796,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Scope), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive), auth.WithFetchAdditionalResourceGrants(additionalResourceGrants...)}
switch a {
case action.List, action.Create, action.ListScopeKeys, action.ListScopeKeyVersionDestructionJobs, action.DestroyScopeKeyVersion:
parentId = id
@@ -803,7 +823,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Scope, opts...)
}
func ToProto(ctx context.Context, in *iam.Scope, opt ...handlers.Option) (*pb.Scope, error) {
@@ -0,0 +1,653 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package sessions_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/ldap"
"github.com/hashicorp/boundary/internal/authtoken"
cred "github.com/hashicorp/boundary/internal/credential"
credstatic "github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/vault"
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/sessions"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/session"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/tcp"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
)
// testSession is a wrapper around "session" to help with setting up additional resources and mutating the session states
// to fill in
func testSession(t *testing.T,
conn *db.DB,
kmsCache *kms.Kms,
wrapper wrapping.Wrapper,
targetRepo *target.Repository,
sessRepo *session.Repository,
projectId string, isActive bool,
) *session.Session {
cats := static.TestCatalogs(t, conn, projectId, 1)
hosts := static.TestHosts(t, conn, cats[0].PublicId, 1)
sets := static.TestSets(t, conn, cats[0].PublicId, 1)
_ = static.TestSetMembers(t, conn, sets[0].PublicId, hosts)
randomString, err := uuid.GenerateUUID()
require.NoError(t, err)
vaultStore := vault.TestCredentialStore(t, conn, wrapper, projectId, fmt.Sprintf("http://vault%s", randomString), fmt.Sprintf("vault-token-%s", randomString), fmt.Sprintf("accessor-%s", randomString))
libIds := vault.TestCredentialLibraries(t, conn, wrapper, vaultStore.GetPublicId(), 2)
tcpTarget := tcp.TestTarget(context.Background(), t, conn, projectId, randomString, target.WithHostSources([]string{sets[0].GetPublicId()}))
staticStore := credstatic.TestCredentialStore(t, conn, wrapper, projectId)
upCreds := credstatic.TestUsernamePasswordCredentials(t, conn, wrapper, randomString, randomString, staticStore.GetPublicId(), projectId, 2)
ids := target.CredentialSources{
BrokeredCredentialIds: []string{libIds[0].GetPublicId(), libIds[1].GetPublicId(), upCreds[0].GetPublicId(), upCreds[1].GetPublicId()},
}
_, err = targetRepo.AddTargetCredentialSources(context.Background(), tcpTarget.GetPublicId(), tcpTarget.GetVersion(), ids)
require.NoError(t, err)
dynamicCreds := []*session.DynamicCredential{
session.NewDynamicCredential(libIds[0].GetPublicId(), cred.BrokeredPurpose),
session.NewDynamicCredential(libIds[1].GetPublicId(), cred.BrokeredPurpose),
}
staticCreds := []*session.StaticCredential{
session.NewStaticCredential(upCreds[0].GetPublicId(), cred.BrokeredPurpose),
session.NewStaticCredential(upCreds[1].GetPublicId(), cred.BrokeredPurpose),
}
at := authtoken.TestAuthToken(t, conn, kmsCache, globals.GlobalPrefix)
uId := at.GetIamUserId()
worker := server.TestPkiWorker(t, conn, wrapper,
server.WithNewIdFunc(func(ctx context.Context) (string, error) {
return server.NewWorkerIdFromScopeAndName(ctx, globals.GlobalPrefix, randomString)
}),
)
sess := session.TestSession(t, conn, wrapper, session.ComposedOf{
UserId: uId,
HostId: hosts[0].GetPublicId(),
TargetId: tcpTarget.GetPublicId(),
HostSetId: sets[0].GetPublicId(),
AuthTokenId: at.GetPublicId(),
ProjectId: projectId,
Endpoint: "tcp://127.0.0.1:22",
ExpirationTime: timestamp.New(time.Now()),
ConnectionLimit: 10,
WorkerFilter: "worker",
EgressWorkerFilter: "egress",
IngressWorkerFilter: "ingress",
DynamicCredentials: dynamicCreds,
StaticCredentials: staticCreds,
ProtocolWorkerId: worker.PublicId,
CorrelationId: randomString,
})
switch isActive {
case true:
session.TestConnection(t, conn, sess.PublicId, "127.0.0.1", 22, "127.0.0.2", 23, "127.0.0.1")
default:
_, err := sessRepo.CancelSession(t.Context(), sess.PublicId, sess.Version)
require.NoError(t, err)
terminated, err := sessRepo.TerminateCompletedSessions(t.Context())
require.NoError(t, err)
require.Equal(t, 1, terminated)
}
return sess
}
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)
targetRepo, err := target.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
require.NoError(t, err)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
sessionRepoFn := func(option ...session.Option) (*session.Repository, error) {
return session.NewRepository(ctx, rw, rw, kmsCache, option...)
}
iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil }
s, err := sessions.NewService(ctx, sessionRepoFn, iamRepoFn, 100)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
org2, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
proj3 := iam.TestProject(t, iamRepo, org2.PublicId, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
sessionRepo, err := sessionRepoFn()
require.NoError(t, err)
proj1Session := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj1.PublicId, true)
proj2Session := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj2.PublicId, true)
proj3Session := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj3.PublicId, false)
type result struct {
wantErr error
outputFields []string
}
t.Run("Get", func(t *testing.T) {
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
inputResultMap map[*pbs.GetSessionRequest]result
}{
{
name: "global role descendants grants can read all sessions",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=*;output_fields=id,target_id,scope,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {outputFields: []string{globals.IdField, globals.TargetIdField, globals.ScopeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
{Id: proj2Session.PublicId}: {outputFields: []string{globals.IdField, globals.TargetIdField, globals.ScopeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
{Id: proj3Session.PublicId}: {outputFields: []string{globals.IdField, globals.TargetIdField, globals.ScopeField, globals.CreatedTimeField, globals.UpdatedTimeField}},
},
},
{
name: "org role individual project grants",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"id=*;type=session;actions=read;output_fields=expiration_time,auth_token_id,user_id,host_set_id,host_ids"},
GrantScopes: []string{proj1.PublicId},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {outputFields: []string{globals.ExpirationTimeField, globals.AuthTokenIdField, globals.UserIdField, globals.HostSetIdField, globals.HostIdsField}},
{Id: proj2Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj3Session.PublicId}: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "org role children grants",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"id=*;type=session;actions=read;output_fields=version,type,scope_id,endpoint,states,status,certificate,authorized_actions,connections,termination_reason"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj2Session.PublicId}: {outputFields: []string{globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField, globals.ConnectionsField}},
{Id: proj3Session.PublicId}: {outputFields: []string{globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField, globals.TerminationReasonField}},
},
},
{
name: "proj role this grants",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.PublicId,
Grants: []string{"id=*;type=session;actions=read;output_fields=version,type,scope_id,endpoint,states,status,certificate,termination_reason,authorized_actions,connections"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {outputFields: []string{globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField, globals.ConnectionsField}},
{Id: proj2Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj3Session.PublicId}: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "multiple project roles",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.PublicId,
Grants: []string{"id=*;type=session;actions=read;output_fields=authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"id=*;type=session;actions=read;output_fields=version"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: proj3.PublicId,
Grants: []string{"id=*;type=session;actions=read;output_fields=id"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {outputFields: []string{globals.AuthorizedActionsField}},
{Id: proj2Session.PublicId}: {outputFields: []string{globals.VersionField}},
{Id: proj3Session.PublicId}: {outputFields: []string{globals.IdField}},
},
},
{
name: "role with incorrect resource grant",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=worker;actions=*"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj2Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj3Session.PublicId}: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "role with incorrect action grant",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=create,update,delete"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
inputResultMap: map[*pbs.GetSessionRequest]result{
{Id: proj1Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj2Session.PublicId}: {wantErr: handlers.ForbiddenError()},
{Id: proj3Session.PublicId}: {wantErr: handlers.ForbiddenError()},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
for input, expect := range tc.inputResultMap {
got, err := s.GetSession(fullGrantAuthCtx, input)
if expect.wantErr != nil {
require.ErrorIs(t, expect.wantErr, err)
continue
}
require.NoError(t, err)
handlers.TestAssertOutputFields(t, got.Item, expect.outputFields)
}
})
}
})
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListSessionsRequest
userFunc func() (*iam.User, auth.Account)
idOutputFieldsMap map[string][]string
wantErr error
}{
{
name: "global list global role descendants grants can read all sessions",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=*;output_fields=id,target_id,scope,created_time,updated_time"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
idOutputFieldsMap: map[string][]string{
proj1Session.PublicId: {globals.IdField, globals.TargetIdField, globals.ScopeField, globals.CreatedTimeField, globals.UpdatedTimeField},
proj2Session.PublicId: {globals.IdField, globals.TargetIdField, globals.ScopeField, globals.CreatedTimeField, globals.UpdatedTimeField},
proj3Session.PublicId: {globals.IdField, globals.TargetIdField, globals.ScopeField, globals.CreatedTimeField, globals.UpdatedTimeField},
},
},
{
name: "global list org role individual project grants",
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"id=*;type=session;actions=list,read;output_fields=id,expiration_time,auth_token_id,user_id,host_set_id,host_ids"},
GrantScopes: []string{proj1.PublicId},
},
}),
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
idOutputFieldsMap: map[string][]string{
proj1Session.PublicId: {globals.IdField, globals.ExpirationTimeField, globals.AuthTokenIdField, globals.UserIdField, globals.HostSetIdField, globals.HostIdsField},
},
},
{
name: "global list org role children grants",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"id=*;type=session;actions=list,read;output_fields=id,version,type,scope_id,endpoint,states,status,certificate,authorized_actions,termination_reason"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
idOutputFieldsMap: map[string][]string{
proj2Session.PublicId: {globals.IdField, globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField},
proj3Session.PublicId: {globals.IdField, globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField, globals.TerminationReasonField},
},
},
{
name: "proj role this grants",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.PublicId,
Grants: []string{"id=*;type=session;actions=list,read;output_fields=id,version,type,scope_id,endpoint,states,status,certificate,termination_reason,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
idOutputFieldsMap: map[string][]string{
proj1Session.PublicId: {globals.IdField, globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField},
},
},
{
name: "multiple project roles",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.PublicId,
Grants: []string{"id=*;type=session;actions=list,no-op;output_fields=id,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"id=*;type=session;actions=list,no-op;output_fields=id,version"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: proj3.PublicId,
Grants: []string{"id=*;type=session;actions=list,no-op;output_fields=id,state"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
idOutputFieldsMap: map[string][]string{
proj1Session.PublicId: {globals.IdField, globals.AuthorizedActionsField},
proj2Session.PublicId: {globals.IdField, globals.VersionField},
proj3Session.PublicId: {globals.IdField, globals.StateField},
},
},
{
name: "role with incorrect resource grant",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=worker;actions=*"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "role with list permission in global but not projects",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=*"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
idOutputFieldsMap: map[string][]string{},
},
{
name: "non-recursive list on global scope without grant",
input: &pbs.ListSessionsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
IncludeTerminated: true,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=create,update,delete"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
}),
wantErr: handlers.ForbiddenError(),
},
{
name: "non-recursive list children grant",
input: &pbs.ListSessionsRequest{
ScopeId: proj2.PublicId,
},
userFunc: iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org2.PublicId,
Grants: []string{"id=*;type=session;actions=list,no-op;output_fields=id,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
}),
idOutputFieldsMap: map[string][]string{
proj2Session.PublicId: {globals.IdField, globals.AuthorizedActionsField},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, err := s.ListSessions(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
require.ErrorIs(t, tc.wantErr, err)
return
}
require.NoError(t, err)
require.Len(t, got.Items, len(tc.idOutputFieldsMap))
for _, item := range got.Items {
wantOutputFields, ok := tc.idOutputFieldsMap[item.GetId()]
require.True(t, ok)
handlers.TestAssertOutputFields(t, item, wantOutputFields)
}
})
}
})
}
func TestGrants_Cancel(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)
targetRepo, err := target.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
require.NoError(t, err)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
sessionRepoFn := func(option ...session.Option) (*session.Repository, error) {
return session.NewRepository(ctx, rw, rw, kmsCache, option...)
}
iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil }
s, err := sessions.NewService(ctx, sessionRepoFn, iamRepoFn, 100)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
org2, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
proj3 := iam.TestProject(t, iamRepo, org2.PublicId, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
sessionRepo, err := sessionRepoFn()
require.NoError(t, err)
testcases := []struct {
name string
setup func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account))
wantOutputField []string
wantErr error
}{
{
name: "global role full grants can cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=cancel;output_fields=id,target_id,authorized_actions"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
})
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj2.PublicId, true)
return sess, userFn
},
wantOutputField: []string{globals.IdField, globals.TargetIdField, globals.AuthorizedActionsField},
wantErr: nil,
},
{
name: "org role children grants can cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.PublicId,
Grants: []string{"id=*;type=session;actions=cancel;output_fields=id,version,type,scope_id,endpoint,states"},
GrantScopes: []string{globals.GrantScopeChildren},
},
})
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj1.PublicId, true)
return sess, userFn
},
wantOutputField: []string{globals.IdField, globals.VersionField, globals.TypeField, globals.ScopeIdField, globals.EndpointField, globals.StatesField},
wantErr: nil,
},
{
name: "proj role children grants can cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj3.PublicId,
Grants: []string{"id=*;type=session;actions=cancel;output_fields=id,status,certificate,authorized_actions"},
GrantScopes: []string{globals.GrantScopeThis},
},
})
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj3.PublicId, true)
return sess, userFn
},
wantOutputField: []string{globals.IdField, globals.StatusField, globals.CertificateField, globals.AuthorizedActionsField},
wantErr: nil,
},
{
name: "global role specific scope grants can cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=cancel;output_fields=id,authorized_actions"},
GrantScopes: []string{proj2.PublicId},
},
})
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj2.PublicId, true)
return sess, userFn
},
wantOutputField: []string{globals.IdField, globals.AuthorizedActionsField},
wantErr: nil,
},
{
name: "global specific scope specific id grants can cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj2.PublicId, true)
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{fmt.Sprintf("id=%s;type=session;actions=cancel;output_fields=id,authorized_actions", sess.PublicId)},
GrantScopes: []string{proj2.PublicId},
},
})
return sess, userFn
},
wantOutputField: []string{globals.IdField, globals.AuthorizedActionsField},
wantErr: nil,
},
{
name: "global role children scope grants cannot cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=cancel;output_fields=id,authorized_actions"},
GrantScopes: []string{globals.GrantScopeChildren},
},
})
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj2.PublicId, true)
return sess, userFn
},
wantErr: handlers.ForbiddenError(),
},
{
name: "global role children scope grants cannot cancel",
setup: func(t *testing.T) (*session.Session, func() (*iam.User, auth.Account)) {
userFn := iam.TestUserManagedGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, ldap.TestAuthMethodWithAccountInManagedGroup, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"id=*;type=session;actions=cancel;output_fields=id,authorized_actions"},
GrantScopes: []string{globals.GrantScopeChildren},
},
})
sess := testSession(t, conn, kmsCache, wrap, targetRepo, sessionRepo, proj2.PublicId, true)
return sess, userFn
},
wantErr: handlers.ForbiddenError(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
sess, userFn := tc.setup(t)
user, account := userFn()
mySession, _, err := sessionRepo.LookupSession(ctx, sess.PublicId)
require.NoError(t, err)
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, err := s.CancelSession(fullGrantAuthCtx, &pbs.CancelSessionRequest{
Id: mySession.GetPublicId(),
Version: mySession.Version,
})
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return
}
require.NoError(t, err)
handlers.TestAssertOutputFields(t, got.GetItem(), tc.wantOutputField)
})
}
}
@@ -84,7 +84,7 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.ReadSelf, false)
authResults := s.authResult(ctx, req.GetId(), action.ReadSelf, false, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -102,9 +102,10 @@ func (s Service) GetSession(ctx context.Context, req *pbs.GetSessionRequest) (*p
return nil, handlers.ForbiddenError()
}
outputFields = authResults.FetchOutputFields(perms.Resource{
Id: ses.GetPublicId(),
ScopeId: ses.ProjectId,
Type: resource.Session,
Id: ses.GetPublicId(),
ScopeId: ses.ProjectId,
Type: resource.Session,
ParentScopeId: authResults.Scope.ParentScopeId,
}, action.Read).SelfOrDefaults(authResults.UserId)
} else {
var ok bool
@@ -139,7 +140,7 @@ func (s Service) ListSessions(ctx context.Context, req *pbs.ListSessionsRequest)
return nil, errors.Wrap(ctx, err, op)
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List, false)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, false, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -290,7 +291,7 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques
return nil, err
}
// Ignore decryption failures to ensure the user can always cancel a session.
authResults := s.authResult(ctx, req.GetId(), action.CancelSelf, true)
authResults := s.authResult(ctx, req.GetId(), action.CancelSelf, true, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -322,9 +323,10 @@ func (s Service) CancelSession(ctx context.Context, req *pbs.CancelSessionReques
return nil, handlers.ForbiddenError()
}
outputFields = authResults.FetchOutputFields(perms.Resource{
Id: ses.GetPublicId(),
ScopeId: ses.ProjectId,
Type: resource.Session,
Id: ses.GetPublicId(),
ScopeId: ses.ProjectId,
Type: resource.Session,
ParentScopeId: authResults.Scope.ParentScopeId,
}, action.Cancel).SelfOrDefaults(authResults.UserId)
} else {
var ok bool
@@ -384,11 +386,11 @@ func (s Service) getFromRepo(ctx context.Context, id string) (*session.Session,
return sess, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type, ignoreSessionDecryptionFailure bool) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, ignoreSessionDecryptionFailure, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
var parentId string
opts := []auth.Option{auth.WithType(resource.Session), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List:
parentId = id
@@ -428,7 +430,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type, ignor
return res
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Session, opts...)
}
func toProto(ctx context.Context, in *session.Session, opt ...handlers.Option) (*pb.Session, error) {
@@ -187,7 +187,7 @@ func (s Service) ListTargets(ctx context.Context, req *pbs.ListTargetsRequest) (
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -340,7 +340,7 @@ func (s Service) GetTarget(ctx context.Context, req *pbs.GetTargetRequest) (*pbs
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -380,7 +380,7 @@ func (s Service) CreateTarget(ctx context.Context, req *pbs.CreateTargetRequest)
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -427,7 +427,7 @@ func (s Service) UpdateTarget(ctx context.Context, req *pbs.UpdateTargetRequest)
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -467,7 +467,7 @@ func (s Service) DeleteTarget(ctx context.Context, req *pbs.DeleteTargetRequest)
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -485,7 +485,7 @@ func (s Service) AddTargetHostSources(ctx context.Context, req *pbs.AddTargetHos
if err := validateAddHostSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddHostSources)
authResults := s.authResult(ctx, req.GetId(), action.AddHostSources, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -527,7 +527,7 @@ func (s Service) SetTargetHostSources(ctx context.Context, req *pbs.SetTargetHos
if err := validateSetHostSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetHostSources)
authResults := s.authResult(ctx, req.GetId(), action.SetHostSources, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -567,7 +567,7 @@ func (s Service) RemoveTargetHostSources(ctx context.Context, req *pbs.RemoveTar
if err := validateRemoveHostSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveHostSources)
authResults := s.authResult(ctx, req.GetId(), action.RemoveHostSources, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -607,7 +607,7 @@ func (s Service) AddTargetCredentialSources(ctx context.Context, req *pbs.AddTar
if err := validateAddCredentialSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddCredentialSources)
authResults := s.authResult(ctx, req.GetId(), action.AddCredentialSources, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -648,7 +648,7 @@ func (s Service) SetTargetCredentialSources(ctx context.Context, req *pbs.SetTar
if err := validateSetCredentialSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetCredentialSources)
authResults := s.authResult(ctx, req.GetId(), action.SetCredentialSources, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -689,7 +689,7 @@ func (s Service) RemoveTargetCredentialSources(ctx context.Context, req *pbs.Rem
if err := validateRemoveCredentialSourcesRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveCredentialSources)
authResults := s.authResult(ctx, req.GetId(), action.RemoveCredentialSources, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -752,7 +752,7 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession
if err := validateAuthorizeSessionRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AuthorizeSession,
authResults := s.authResult(ctx, req.GetId(), action.AuthorizeSession, false,
target.WithName(req.GetName()),
target.WithProjectId(req.GetScopeId()),
target.WithProjectName(req.GetScopeName()),
@@ -1555,7 +1555,7 @@ func (s Service) removeCredentialSourcesInRepo(ctx context.Context, targetId str
func (s Service) aliasCreateAuthResult(ctx context.Context, parentId string) auth.VerifyResults {
res := auth.VerifyResults{}
a := action.Create
opts := []auth.Option{auth.WithType(resource.Alias), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a)}
iamRepo, err := s.iamRepoFn()
if err != nil {
res.Error = err
@@ -1571,16 +1571,16 @@ func (s Service) aliasCreateAuthResult(ctx context.Context, parentId string) aut
return res
}
opts = append(opts, auth.WithScopeId(parentId))
ret := auth.Verify(ctx, opts...)
ret := auth.Verify(ctx, resource.Alias, opts...)
return ret
}
func (s Service) authResult(ctx context.Context, id string, a action.Type, lookupOpt ...target.Option) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool, lookupOpt ...target.Option) auth.VerifyResults {
res := auth.VerifyResults{}
var parentId string
var t target.Target
opts := []auth.Option{auth.WithType(resource.Target), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -1623,7 +1623,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type, looku
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
ret := auth.Verify(ctx, opts...)
ret := auth.Verify(ctx, resource.Target, opts...)
ret.RoundTripValue = t
return ret
}
@@ -5,103 +5,595 @@ package tcp_test
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
credstatic "github.com/hashicorp/boundary/internal/credential/static"
"github.com/hashicorp/boundary/internal/credential/vault"
controllerauth "github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"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"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/targets"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// 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)
kmsCache := kms.TestKms(t, conn, wrapper)
iamRepo := iam.TestRepo(t, conn, wrapper)
s, err := testService(t, ctx, conn, kms, wrapper)
s, err := testService(t, ctx, conn, kmsCache, wrapper)
require.NoError(t, err)
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo)
_, proj2 := iam.TestScopes(t, iamRepo)
org1, proj1 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
_, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
noTargetOrg := iam.TestOrg(t, iamRepo, iam.WithSkipDefaultRoleCreation(true), iam.WithSkipAdminRoleCreation(true))
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 string
input *pbs.ListTargetsRequest
userFunc func() (*iam.User, auth.Account)
wantErr error
wantIdOutputFields map[string][]string
}{
{
name: "global role grant descendants returns all created targets",
input: &pbs.ListTargetsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,authorized_actions,scope_id,address"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantErr: nil,
wantIdOutputFields: map[string][]string{
target1.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
target2.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
target3.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
},
},
{
name: "global role grant descendants list at org return all org targets",
input: &pbs.ListTargetsRequest{
ScopeId: org1.PublicId,
Recursive: true,
},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,authorized_actions,scope_id,address"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantErr: nil,
wantIdOutputFields: map[string][]string{
target1.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
target2.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
},
},
{
name: "org role grant this returns all created targets",
input: &pbs.ListTargetsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org1.GetPublicId(),
GrantStrings: []string{"ids=*;type=target;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org1.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,authorized_actions,scope_id,address"},
GrantScopes: []string{globals.GrantScopeChildren},
},
},
}),
wantErr: nil,
wantIDs: []string{target1.GetPublicId(), target2.GetPublicId()},
wantIdOutputFields: map[string][]string{
target1.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
target2.GetPublicId(): {globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
},
},
{
name: "project role grant this returns all created targets",
input: &pbs.ListTargetsRequest{
ScopeId: proj2.GetPublicId(),
ScopeId: proj2.GetPublicId(),
},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj2.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantErr: nil,
wantIdOutputFields: map[string][]string{
target3.GetPublicId(): {globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField},
},
},
{
name: "recursive list return no result but no error",
input: &pbs.ListTargetsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: proj2.GetPublicId(),
GrantStrings: []string{"ids=*;type=target;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: noTargetOrg.PublicId,
GrantScopes: []string{globals.GrantScopeThis},
Grants: []string{"ids=*;type=target;actions=list"},
},
}),
wantErr: nil,
wantIdOutputFields: map[string][]string{},
},
{
name: "no grant recursive list return error",
input: &pbs.ListTargetsRequest{
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
wantErr: nil,
wantIDs: []string{target3.GetPublicId()},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
wantErr: handlers.ForbiddenError(),
},
{
name: "no grant non-recursive list return error",
input: &pbs.ListTargetsRequest{
ScopeId: proj2.GetPublicId(),
},
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
wantErr: handlers.ForbiddenError(),
},
}
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)
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.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.Len(t, got.Items, len(tc.wantIdOutputFields))
for _, item := range got.Items {
wantOutputFields, ok := tc.wantIdOutputFields[item.Id]
require.True(t, ok)
handlers.TestAssertOutputFields(t, item, wantOutputFields)
}
})
}
})
type readTestResult struct {
wantErr error
wantOutputFields []string
}
t.Run("Read", func(t *testing.T) {
testcases := []struct {
name string
userFunc func() (*iam.User, auth.Account)
wantIdOutputFields map[string]readTestResult
}{
{
name: "global role grant descendants returns all created targets",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,authorized_actions,scope_id,address"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
wantIdOutputFields: map[string]readTestResult{
target1.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
target2.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
target3.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
},
},
{
name: "org role grant children returns all its created targets",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,authorized_actions,scope_id,address"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
wantIdOutputFields: map[string]readTestResult{
target1.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
target2.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
target3.GetPublicId(): {wantErr: handlers.ForbiddenError()},
},
},
{
name: "project role grant this returns all created targets",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj2.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
wantIdOutputFields: map[string]readTestResult{
target1.GetPublicId(): {wantErr: handlers.ForbiddenError()},
target2.GetPublicId(): {wantErr: handlers.ForbiddenError()},
target3.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
},
},
{
name: "global role with individual project grant returns all created targets on the project",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=target;actions=list,read;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{proj2.PublicId},
},
}),
wantIdOutputFields: map[string]readTestResult{
target1.GetPublicId(): {wantErr: handlers.ForbiddenError()},
target2.GetPublicId(): {wantErr: handlers.ForbiddenError()},
target3.GetPublicId(): {wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrapper, tok, iamRepo)
for targetId, expect := range tc.wantIdOutputFields {
got, finalErr := s.GetTarget(fullGrantAuthCtx, &pbs.GetTargetRequest{
Id: targetId,
})
if expect.wantErr != nil {
require.ErrorIs(t, finalErr, expect.wantErr)
continue
}
require.NoError(t, err)
handlers.TestAssertOutputFields(t, got.Item, expect.wantOutputFields)
}
require.ElementsMatch(t, tc.wantIDs, gotIDs)
})
}
})
}
func TestGrants_Create(t *testing.T) {
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
iamRepo := iam.TestRepo(t, conn, wrapper)
s, err := testService(t, ctx, conn, kmsCache, wrapper)
require.NoError(t, err)
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
_, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
type expectResult struct {
wantErr error
wantOutputFields []string
}
testcases := []struct {
name string
input *pbs.CreateTargetRequest
userFunc func() (*iam.User, auth.Account)
scopeResultMap map[string]expectResult
}{
{
name: "global role descendants grants can create anywhere",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=target;actions=*;output_fields=id,authorized_actions,scope_id,address"},
GrantScopes: []string{globals.GrantScopeDescendants},
},
}),
scopeResultMap: map[string]expectResult{
proj1.PublicId: {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
proj2.PublicId: {wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField}},
},
},
{
name: "org role children grants can create in children scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=create;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeChildren},
},
}),
scopeResultMap: map[string]expectResult{
proj1.PublicId: {wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
proj2.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "proj role this grants can create this scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=create;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{globals.GrantScopeThis},
},
}),
scopeResultMap: map[string]expectResult{
proj1.PublicId: {wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
proj2.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
{
name: "global role specific project grants can create in project scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=target;actions=create;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{proj2.PublicId},
},
}),
scopeResultMap: map[string]expectResult{
proj1.PublicId: {wantErr: handlers.ForbiddenError()},
proj2.PublicId: {wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
},
},
{
name: "org role specific project grants can create in project scopes",
userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.GetPublicId(),
Grants: []string{"ids=*;type=target;actions=create;output_fields=id,name,created_time,updated_time,version"},
GrantScopes: []string{proj1.PublicId},
},
}),
scopeResultMap: map[string]expectResult{
proj1.PublicId: {wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField}},
proj2.PublicId: {wantErr: handlers.ForbiddenError()},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrapper, tok, iamRepo)
for scope, expect := range tc.scopeResultMap {
tgt := validTcpTarget(t, scope)
got, finalErr := s.CreateTarget(fullGrantAuthCtx, &pbs.CreateTargetRequest{
Item: tgt,
})
if expect.wantErr != nil {
require.ErrorIs(t, finalErr, expect.wantErr)
continue
}
require.NoError(t, finalErr)
handlers.TestAssertOutputFields(t, got.Item, expect.wantOutputFields)
}
})
}
}
type userFn func() (*iam.User, auth.Account)
func TestGrants_SetTargetCredentialSources(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
kmsCache := kms.TestKms(t, conn, wrapper)
iamRepo := iam.TestRepo(t, conn, wrapper)
s, err := testService(t, ctx, conn, kmsCache, wrapper)
require.NoError(t, err)
rw := db.New(conn)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
_, proj2 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj1Vault := vault.TestCredentialStores(t, conn, wrapper, proj1.GetPublicId(), 1)[0]
proj1Cls := vault.TestCredentialLibraries(t, conn, wrapper, proj1Vault.GetPublicId(), 2)
proj2StoreStatic := credstatic.TestCredentialStore(t, conn, wrapper, proj2.GetPublicId())
proj2Creds := credstatic.TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", proj2StoreStatic.GetPublicId(), proj2.GetPublicId(), 2)
testcases := []struct {
name string
input *pbs.ListTargetsRequest
setup func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn)
wantErr error
wantOutputFields []string
}{
{
name: "global role grant descendants returns succeed",
setup: func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn) {
setupUser := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
GrantScopes: []string{globals.GrantScopeDescendants},
Grants: []string{"ids=*;type=target;actions=*;output_fields=id,authorized_actions,scope_id,address"},
},
})
randId, err := uuid.GenerateUUID()
require.NoError(t, err)
tgt := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), randId, target.WithAddress("8.8.8.8"))
return &pbs.SetTargetCredentialSourcesRequest{
Id: tgt.GetPublicId(),
Version: tgt.GetVersion(),
BrokeredCredentialSourceIds: []string{proj1Cls[0].GetPublicId()},
}, setupUser
},
wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
},
{
name: "global role grant specific project and pinned id returns succeed",
setup: func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn) {
randId, err := uuid.GenerateUUID()
require.NoError(t, err)
tgt := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), randId, target.WithAddress("8.8.8.8"))
setupUser := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
GrantScopes: []string{proj1.PublicId},
Grants: []string{fmt.Sprintf("ids=%s;actions=set-credential-sources;output_fields=id,authorized_actions,scope_id,address", tgt.GetPublicId())},
},
})
return &pbs.SetTargetCredentialSourcesRequest{
Id: tgt.GetPublicId(),
Version: tgt.GetVersion(),
BrokeredCredentialSourceIds: []string{proj1Cls[0].GetPublicId()},
}, setupUser
},
wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
},
{
name: "global role grant specific project and pinned id wrong action error",
setup: func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn) {
randId, err := uuid.GenerateUUID()
require.NoError(t, err)
tgt := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), randId, target.WithAddress("8.8.8.8"))
setupUser := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
GrantScopes: []string{proj1.PublicId},
Grants: []string{fmt.Sprintf("ids=%s;actions=remove-credential-sources;output_fields=id,authorized_actions,scope_id,address", tgt.GetPublicId())},
},
})
return &pbs.SetTargetCredentialSourcesRequest{
Id: tgt.GetPublicId(),
Version: tgt.GetVersion(),
BrokeredCredentialSourceIds: []string{proj1Cls[0].GetPublicId()},
}, setupUser
},
wantErr: handlers.ForbiddenError(),
},
{
name: "org role grant children returns all succeed",
setup: func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn) {
setupUser := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.GetPublicId(),
GrantScopes: []string{globals.GrantScopeChildren},
Grants: []string{"ids=*;type=target;actions=*;output_fields=id,authorized_actions,scope_id,address"},
},
})
randId, err := uuid.GenerateUUID()
require.NoError(t, err)
tgt := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), randId, target.WithAddress("8.8.8.8"))
return &pbs.SetTargetCredentialSourcesRequest{
Id: tgt.GetPublicId(),
Version: tgt.GetVersion(),
BrokeredCredentialSourceIds: []string{proj1Cls[0].GetPublicId()},
}, setupUser
},
wantOutputFields: []string{globals.IdField, globals.AuthorizedActionsField, globals.ScopeIdField, globals.AddressField},
},
{
name: "org role grant this returns error",
setup: func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn) {
setupUser := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: org1.GetPublicId(),
GrantScopes: []string{globals.GrantScopeThis},
Grants: []string{"ids=*;type=target;actions=*;output_fields=id,authorized_actions,scope_id,address"},
},
})
randId, err := uuid.GenerateUUID()
require.NoError(t, err)
tgt := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), randId, target.WithAddress("8.8.8.8"))
return &pbs.SetTargetCredentialSourcesRequest{
Id: tgt.GetPublicId(),
Version: tgt.GetVersion(),
BrokeredCredentialSourceIds: []string{proj1Cls[0].GetPublicId()},
}, setupUser
},
wantErr: handlers.ForbiddenError(),
},
{
name: "project role grant this succeed",
setup: func(t *testing.T) (*pbs.SetTargetCredentialSourcesRequest, userFn) {
setupUser := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj2.GetPublicId(),
GrantScopes: []string{globals.GrantScopeThis},
Grants: []string{"ids=*;type=target;actions=*;output_fields=id,name,created_time,updated_time,version"},
},
})
randId, err := uuid.GenerateUUID()
require.NoError(t, err)
tgt := tcp.TestTarget(ctx, t, conn, proj2.GetPublicId(), randId, target.WithAddress("8.8.8.8"))
return &pbs.SetTargetCredentialSourcesRequest{
Id: tgt.GetPublicId(),
Version: tgt.GetVersion(),
BrokeredCredentialSourceIds: []string{proj2Creds[0].GetPublicId()},
}, setupUser
},
wantOutputFields: []string{globals.IdField, globals.NameField, globals.CreatedTimeField, globals.UpdatedTimeField, globals.VersionField},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
input, userFunc := tc.setup(t)
user, account := userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrapper, tok, iamRepo)
got, err := s.SetTargetCredentialSources(fullGrantAuthCtx, input)
if tc.wantErr != nil {
require.Error(t, err)
require.ErrorIs(t, err, tc.wantErr)
return
}
require.NoError(t, err)
handlers.TestAssertOutputFields(t, got.Item, tc.wantOutputFields)
})
}
}
func validTcpTarget(t *testing.T, scopeId string) *pb.Target {
randString, err := uuid.GenerateUUID()
require.NoError(t, err)
return &pb.Target{
ScopeId: scopeId,
Name: wrapperspb.String(randString),
Description: wrapperspb.String(randString),
Type: "tcp",
Attrs: &pb.Target_TcpTargetAttributes{
TcpTargetAttributes: &pb.TcpTargetAttributes{
DefaultPort: wrapperspb.UInt32(2),
DefaultClientPort: wrapperspb.UInt32(3),
},
},
}
}
@@ -1295,7 +1295,7 @@ func TestCreate(t *testing.T) {
r := iam.TestRole(t, conn, "global")
_ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId())
_ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*")
_ = iam.TestRoleGrantScope(t, conn, r.GetPublicId(), globals.GrantScopeDescendants)
_ = iam.TestRoleGrantScope(t, conn, r, globals.GrantScopeDescendants)
// Ensure we are using the OSS worker filter function. This prevents us from
// running tests in parallel.
@@ -1453,7 +1453,7 @@ func TestCreate(t *testing.T) {
},
},
}},
errStr: "unable to create target alias",
errStr: "PermissionDenied",
},
{
name: "Create a target with duplicate aliasses",
@@ -0,0 +1,29 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package handlers
import (
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"google.golang.org/protobuf/proto"
)
// TestAssertOutputFields asserts that the output fields of a group match the expected fields
// fields that is nil or empty in the result will throw an error if they are listed in expectedFields
// e.g. members when group does not contain any members
func TestAssertOutputFields(t *testing.T, p proto.Message, expectFields []string) {
msg := p.ProtoReflect()
descriptor := msg.Descriptor()
for i := 0; i < descriptor.Fields().Len(); i++ {
fd := descriptor.Fields().Get(i)
fieldName := string(fd.Name())
if !slices.Contains(expectFields, fieldName) {
require.Falsef(t, msg.Has(fd), "expect field '%s' to be empty but got %+v", fd.Name(), msg.Get(fd).Interface())
continue
}
require.Truef(t, msg.Has(fd), "expect field '%s' NOT be empty but got %+v", fd.Name(), msg.Get(fd).Interface())
}
}
@@ -5,36 +5,27 @@ package users_test
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/boundary/globals"
talias "github.com/hashicorp/boundary/internal/alias/target"
"github.com/hashicorp/boundary/internal/auth"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
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/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/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/tcp"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)
// 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")
@@ -50,6 +41,9 @@ func TestGrants_ReadActions(t *testing.T) {
}
s, err := users.NewService(ctx, repoFn, aliasRepoFn, 1000)
require.NoError(t, err)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
org1, _ := iam.TestScopes(t, iamRepo)
org2, _ := iam.TestScopes(t, iamRepo)
@@ -60,13 +54,13 @@ func TestGrants_ReadActions(t *testing.T) {
t.Run("List", func(t *testing.T) {
testcases := []struct {
name string
input *pbs.ListUsersRequest
name string
input *pbs.ListUsersRequest
userFunc func() (*iam.User, auth.Account)
// 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
}{
@@ -76,13 +70,13 @@ func TestGrants_ReadActions(t *testing.T) {
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
},
}),
includeTestUsers: true,
wantErr: nil,
wantIDs: []string{
@@ -102,13 +96,13 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: false,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org2.PublicId,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
},
},
}),
wantErr: nil,
wantIDs: []string{
org2User1.PublicId,
@@ -122,13 +116,13 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: false,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
},
},
}),
wantErr: nil,
wantIDs: []string{
org2User1.PublicId,
@@ -142,13 +136,13 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: false,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
},
},
}),
wantErr: nil,
wantIDs: []string{
org1User1.PublicId,
@@ -163,13 +157,13 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: false,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org1.PublicId,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: []string{
org1User1.PublicId,
@@ -182,18 +176,18 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: false,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org1.PublicId,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: org1.PublicId,
Grants: []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},
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: []string{
org1User1.PublicId,
@@ -208,13 +202,13 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: false,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: []string{},
},
@@ -225,13 +219,13 @@ func TestGrants_ReadActions(t *testing.T) {
Recursive: true,
},
includeTestUsers: true,
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeThis},
},
},
}),
wantErr: nil,
wantIDs: []string{
globals.AnyAuthenticatedUserId,
@@ -246,13 +240,13 @@ func TestGrants_ReadActions(t *testing.T) {
ScopeId: globals.GlobalPrefix,
Recursive: false,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: globals.GlobalPrefix,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
RoleScopeId: globals.GlobalPrefix,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
},
},
}),
wantErr: handlers.ForbiddenError(),
wantIDs: nil,
},
@@ -262,13 +256,13 @@ func TestGrants_ReadActions(t *testing.T) {
ScopeId: globals.GlobalPrefix,
Recursive: false,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeID: org2.PublicId,
GrantStrings: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
RoleScopeId: org2.PublicId,
Grants: []string{"ids=*;type=user;actions=list,read"},
GrantScopes: []string{globals.GrantScopeChildren},
},
},
}),
wantErr: handlers.ForbiddenError(),
wantIDs: nil,
},
@@ -278,20 +272,21 @@ func TestGrants_ReadActions(t *testing.T) {
ScopeId: globals.GlobalPrefix,
Recursive: true,
},
rolesToCreate: []authtoken.TestRoleGrantsForToken{},
wantErr: handlers.ForbiddenError(),
wantIDs: nil,
userFunc: iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
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)
user, account := tc.userFunc()
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
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)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, finalErr := s.ListUsers(fullGrantAuthCtx, tc.input)
if tc.wantErr != nil {
require.ErrorIs(t, finalErr, tc.wantErr)
@@ -310,3 +305,377 @@ func TestGrants_ReadActions(t *testing.T) {
}
})
}
func TestGrants_ListResolvableAliases(t *testing.T) {
ctx := t.Context()
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)
atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
require.NoError(t, err)
repoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
sqlDb, err := conn.SqlDB(ctx)
require.NoError(t, err)
// delete all default roles because they come with list-resolvable-aliases permissions
_, err = sqlDb.ExecContext(ctx, `delete from iam_role`)
require.NoError(t, err)
aliasRepoFn := func() (*talias.Repository, error) {
return talias.NewRepository(t.Context(), rw, rw, kmsCache)
}
s, err := users.NewService(ctx, repoFn, aliasRepoFn, 1000)
require.NoError(t, err)
org1, proj1 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
proj2 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
org2, proj3 := iam.TestScopes(t, iamRepo, iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
target1 := tcp.TestTarget(ctx, t, conn, proj1.GetPublicId(), "test address-1", target.WithAddress("8.8.8.8"))
target2 := tcp.TestTarget(ctx, t, conn, proj2.GetPublicId(), "test address-2", target.WithAddress("8.8.8.8"))
target3 := tcp.TestTarget(ctx, t, conn, proj3.GetPublicId(), "test address-3", target.WithAddress("8.8.8.8"))
alias1 := talias.TestAlias(t, rw, "target1",
talias.WithName("target1"),
talias.WithDescription("target1"),
talias.WithDestinationId(target1.GetPublicId()),
talias.WithHostId(target1.GetPublicId()))
alias2 := talias.TestAlias(t, rw, "target2",
talias.WithName("target2"),
talias.WithDescription("target2"),
talias.WithDestinationId(target2.GetPublicId()),
talias.WithHostId(target2.GetPublicId()))
alias3 := talias.TestAlias(t, rw, "target3",
talias.WithName("target3"),
talias.WithDescription("target3"),
talias.WithDestinationId(target3.GetPublicId()),
talias.WithHostId(target3.GetPublicId()))
fmt.Println(alias1, "\n", alias2, "\n", alias3)
testcases := []struct {
name string
// setting returns user/account of a listing user which may be different than user in ListResolvableAliasesRequest
setupInput func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest)
expectIdOutputFieldsMap map[string][]string
wantErr error
}{
{
name: "global role this and descendants grants return all aliases list same user",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,name,version,description,value",
"ids=*;type=target;actions=*",
},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: listingUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{
alias1.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias2.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias3.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
},
},
{
name: "global role this and org2 role children grants return all aliases list same user",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value",
},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: org2.PublicId,
Grants: []string{
"ids=*;type=target;actions=*",
},
GrantScopes: []string{globals.GrantScopeChildren},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: listingUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{
alias3.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
},
},
{
name: "list other user without permissions returns nothing",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value",
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{},
},
{
name: "list other user with list-resolvable-aliases permissions but no targets permissions returns nothing",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=*",
},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value",
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{},
},
{
name: "list other user with no list-resolvable-aliases permissions but all targets permissions returns nothing",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=target;actions=*",
},
GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
},
})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value",
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{
alias1.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias2.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias3.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
},
},
{
name: "list other user with list-resolvable-aliases permissions and some targets permissions returns allowed targets",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj3.PublicId,
Grants: []string{
fmt.Sprintf("ids=%s;type=target;actions=authorize-session", target3.GetPublicId()),
},
GrantScopes: []string{proj3.PublicId},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{
fmt.Sprintf("ids=%s;type=target;actions=authorize-session", "*"),
},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: org1.PublicId,
Grants: []string{
fmt.Sprintf("ids=%s;type=target;actions=authorize-session", target1.GetPublicId()),
},
GrantScopes: []string{globals.GrantScopeChildren},
},
})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
fmt.Sprintf("ids=%s;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value", testUser.PublicId),
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{
alias1.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias2.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias3.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
},
},
{
name: "list other user with list-resolvable-aliases permissions and some targets permissions returns allowed with children grants granting different resources",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: proj1.PublicId,
Grants: []string{"ids=*;type=target;actions=authorize-session"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: proj2.PublicId,
Grants: []string{"ids=*;type=target;actions=authorize-session"},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: org1.PublicId,
Grants: []string{"ids=*;type=role;actions=*"},
GrantScopes: []string{globals.GrantScopeChildren},
},
})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
fmt.Sprintf("ids=%s;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value", testUser.PublicId),
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{
alias1.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias2.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
},
},
{
name: "list other user with specific list-resolvable-aliases permissions and specific targets permissions returns allowed targets",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list-resolvable-aliases;output_fields=created_time",
},
GrantScopes: []string{globals.GrantScopeThis},
},
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
fmt.Sprintf("ids=%s,%s;type=target;actions=authorize-session", target2.GetPublicId(), target3.GetPublicId()),
},
GrantScopes: []string{globals.GrantScopeDescendants},
},
})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
fmt.Sprintf("ids=%s;type=user;actions=list-resolvable-aliases;output_fields=id,created_time,value", testUser.PublicId),
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
expectIdOutputFieldsMap: map[string][]string{
alias2.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
alias3.PublicId: {globals.IdField, globals.CreatedTimeField, globals.ValueField},
},
},
{
name: "no permissions cannot list resolvable aliases",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
"ids=*;type=user;actions=list;output_fields=id,created_time,value",
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: listingUser.PublicId,
}
},
wantErr: handlers.ForbiddenError(),
},
{
name: "no permissions cannot list resolvable aliases from another user",
setupInput: func(t *testing.T) (*iam.User, auth.Account, *pbs.ListResolvableAliasesRequest) {
testUser, _ := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{})()
listingUser, listingAccount := iam.TestUserGroupGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
{
RoleScopeId: globals.GlobalPrefix,
Grants: []string{
fmt.Sprintf("ids=*;type=user;actions=delete"),
},
GrantScopes: []string{globals.GrantScopeThis},
},
})()
return listingUser, listingAccount, &pbs.ListResolvableAliasesRequest{
Id: testUser.PublicId,
}
},
wantErr: handlers.ForbiddenError(),
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
user, account, input := tc.setupInput(t)
tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
require.NoError(t, err)
fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
got, err := s.ListResolvableAliases(fullGrantAuthCtx, input)
if tc.wantErr != nil {
require.ErrorIs(t, tc.wantErr, tc.wantErr)
return
}
require.NoError(t, err)
var gotIds []string
for _, i := range got.Items {
gotIds = append(gotIds, i.Id)
}
require.ElementsMatch(t, gotIds, maps.Keys(tc.expectIdOutputFieldsMap))
require.Len(t, got.Items, len(tc.expectIdOutputFieldsMap))
for _, item := range got.Items {
expectFields, ok := tc.expectIdOutputFieldsMap[item.GetId()]
require.True(t, ok)
handlers.TestAssertOutputFields(t, item, expectFields)
}
})
}
}
@@ -102,7 +102,7 @@ func (s Service) ListUsers(ctx context.Context, req *pbs.ListUsersRequest) (*pbs
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -240,7 +240,7 @@ func (s Service) GetUser(ctx context.Context, req *pbs.GetUserRequest) (*pbs.Get
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -278,7 +278,7 @@ func (s Service) CreateUser(ctx context.Context, req *pbs.CreateUserRequest) (*p
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -315,7 +315,7 @@ func (s Service) UpdateUser(ctx context.Context, req *pbs.UpdateUserRequest) (*p
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -351,7 +351,7 @@ func (s Service) DeleteUser(ctx context.Context, req *pbs.DeleteUserRequest) (*p
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -369,7 +369,7 @@ func (s Service) AddUserAccounts(ctx context.Context, req *pbs.AddUserAccountsRe
if err := validateAddUserAccountsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddAccounts)
authResults := s.authResult(ctx, req.GetId(), action.AddAccounts, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -407,7 +407,7 @@ func (s Service) SetUserAccounts(ctx context.Context, req *pbs.SetUserAccountsRe
if err := validateSetUserAccountsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetAccounts)
authResults := s.authResult(ctx, req.GetId(), action.SetAccounts, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -445,7 +445,7 @@ func (s Service) RemoveUserAccounts(ctx context.Context, req *pbs.RemoveUserAcco
if err := validateRemoveUserAccountsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveAccounts)
authResults := s.authResult(ctx, req.GetId(), action.RemoveAccounts, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -483,7 +483,7 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.ListResolvableAliases)
authResults := s.authResult(ctx, req.GetId(), action.ListResolvableAliases, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -492,18 +492,12 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
acl := authResults.ACL()
grantsHash, err := authResults.GrantsHash(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if req.GetId() != authResults.UserId {
var err error
acl, grantsHash, err = s.aclAndGrantHashForUser(ctx, req.GetId())
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
}
// fetch ACL and grantsHash for target resource so we can resolve ListResolvableAliasesPermissions
// because permissions in authResults only contains permissions relevant to resource.User
acl, grantsHash, err := s.aclAndGrantHashForUser(ctx, req.GetId(), []resource.Type{resource.Target})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
}
permissions := acl.ListResolvableAliasesPermissions(resource.Target, targets.IdActions)
@@ -597,13 +591,18 @@ func (s Service) ListResolvableAliases(ctx context.Context, req *pbs.ListResolva
// aclAndGrantHashForUser returns an ACL from the grants provided to the user and
// the hash of those grants.
func (s Service) aclAndGrantHashForUser(ctx context.Context, userId string) (perms.ACL, []byte, error) {
func (s Service) aclAndGrantHashForUser(ctx context.Context, userId string, resourceType []resource.Type) (perms.ACL, []byte, error) {
const op = "users.(Service).aclAndGrantHashForUser"
iamRepo, err := s.repoFn()
if err != nil {
return perms.ACL{}, nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
}
grantTuples, err := iamRepo.GrantsForUser(ctx, userId)
// Need to resolve all possible permissions for a user on a specific resource type because this request
// does not have a request scope. A user may be associated with a role at a higher-level scope
// (e.g. user in an org can be a principal of a role in the global scope) so we always have to
// look up the user's grants as if the request is a global-scoped to resolve the user's
// full permissions tree
grantTuples, err := iamRepo.GrantsForUser(ctx, userId, resourceType, globals.GlobalPrefix, iam.WithRecursive(true))
if err != nil {
return perms.ACL{}, nil, errors.Wrap(ctx, err, op, errors.WithoutEvent())
}
@@ -786,7 +785,7 @@ func (s Service) removeInRepo(ctx context.Context, userId string, accountIds []s
return out, accts, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
repo, err := s.repoFn()
if err != nil {
@@ -795,7 +794,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.User), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.Create:
parentId = id
@@ -822,7 +821,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.User, opts...)
}
func toProto(ctx context.Context, in *iam.User, accts []string, opt ...handlers.Option) (*pb.User, error) {
@@ -887,6 +886,8 @@ func toProto(ctx context.Context, in *iam.User, accts []string, opt ...handlers.
return &out, nil
}
// toResolvableAliasProto converts *talias.Alias to *aliaspb.Alias only including fields specified in output_fields.
// These fields are not included in the result set: Name, Description, Version
func toResolvableAliasProto(a *talias.Alias, opt ...handlers.Option) (*aliaspb.Alias, error) {
opts := handlers.GetOpts(opt...)
if opts.WithOutputFields == nil {
@@ -916,7 +917,6 @@ func toResolvableAliasProto(a *talias.Alias, opt ...handlers.Option) (*aliaspb.A
if outputFields.Has(globals.TypeField) {
pbItem.Type = "target"
}
return pbItem, nil
}
File diff suppressed because it is too large. Load diff
@@ -124,7 +124,7 @@ func (s Service) ListWorkers(ctx context.Context, req *pbs.ListWorkersRequest) (
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
authResults := s.authResult(ctx, req.GetScopeId(), action.List, req.GetRecursive())
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
@@ -202,7 +202,7 @@ func (s Service) GetWorker(ctx context.Context, req *pbs.GetWorkerRequest) (*pbs
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
authResults := s.authResult(ctx, req.GetId(), action.Read, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -286,7 +286,7 @@ func (s Service) CreateControllerLed(ctx context.Context, req *pbs.CreateControl
func (s Service) createCommon(ctx context.Context, in *pb.Worker, act action.Type, opt ...server.Option) (*pb.Worker, error) {
const op = "workers.(Service).createCommon"
authResults := s.authResult(ctx, in.GetScopeId(), act)
authResults := s.authResult(ctx, in.GetScopeId(), act, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -323,7 +323,7 @@ func (s Service) DeleteWorker(ctx context.Context, req *pbs.DeleteWorkerRequest)
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
authResults := s.authResult(ctx, req.GetId(), action.Delete, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -351,7 +351,7 @@ func (s Service) UpdateWorker(ctx context.Context, req *pbs.UpdateWorkerRequest)
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
authResults := s.authResult(ctx, req.GetId(), action.Update, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -425,7 +425,7 @@ func (s Service) AddWorkerTags(ctx context.Context, req *pbs.AddWorkerTagsReques
if err := validateAddTagsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.AddWorkerTags)
authResults := s.authResult(ctx, req.GetId(), action.AddWorkerTags, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -462,7 +462,7 @@ func (s Service) SetWorkerTags(ctx context.Context, req *pbs.SetWorkerTagsReques
if err := validateSetTagsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.SetWorkerTags)
authResults := s.authResult(ctx, req.GetId(), action.SetWorkerTags, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -499,7 +499,7 @@ func (s Service) RemoveWorkerTags(ctx context.Context, req *pbs.RemoveWorkerTags
if err := validateRemoveTagsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.RemoveWorkerTags)
authResults := s.authResult(ctx, req.GetId(), action.RemoveWorkerTags, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -536,7 +536,7 @@ func (s Service) ReadCertificateAuthority(ctx context.Context, req *pbs.ReadCert
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.ReadCertificateAuthority)
authResults := s.authResult(ctx, req.GetScopeId(), action.ReadCertificateAuthority, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -557,7 +557,7 @@ func (s Service) ReinitializeCertificateAuthority(ctx context.Context, req *pbs.
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.ReinitializeCertificateAuthority)
authResults := s.authResult(ctx, req.GetScopeId(), action.ReinitializeCertificateAuthority, false)
if authResults.Error != nil {
return nil, authResults.Error
}
@@ -751,7 +751,7 @@ func (s Service) removeTagsInRepo(ctx context.Context, workerId string, workerVe
return w, nil
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
func (s Service) authResult(ctx context.Context, id string, a action.Type, isRecursive bool) auth.VerifyResults {
res := auth.VerifyResults{}
repo, err := s.repoFn()
if err != nil {
@@ -760,7 +760,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Worker), auth.WithAction(a)}
opts := []auth.Option{auth.WithAction(a), auth.WithRecursive(isRecursive)}
switch a {
case action.List, action.CreateWorkerLed, action.CreateControllerLed, action.ReadCertificateAuthority, action.ReinitializeCertificateAuthority:
parentId = id
@@ -778,7 +778,7 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
return auth.Verify(ctx, resource.Worker, opts...)
}
func (s Service) listCertificateAuthorityFromRepo(ctx context.Context) (*types.RootCertificates, error) {
@@ -32,6 +32,7 @@ import (
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/target/tcp"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
"github.com/mr-tron/base58"
@@ -355,7 +356,9 @@ func Test_unaryCtxInterceptor(t *testing.T) {
return
}
require.NoError(err)
verifyResults := auth.Verify(retCtx.(context.Context))
// Use resource.Scope here but resource type shouldn't matter since we're only validating that auth.Verify
// can use the 'context' with additional ctx.Value from the interceptor
verifyResults := auth.Verify(retCtx.(context.Context), resource.Scope)
assert.NotEmpty(verifyResults)
})
}
@@ -641,7 +644,9 @@ func Test_streamCtxInterceptor(t *testing.T) {
return
}
require.NoError(err)
verifyResults := auth.Verify(hdCtx.(context.Context))
// Use resource.Scope here but resource type shouldn't matter since we're only validating that auth.Verify
// can use the 'context' with additional ctx.Value from the interceptor
verifyResults := auth.Verify(hdCtx.(context.Context), resource.Scope)
assert.NotEmpty(verifyResults)
})
}
@@ -0,0 +1,115 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hook97001
import (
"context"
"database/sql"
"fmt"
"github.com/hashicorp/boundary/internal/db/schema/migration"
)
var RepairDescription = `Removes redundant grant scopes from roles. Descendants and Children grant scopes grant permissions to
multiple scopes which may overlap with individually granted scopes. Any individually granted scopes that have already been
covered by 'descendants' or 'children' grants are considered invalid and are removed`
type invalidAssociation struct {
RoleId string `db:"role_id"`
RoleScopeId string `db:"role_scope_id"`
CoveredByGrantScope string `db:"covered_by_grant_scope"`
IndividualGrantScope string `db:"individual_grant_scope"`
}
func (e *invalidAssociation) problemString() string {
return fmt.Sprintf(`Role '%s' in scope '%s' has the '%s' grant scope which covers '%s'`,
e.RoleId, e.RoleScopeId, e.CoveredByGrantScope, e.IndividualGrantScope)
}
func (e *invalidAssociation) repairString() string {
return fmt.Sprintf(`Remove redundant grant scopes '%s' association from role '%s' in scope '%s' because it overlaps with '%s'`,
e.IndividualGrantScope, e.RoleId, e.RoleScopeId, e.CoveredByGrantScope)
}
// FindInvalidAssociations executes a query to identify invalid associations between
// roles and overlapping grant scopes.
// roles in global scope
// - 'descendants' covers all individual scope so any individual grant scope is invalid if a role already has 'descendants' grant scope
// - 'children' covers all org scopes so any individual org grant scope is invalid if a role already has 'children' grant scope
//
// roles in org scope
// - 'children' covers all projects the org owns. Any individually project grant scope when role already has 'children' grant scope is invalid
//
// It returns migration.Problems if
// any invalid associations were found; nil if no invalid associations were found
// or an error. Implements the CheckFunc definition from the migration package.
//
// An example of a migration problem:
// Role 'r_orgaa___97001' in scope 'o_ta___97001' has 'children' grant scope which covers 'p_pA___97001'
func FindInvalidAssociations(ctx context.Context, tx *sql.Tx) (migration.Problems, error) {
if tx == nil {
return nil, fmt.Errorf("query to get invalid associations failed: missing transaction")
}
invalidAssociations, err := query(ctx, tx, getInvalidGrantsAssociationsQuery)
if err != nil {
return nil, fmt.Errorf("query to get invalid associations failed: %v", err)
}
if len(invalidAssociations) > 0 {
var problems migration.Problems
for _, ia := range invalidAssociations {
problems = append(problems, ia.problemString())
}
return problems, nil
}
return nil, nil
}
// RepairInvalidAssociations executes a query to remove redundant grant scopes from roles.
// Redundant grant scopes are individual scopes assigned to roles that are already granted the same permissions by broader, special grant scopes
// ['children', 'descendants']. These broader grant scopes automatically include permissions to the individual
// scopes, making the individual assignments unnecessary.
//
// It returns migration.Repairs if any redundant grant scopes were removed, nil if none were found, or an error.
// Implements the RepairFunc definition from the migration package.
//
// Example migration repair message:
// "Remove redundant grant scopes 'o_ta___97001' association from role 'r_globala_97001' in scope 'global' because it overlaps with 'descendants'"
func RepairInvalidAssociations(ctx context.Context, tx *sql.Tx) (migration.Repairs, error) {
if tx == nil {
return nil, fmt.Errorf("query to delete invalid grants associations failed: missing transaction")
}
invalid, err := query(ctx, tx, deleteInvalidGrantsAssociationsQuery)
if err != nil {
return nil, fmt.Errorf("query to delete invalid grants associations failed: %v", err)
}
if len(invalid) > 0 {
var repairs migration.Repairs
for _, ia := range invalid {
repairs = append(repairs, ia.repairString())
}
return repairs, nil
}
return nil, nil
}
func query(ctx context.Context, tx *sql.Tx, query string) ([]invalidAssociation, error) {
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
invalid := make([]invalidAssociation, 0)
for rows.Next() {
var r invalidAssociation
if err := rows.Scan(&r.RoleId, &r.RoleScopeId, &r.CoveredByGrantScope, &r.IndividualGrantScope); err != nil {
return nil, err
}
invalid = append(invalid, r)
}
if err := rows.Err(); err != nil {
return nil, err
}
return invalid, nil
}
@@ -0,0 +1,134 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hook97001
const (
baseQuery = `
with
global_roles (role_id) as (
select public_id
from iam_role
where scope_id = 'global'
),
org_roles (role_id) as (
select public_id
from iam_role
where scope_id like 'o_%'
),
-- find all global role ids with grant scope descendants and individual org or project grant scopes
-- descendants already cover all orgs and projects so individual grant scopes are not necessary
global_roles_with_overlap_descendants_and_individual(role_id) as (
select role_id
from iam_role_grant_scope
where role_id in (select role_id from global_roles)
group by role_id
having
count(*) filter (where scope_id_or_special like 'o_%' or scope_id_or_special like 'p_%') >=1 and
count(*) filter (where scope_id_or_special = 'descendants') >= 1
),
-- find all individual org or project grant scopes associated with role ids found to have overlapping grants
-- filter out all special grants because we want to keep them and delete individual grant scopes
global_descendants_overlap(role_id, role_scope_id, grant_scope_id) as (
select rgs.role_id as role_id,
r.scope_id as role_scope_id,
rgs.scope_id_or_special as grant_scope_id
from iam_role_grant_scope rgs
join iam_role r on r.public_id = rgs.role_id
-- skip special grants because we only need individually grant scopes in the result set
where scope_id_or_special not in ('this', 'children', 'descendants')
and role_id in (select role_id
from global_roles_with_overlap_descendants_and_individual)
),
-- find all global role ids with grant scope children and individual project grant scopes
-- children already cover all projects so individual grant scopes are not necessary
global_roles_with_overlap_children_and_orgs(role_id) as (
select role_id
from iam_role_grant_scope
where role_id in (select role_id from global_roles)
group by role_id
having
count(*) filter (where scope_id_or_special like 'o_%') >= 1 and
count(*) filter (where scope_id_or_special = 'children') >= 1
),
-- find all individual project grant scopes associated with role ids found to have overlapping grants
-- filter out all special grants because we want to keep them and delete individual grant scopes
global_children_overlap(role_id, role_scope_id, grant_scope_id) as (
select rgs.role_id as role_id,
r.scope_id as role_scope_id,
rgs.scope_id_or_special as grant_scope_id
from iam_role_grant_scope rgs
join iam_role r on r.public_id = rgs.role_id
where scope_id_or_special not in ('this', 'children', 'descendants')
and scope_id_or_special not like 'p_%' -- filter out projects because children + project is valid
and role_id in (select role_id
from global_roles_with_overlap_children_and_orgs)
),
-- find all global role ids with grant scope children and individual project grant scopes
-- children already cover all projects so individual grant scopes are not necessary
org_roles_with_overlap_children_and_projects(role_id) as (
select role_id
from iam_role_grant_scope
where role_id in (select role_id from org_roles)
group by role_id
having
count(*) filter (where scope_id_or_special like 'p_%') >= 1 and
count(*) filter (where scope_id_or_special = 'children') >= 1
),
-- find all individual project grant scopes associated with role ids found to have overlapping grants
-- filter out all special grants because we want to keep them and delete individual grant scopes
org_children_overlap(role_id, role_scope_id, grant_scope_id) as (
select rgs.role_id as role_id,
r.scope_id as role_scope_id,
rgs.scope_id_or_special as grant_scope_id
from iam_role_grant_scope rgs
join iam_role r on r.public_id = rgs.role_id
where scope_id_or_special not in ('this', 'children', 'descendants')
and role_id in (select role_id
from org_roles_with_overlap_children_and_projects)
),
problems (role_id, role_scope_id, covered_by_grant_scope, individual_grant_scope) as (
select role_id as role_id,
role_scope_id as role_scope_id,
'descendants' as covered_by_grant_scope,
grant_scope_id as individual_grant_scope
from global_descendants_overlap
union
select role_id as role_id,
role_scope_id as role_scope_id,
'children' as covered_by_grant_scope,
grant_scope_id as individual_grant_scope
from global_children_overlap
union
select role_id as role_id,
role_scope_id as role_scope_id,
'children' as covered_by_grant_scope,
grant_scope_id as individual_grant_scope
from org_children_overlap
)`
getInvalidGrantsAssociationsQuery = baseQuery + `
select * from problems order by role_id, individual_grant_scope;
`
deleteInvalidGrantsAssociationsQuery = baseQuery + `,
deleted_grant_scope (role_id, scope_id_or_special) as (
delete
from iam_role_grant_scope
where (role_id, scope_id_or_special) in (select role_id,
individual_grant_scope
from problems)
returning role_id, scope_id_or_special
),
deleted_problems (role_id, role_scope_id, covered_by_grant_scope, individual_grant_scope) as (
select role_id as role_id,
role_scope_id as role_scope_id,
covered_by_grant_scope as covered_by_grant_scope,
individual_grant_scope as individual_grant_scope
from problems
where (role_id, individual_grant_scope) in (select role_id,
scope_id_or_special
from deleted_grant_scope)
)
select * from deleted_problems;`
)
+6
View File
@@ -12,6 +12,7 @@ import (
"github.com/hashicorp/boundary/internal/db/schema/internal/edition"
"github.com/hashicorp/boundary/internal/db/schema/migration"
"github.com/hashicorp/boundary/internal/db/schema/migrations/oss/internal/hook46001"
"github.com/hashicorp/boundary/internal/db/schema/migrations/oss/internal/hook97001"
)
// postgres contains the migrations sql files for postgres oss edition
@@ -25,6 +26,11 @@ var prehooks = map[int]*migration.Hook{
RepairFunc: hook46001.RepairIllegalAssociations,
RepairDescription: hook46001.RepairDescription,
},
97001: {
CheckFunc: hook97001.FindInvalidAssociations,
RepairFunc: hook97001.RepairInvalidAssociations,
RepairDescription: hook97001.RepairDescription,
},
}
func init() {
@@ -332,6 +332,7 @@ create table iam_role (
);
-- Grants are immutable, which is enforced via the trigger below
-- Altered in 100/05_iam_grant.up.sql to add constraint on canonical_grant
create table iam_role_grant (
create_time wt_timestamp,
role_id wt_role_id -- pk
@@ -0,0 +1,297 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- Create the enumeration table for the grant scope types for the global iam_role
create table iam_role_global_grant_scope_enm (
name text primary key
constraint only_predefined_scope_types_allowed
check(
name in (
'descendants',
'children',
'individual'
)
)
);
comment on table iam_role_global_grant_scope_enm is
'iam_role_global_grant_scope_enm is an enumeration table for role grant scope types for the iam_role_global table.';
-- global iam_role must have a scope_id of global.
--
-- grant_this_role_scope indicates if the role can apply its grants to the scope.
-- grant_scope indicates the scope of the grants.
-- grant_scope can be 'descendants', 'children', or 'individual'.
--
-- grant_this_role_scope_update_time and grant_scope_update_time are used to track
-- the last time the grant_this_role_scope and grant_scope columns were updated.
-- This is used to represent the grant scope create_time column from the
-- iam_role_grant_scope table in 83/01_iam_role_grant_scope.up.sql.
-- This matches the representation of the existing create_time field at the
-- role domain layer that indicates when the grant scope was created.
create table iam_role_global (
public_id wt_role_id primary key
constraint iam_role_fkey
references iam_role(public_id)
on delete cascade
on update cascade,
scope_id wt_scope_id not null
constraint iam_scope_global_fkey
references iam_scope_global(scope_id)
on delete cascade
on update cascade,
name text,
description text,
grant_this_role_scope boolean not null default false,
grant_scope text not null
constraint iam_role_global_grant_scope_enm_fkey
references iam_role_global_grant_scope_enm(name)
on delete restrict
on update cascade,
version wt_version,
grant_this_role_scope_update_time wt_timestamp,
grant_scope_update_time wt_timestamp,
create_time wt_timestamp,
update_time wt_timestamp,
constraint iam_role_global_grant_scope_public_id_uq
unique(grant_scope, public_id),
constraint iam_role_global_name_scope_id_uq
unique(name, scope_id)
);
comment on table iam_role_global is
'iam_role_global is the subtype table for the global role. grant_this_role_scope_update_time and grant_scope_update_time are used to track the last time the grant_this_role_scope and grant_scope columns were updated.';
create table iam_role_global_individual_org_grant_scope (
role_id wt_role_id
constraint iam_role_global_fkey
references iam_role_global(public_id)
on delete cascade
on update cascade,
scope_id wt_scope_id not null
constraint iam_scope_org_fkey
references iam_scope_org(scope_id)
on delete cascade
on update cascade,
-- grant_scope is used for constraint checking.
-- This restricts the grant_scope to be 'individual'
-- and since it is also a foreign key to the iam_role_global
-- grant_scope, it ensures that iam_role_global is set to 'individual'
-- if this table is populated for the corresponding role.
grant_scope text not null
constraint only_individual_grant_scope_allowed
check(
grant_scope = 'individual'
),
constraint iam_role_global_grant_scope_fkey
foreign key (role_id, grant_scope)
references iam_role_global(public_id, grant_scope)
on delete cascade
on update cascade,
create_time wt_timestamp,
primary key(role_id, scope_id)
);
comment on table iam_role_global_individual_org_grant_scope is
'iam_role_global_individual_org_grant_scope is a list of individually granted org scope to global roles with grant_scope of individual.';
create table iam_role_global_individual_project_grant_scope (
role_id wt_role_id
constraint iam_role_global_fkey
references iam_role_global(public_id)
on delete cascade
on update cascade,
scope_id wt_scope_id not null
constraint iam_scope_project_fkey
references iam_scope_project(scope_id)
on delete cascade
on update cascade,
-- grant_scope is used for constraint checking.
-- This restricts the grant_scope to be 'individual'
-- and since it is also a foreign key to the iam_role_global
-- grant_scope, it ensures that iam_role_global is set to 'individual'
-- if this table is populated for the corresponding role.
-- both children and individual are allowed for this global role
-- because projects can be individually in addition to children
-- which grants all orgs
grant_scope text not null
constraint only_individual_or_children_grant_scope_allowed
check(
grant_scope in ('individual', 'children')
),
constraint iam_role_global_grant_scope_fkey
foreign key (role_id, grant_scope)
references iam_role_global(public_id, grant_scope)
on delete cascade
on update cascade,
create_time wt_timestamp,
primary key(role_id, scope_id)
);
comment on table iam_role_global_individual_project_grant_scope is
'iam_role_global_individual_project_grant_scope is a list of individually granted project scope table to global role with grant_scope of individual or children.';
-- Create the enumeration table for the grant scope types for the org iam_role
create table iam_role_org_grant_scope_enm (
name text primary key
constraint only_predefined_scope_types_allowed
check(
name in (
'children',
'individual'
)
)
);
comment on table iam_role_org_grant_scope_enm is
'iam_role_org_grant_scope_enm is an enumeration table for role grant scope types for the iam_role_org table.';
create table iam_role_org (
public_id wt_role_id primary key
constraint iam_role_fkey
references iam_role(public_id)
on delete cascade
on update cascade,
scope_id wt_scope_id not null
constraint iam_scope_org_fkey
references iam_scope_org(scope_id)
on delete cascade
on update cascade,
name text,
description text,
grant_this_role_scope boolean not null default false,
grant_scope text not null
constraint iam_role_org_grant_scope_enm_fkey
references iam_role_org_grant_scope_enm(name)
on delete restrict
on update cascade,
version wt_version,
grant_this_role_scope_update_time wt_timestamp,
grant_scope_update_time wt_timestamp,
create_time wt_timestamp,
update_time wt_timestamp,
constraint iam_role_org_grant_scope_public_id_uq
unique(grant_scope, public_id),
constraint iam_role_org_name_scope_id_uq
unique(name, scope_id)
);
comment on table iam_role_org is
'iam_role_org is a subtype table of the iam_role table. It is used to store roles that are scoped to an org.';
create table iam_role_org_individual_grant_scope (
role_id wt_role_id
constraint iam_role_org_fkey
references iam_role_org(public_id)
on delete cascade
on update cascade,
scope_id wt_scope_id not null
constraint iam_scope_project_fkey
references iam_scope_project(scope_id)
on delete cascade
on update cascade,
-- grant_scope is used for constraint checking.
-- This restricts the grant_scope to be 'individual'
-- and since it is also a foreign key to the iam_role_org
-- grant_scope, it ensures that iam_role_org is set to 'individual'
-- if this table is populated for the corresponding role.
grant_scope text not null
constraint only_individual_grant_scope_allowed
check(
grant_scope = 'individual'
),
constraint iam_role_org_grant_scope_fkey
foreign key (role_id, grant_scope)
references iam_role_org(public_id, grant_scope)
on delete cascade
on update cascade,
create_time wt_timestamp,
primary key(role_id, scope_id)
);
comment on table iam_role_org_individual_grant_scope is
'iam_role_org_individual_grant_scope is a list of individually granted project scope to org roles with grant_scope of individual';
create table iam_role_project (
public_id wt_role_id primary key
constraint iam_role_fkey
references iam_role(public_id)
on delete cascade
on update cascade,
scope_id wt_scope_id not null
constraint iam_scope_project_fkey
references iam_scope_project(scope_id)
on delete cascade
on update cascade,
name text,
description text,
grant_this_role_scope boolean not null default false,
version wt_version,
grant_this_role_scope_update_time wt_timestamp,
create_time wt_timestamp,
update_time wt_timestamp,
constraint iam_role_project_name_scope_id_uq
unique(name, scope_id)
);
comment on table iam_role_project is
'iam_role_project is a subtype table of the iam_role table. It is used to store roles that are scoped to a project.';
create table iam_grant_resource_enm (
name text primary key
constraint only_predefined_resource_types_allowed
check(
name in (
'*',
'alias',
'auth-method',
'auth-token',
'account',
'billing',
'controller',
'credential',
'credential-library',
'credential-store',
'group',
'host',
'host-catalog',
'host-set',
'managed-group',
'policy',
'role',
'scope',
'session',
'session-recording',
'storage-bucket',
'target',
'unknown',
'user',
'worker'
)
)
);
comment on table iam_grant_resource_enm is
'iam_grant_resource_enm is an enumeration table for resource types.';
-- wt_canonical_grant domain represents Boundary canonical grant.
-- A canonical grant is a semicolon-separated list of key=value pairs.
-- e.g. "ids=*;type=role;actions=read;output_fields=id,name"
create domain wt_canonical_grant as text
check(
value ~ '^(?:[^;=]+=[^;=]+)(?:;[^;=]+=[^;=]+)*?$'
);
comment on domain wt_canonical_grant is
'A canonical grant is a semicolon-separated list of key=value pairs.';
-- iam_grant is the root table for a grant value object.
-- A grant can only reference a single resource, including the special
-- strings "*" to indicate "all" resources, and "unknown" when no resource is set.
create table iam_grant (
canonical_grant wt_canonical_grant primary key,
resource text not null
constraint iam_grant_resource_enm_fkey
references iam_grant_resource_enm(name)
on delete restrict
on update cascade
);
comment on table iam_grant is
'iam_grant is the root table for a grant value object. A grant can only reference a single resource, including the special strings "*" to indicate "all" resources, and "unknown" when no resource is set.';
create index iam_grant_resource_ix
on iam_grant (resource);
commit;
@@ -0,0 +1,140 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create function insert_role_subtype() returns trigger
as $$
begin
insert into iam_role
(public_id, scope_id)
values
(new.public_id, new.scope_id);
return new;
end;
$$ language plpgsql;
comment on function insert_role_subtype() is
'insert_role_subtype is used to automatically insert a row into the iam_role table '
'whenever a row is inserted into the subtype table';
create function insert_grant_scope_update_time() returns trigger
as $$
begin
if new.grant_scope is distinct from old.grant_scope then
new.grant_scope_update_time = now();
end if;
return new;
end;
$$ language plpgsql;
comment on function insert_grant_scope_update_time() is
'insert_grant_scope_update_time is used to automatically update the grant_scope_update_time '
'of the subtype table whenever the grant_scope column is updated';
create function insert_grant_this_role_scope_update_time() returns trigger
as $$
begin
if new.grant_this_role_scope is distinct from old.grant_this_role_scope then
new.grant_this_role_scope_update_time = now();
end if;
return new;
end;
$$ language plpgsql;
comment on function insert_grant_this_role_scope_update_time() is
'insert_grant_this_role_scope_update_time is used to automatically update the grant_scope_update_time '
'of the subtype table whenever the grant_this_role_scope column is updated';
-- Add trigger to update the new update_time column on every iam_role subtype update.
create function update_iam_role_table_update_time() returns trigger
as $$
begin
update iam_role set update_time = new.update_time where public_id = new.public_id;
return new;
end;
$$ language plpgsql;
comment on function update_iam_role_table_update_time() is
'update_iam_role_table_update_time is used to automatically update the update_time '
'of the base table whenever one of the subtype iam_role tables are updated';
create function delete_iam_role_subtype() returns trigger
as $$
begin
delete
from iam_role
where public_id = old.public_id;
return null; -- result is ignored since this is an after trigger
end;
$$ language plpgsql;
comment on function delete_iam_role_subtype() is
'delete_iam_role_subtype is used to automatically delete associated iam_role entry'
'since domain implementation performs deletion on the child table which does not cleanup the base iam_role table ';
-- ensure the project's parent is the role's scope
create function ensure_project_belongs_to_role_org() returns trigger
as $$
begin
perform
from iam_scope_project
join iam_role_org
on iam_role_org.scope_id = iam_scope_project.parent_id
where iam_scope_project.scope_id = new.scope_id
and iam_role_org.public_id = new.role_id;
if not found then
raise exception 'project scope_id % not found in org', new.scope_id;
end if;
return new;
end;
$$ language plpgsql;
comment on function ensure_project_belongs_to_role_org() is
'ensure_project_belongs_to_role_org ensures the project belongs to the org of the role.';
-- set_resource sets the resource column based on the "type" token in the canonical_grant.
create function set_resource() returns trigger
as $$
declare type_matches text[];
begin
-- Extract all "type" tokens from the canonical_grant string
with
parts (p) as (
select p
from regexp_split_to_table(new.canonical_grant, ';') as p
),
kv (k, v) as (
select part[1] as k,
part[2] as v
from parts,
regexp_split_to_array(parts.p, '=') as part
)
select array_agg(v)
into type_matches
from kv
where k = 'type';
-- if there are multiple canonical grant types specified, throw an error.
-- Ensure that the canonical_grant type is only referencing a single resource
if type_matches is not null and array_length(type_matches, 1) > 1 then
raise exception 'multiple type tokens in grant. only one type expected: %', new.canonical_grant;
elsif type_matches is not null and array_length(type_matches, 1) = 1 then
new.resource := type_matches[1];
else
new.resource := 'unknown';
end if;
return new;
end
$$ language plpgsql;
comment on function set_resource() is
'set_resource sets the resource column based on the "type" token. A valid grant without a type token results in resource being set to "unknown".';
create function upsert_canonical_grant() returns trigger
as $$
begin
insert into iam_grant
(canonical_grant)
values
(new.canonical_grant)
on conflict do nothing;
return new;
end
$$ language plpgsql;
comment on function upsert_canonical_grant() is
'upsert_canonical_grant is a trigger function that inserts a row into the iam_grant table if the canonical_grant does not exist.';
commit;
@@ -0,0 +1,390 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- Insert the predefined grant scope types for iam_role_global
insert into iam_role_global_grant_scope_enm (name)
values
('descendants'),
('children'),
('individual');
-- Insert the predefined grant scope types for iam_role_org
insert into iam_role_org_grant_scope_enm (name)
values
('children'),
('individual');
-- Insert any global roles that have an associated grant scope
with aggregated_iam_role_grant_scope as (
select r.role_id,
json_agg (
json_build_object (
'create_time', r.create_time,
'scope_id_or_special', r.scope_id_or_special
)
order by r.create_time
) as grant_scope
from iam_role_grant_scope r
group by r.role_id
),
grant_scope_values as (
select a.role_id,
-- if scope_id_or_special is 'this' set grant_this_role_scope to true
-- if scope_id_or_special is 'global' set grant_this_role_scope to true
-- if neither are present set grant_this_role_scope to false
exists (
select 1
from jsonb_array_elements(a.grant_scope::jsonb) arr
where arr->>'scope_id_or_special' = 'this' or arr->>'scope_id_or_special' = 'global'
) as grant_this_role_scope,
-- if scope_id_or_special is 'descendants' set grant_scope to 'descendants'.
-- if scope_id_or_special is 'children' set grant_scope to 'children'.
-- if neither are present set grant_scope to 'individual' because
-- the value could be a scope_id in which case the grant_scope should be 'individual'.
-- if the value is 'this' then the grant_scope should be 'individual' as well
-- because the grant_scope column cannot be null.
case
when exists (
select 1
from jsonb_array_elements(a.grant_scope::jsonb) arr
where arr->>'scope_id_or_special' = 'descendants'
) then 'descendants'
when exists (
select 1
from jsonb_array_elements(a.grant_scope::jsonb) arr
where arr->>'scope_id_or_special' = 'children'
) then 'children'
else 'individual'
end as grant_scope,
-- there should only be one create_time for when scope_id_or_special is 'this'
-- so we can just pick the first create_time for this value
(select (x->>'create_time')::wt_timestamp
from jsonb_array_elements(a.grant_scope::jsonb) x
where x->>'scope_id_or_special' = 'this'
order by (x->>'create_time')::wt_timestamp
limit 1
) as grant_this_role_scope_update_time,
-- if scope_id_or_special is not 'this' then we can just pick the first create_time.
(select (x->>'create_time')::wt_timestamp
from jsonb_array_elements(a.grant_scope::jsonb) x
where x->>'scope_id_or_special' != 'this'
order by (x->>'create_time')::wt_timestamp
limit 1
) as grant_scope_update_time
from aggregated_iam_role_grant_scope a
)
insert into iam_role_global (
public_id,
scope_id,
name,
description,
version,
grant_this_role_scope,
grant_scope,
grant_this_role_scope_update_time,
grant_scope_update_time,
create_time,
update_time
)
select r.public_id,
r.scope_id,
r.name,
r.description,
r.version,
rs.grant_this_role_scope,
rs.grant_scope,
rs.grant_this_role_scope_update_time,
rs.grant_scope_update_time,
r.create_time,
r.update_time
from grant_scope_values rs
join iam_role as r
on rs.role_id = r.public_id
join iam_scope as s
on s.public_id = r.scope_id
where s.type = 'global';
-- Insert any remaining global roles that do not have an associated grant scope
-- These roles will have a grant_scope of 'individual' and grant_this_role_scope of false
insert into iam_role_global (
public_id,
scope_id,
name,
description,
version,
grant_this_role_scope,
grant_scope,
grant_this_role_scope_update_time,
grant_scope_update_time,
create_time,
update_time
)
select public_id,
scope_id,
name,
description,
version,
false,
'individual',
create_time,
create_time,
create_time,
update_time
from iam_role
where scope_id = 'global'
on conflict (public_id) do nothing;
-- Insert any org-level roles with the appropriate grant scopes
with aggregated_iam_role_grant_scope as (
select r.role_id,
json_agg (
json_build_object (
'create_time', r.create_time,
'scope_id_or_special', r.scope_id_or_special
)
order by r.create_time
) as grant_scope
from iam_role_grant_scope r
group by r.role_id
),
grant_scope_values as (
select a.role_id,
-- if scope_id_or_special is 'this' set grant_this_role_scope to true
-- if not, set grant_this_role_scope to false
exists (
select 1
from jsonb_array_elements(a.grant_scope::jsonb) arr
where arr->>'scope_id_or_special' = 'this'
) as grant_this_role_scope,
-- if scope_id_or_special is 'children' set grant_scope to 'children'.
-- if not present, set grant_scope to 'individual' because
-- the value could be a scope_id in which case the grant_scope should be 'individual'.
-- if the value is 'this' then the grant_scope should be 'individual' as well
-- because the grant_scope column cannot be null.
case
when exists (
select 1
from jsonb_array_elements(a.grant_scope::jsonb) arr
where arr->>'scope_id_or_special' = 'children'
) then 'children'
else 'individual'
end as grant_scope,
-- there should only be one create_time for when scope_id_or_special is 'this'
-- so we can just pick the first create_time for this value
(select (x->>'create_time')::wt_timestamp
from jsonb_array_elements(a.grant_scope::jsonb) x
where x->>'scope_id_or_special' = 'this'
order by (x->>'create_time')::wt_timestamp
limit 1
) as grant_this_role_scope_update_time,
-- if scope_id_or_special is not 'this' then we can just pick the first create_time.
(select (x->>'create_time')::wt_timestamp
from jsonb_array_elements(a.grant_scope::jsonb) x
where x->>'scope_id_or_special' != 'this'
order by (x->>'create_time')::wt_timestamp
limit 1
) as grant_scope_update_time
from aggregated_iam_role_grant_scope a
)
insert into iam_role_org (
public_id,
scope_id,
name,
description,
version,
grant_this_role_scope,
grant_scope,
grant_this_role_scope_update_time,
grant_scope_update_time,
create_time,
update_time
)
select r.public_id,
r.scope_id,
r.name,
r.description,
r.version,
rs.grant_this_role_scope,
rs.grant_scope,
rs.grant_this_role_scope_update_time,
rs.grant_scope_update_time,
r.create_time,
r.update_time
from grant_scope_values rs
join iam_role as r
on rs.role_id = r.public_id
join iam_scope as s
on s.public_id = r.scope_id
where s.type = 'org';
-- Insert any org roles that do not have an associated grant scope
-- These roles will have a grant_scope of 'individual' and grant_this_role_scope of false
insert into iam_role_org (
public_id,
scope_id,
name,
description,
version,
grant_this_role_scope,
grant_scope,
grant_this_role_scope_update_time,
grant_scope_update_time,
create_time,
update_time
)
select public_id,
scope_id,
name,
description,
version,
false,
'individual',
create_time,
create_time,
create_time,
update_time
from iam_role
where scope_id like 'o_%'
on conflict (public_id) do nothing;
-- Update the iam_role_org table to set the grant_this_role_scope
-- and grant_this_role_scope_update_time based on if they are granted 'this'
with grant_scope_this as (
select gs.role_id,
gs.create_time
from iam_role_grant_scope gs
join iam_role_org as o
on gs.role_id = o.public_id
where scope_id_or_special = o.scope_id
)
update iam_role_org
set grant_this_role_scope = true,
grant_this_role_scope_update_time = grant_scope_this.create_time
from grant_scope_this
where iam_role_org.public_id = grant_scope_this.role_id;
-- Migrate project roles (if they have a scope_id, they get migrated)
insert into iam_role_project (
public_id,
scope_id,
name,
description,
version,
grant_this_role_scope_update_time,
create_time,
update_time
)
select r.public_id,
r.scope_id,
r.name,
r.description,
r.version,
r.create_time,
r.create_time,
r.update_time
from iam_role r
join iam_scope as s
on s.public_id = r.scope_id
where s.type = 'project';
-- Update the iam_role_project table to set the grant_this_role_scope
-- and grant_this_role_scope_update_time based on if they are granted 'this'
with grant_scope_this as (
select gs.role_id,
gs.create_time,
gs.scope_id_or_special
from iam_role_grant_scope gs
join iam_role_project as p
on p.public_id = gs.role_id
)
update iam_role_project
set grant_this_role_scope = true,
grant_this_role_scope_update_time = grant_scope_this.create_time
from grant_scope_this
where iam_role_project.public_id = grant_scope_this.role_id;
-- Insert the predefined resource types
insert into iam_grant_resource_enm (name)
values
('*'),
('alias'),
('auth-method'),
('auth-token'),
('account'),
('billing'),
('controller'),
('credential'),
('credential-library'),
('credential-store'),
('group'),
('host'),
('host-catalog'),
('host-set'),
('managed-group'),
('policy'),
('role'),
('scope'),
('session'),
('session-recording'),
('storage-bucket'),
('target'),
('unknown'),
('user'),
('worker');
-- any global roles that are granted specific org scopes are migrated to its individual grant scope table
insert into iam_role_global_individual_org_grant_scope (
role_id,
grant_scope,
scope_id,
create_time
)
select rs.role_id,
r.grant_scope,
rs.scope_id_or_special,
rs.create_time
from iam_role_grant_scope as rs
join iam_role_global as r
on r.public_id = rs.role_id
where rs.scope_id_or_special like 'o_%';
-- any global roles that are granted specific project scopes are migrated to its individual grant scope table
insert into iam_role_global_individual_project_grant_scope (
role_id,
grant_scope,
scope_id,
create_time
)
select rs.role_id,
r.grant_scope,
rs.scope_id_or_special,
rs.create_time
from iam_role_grant_scope as rs
join iam_role_global as r
on r.public_id = rs.role_id
where rs.scope_id_or_special like 'p_%';
-- any org roles that are granted specific project scopes are migrated to its individual grant scope table
insert into iam_role_org_individual_grant_scope (
role_id,
grant_scope,
scope_id,
create_time
)
select rs.role_id,
r.grant_scope,
rs.scope_id_or_special,
rs.create_time
from iam_role_grant_scope as rs
join iam_role_org as r
on r.public_id = rs.role_id
where rs.scope_id_or_special like 'p_%';
insert into oplog_ticket (name, version)
values ('iam_role_global', 1),
('iam_role_org', 1),
('iam_role_project', 1);
commit;
@@ -0,0 +1,126 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
create trigger insert_role_subtype before insert on iam_role_global
for each row execute procedure insert_role_subtype();
create trigger insert_grant_scope_update_time before insert on iam_role_global
for each row execute procedure insert_grant_scope_update_time();
create trigger insert_grant_this_role_scope_update_time before insert on iam_role_global
for each row execute procedure insert_grant_this_role_scope_update_time();
create trigger update_iam_role_global_grant_scope_update_time before update on iam_role_global
for each row execute procedure insert_grant_scope_update_time();
create trigger update_iam_role_global_grant_this_role_scope_update_time before update on iam_role_global
for each row execute procedure insert_grant_this_role_scope_update_time();
create trigger update_iam_role_global_base_table_update_time after update on iam_role_global
for each row execute procedure update_iam_role_table_update_time();
create trigger delete_iam_role_subtype after delete on iam_role_global
for each row execute procedure delete_iam_role_subtype();
create trigger default_create_time_column before insert on iam_role_global
for each row execute procedure default_create_time();
create trigger update_time_column before update on iam_role_global
for each row execute procedure update_time_column();
create trigger update_version_column after update on iam_role_global
for each row execute procedure update_version_column();
create trigger immutable_columns before update on iam_role_global
for each row execute procedure immutable_columns('scope_id', 'create_time');
create trigger default_create_time_column before insert on iam_role_global_individual_org_grant_scope
for each row execute procedure default_create_time();
create trigger immutable_columns before update on iam_role_global_individual_org_grant_scope
for each row execute procedure immutable_columns('role_id', 'scope_id', 'grant_scope', 'create_time');
create trigger default_create_time_column before insert on iam_role_global_individual_project_grant_scope
for each row execute procedure default_create_time();
create trigger immutable_columns before update on iam_role_global_individual_project_grant_scope
for each row execute procedure immutable_columns('role_id', 'scope_id', 'create_time');
create trigger insert_role_subtype before insert on iam_role_org
for each row execute procedure insert_role_subtype();
create trigger insert_iam_role_org_grant_scope_update_time before insert on iam_role_org
for each row execute procedure insert_grant_scope_update_time();
create trigger insert_iam_role_org_grant_this_role_scope_update_time before insert on iam_role_org
for each row execute procedure insert_grant_this_role_scope_update_time();
create trigger update_iam_role_org_grant_scope_update_time before update on iam_role_org
for each row execute procedure insert_grant_scope_update_time();
create trigger update_iam_role_org_grant_this_role_scope_update_time before update on iam_role_org
for each row execute procedure insert_grant_this_role_scope_update_time();
create trigger update_iam_role_org_base_table_update_time after update on iam_role_org
for each row execute procedure update_iam_role_table_update_time();
create trigger delete_iam_role_subtype after delete on iam_role_org
for each row execute procedure delete_iam_role_subtype();
create trigger default_create_time_column before insert on iam_role_org
for each row execute procedure default_create_time();
create trigger update_time_column before update on iam_role_org
for each row execute procedure update_time_column();
create trigger update_version_column after update on iam_role_org
for each row execute procedure update_version_column();
create trigger immutable_columns before update on iam_role_org
for each row execute procedure immutable_columns('scope_id', 'create_time');
create trigger default_create_time_column before insert on iam_role_org_individual_grant_scope
for each row execute procedure default_create_time();
create trigger immutable_columns before update on iam_role_org_individual_grant_scope
for each row execute procedure immutable_columns('role_id', 'grant_scope', 'scope_id', 'create_time');
create trigger ensure_project_belongs_to_role_org before insert or update on iam_role_org_individual_grant_scope
for each row execute procedure ensure_project_belongs_to_role_org();
create trigger insert_role_subtype before insert on iam_role_project
for each row execute procedure insert_role_subtype();
create trigger default_create_time_column before insert on iam_role_project
for each row execute procedure default_create_time();
create trigger update_time_column before update on iam_role_project
for each row execute procedure update_time_column();
create trigger update_version_column after update on iam_role_project
for each row execute procedure update_version_column();
create trigger update_iam_role_project_base_table_update_time after update on iam_role_project
for each row execute procedure update_iam_role_table_update_time();
create trigger delete_iam_role_subtype after delete on iam_role_project
for each row execute procedure delete_iam_role_subtype();
create trigger insert_iam_role_project_grant_this_role_scope_update_time before insert on iam_role_project
for each row execute procedure insert_grant_this_role_scope_update_time();
create trigger update_iam_role_project_grant_this_role_scope_update_time before update on iam_role_project
for each row execute procedure insert_grant_this_role_scope_update_time();
create trigger immutable_columns before update on iam_role_project
for each row execute procedure immutable_columns('scope_id', 'create_time');
create trigger set_resource before insert on iam_grant
for each row execute procedure set_resource();
create trigger upsert_canonical_grant before insert on iam_role_grant
for each row execute procedure upsert_canonical_grant();
commit;
@@ -0,0 +1,32 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
-- for the following foreign key constraint to work,
-- we need to ensure that all canonical_grant values in iam_role_grant exist in iam_grant.
insert into iam_grant (canonical_grant)
select canonical_grant
from iam_role_grant
on conflict do nothing;
-- Add a foreign key constraint to the iam_role_grant table to ensure that the canonical_grant exists in the iam_grant table.
-- Alter to add foreign key constraint to the iam_role_grant table defined in 01/06_iam.up.sql
alter table iam_role_grant
add constraint iam_grant_fkey
foreign key (canonical_grant)
references iam_grant(canonical_grant)
on delete cascade
on update cascade;
-- remove iam_role_grant_scope and all cross-table dependencies
drop table iam_role_grant_scope cascade;
drop trigger cascade_deletion_iam_scope_to_iam_role_grant_scope on iam_scope;
-- remove name, description, version from iam_role
alter table iam_role
drop column name,
drop column description,
drop column version;
commit;
@@ -0,0 +1,200 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oss_test
import (
"context"
"testing"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/common"
"github.com/hashicorp/boundary/internal/db/schema"
"github.com/hashicorp/boundary/internal/db/schema/migration"
"github.com/hashicorp/boundary/internal/db/schema/migrations/oss/internal/hook97001"
"github.com/hashicorp/boundary/testing/dbtest"
"github.com/stretchr/testify/require"
)
// cases to look at [NA = not-applicable | P = pass | F = fail] - 'FindProblem' query should find all `F` cases
// [F] Global Role (r_globala_97001) - Grant This, Descendants, individual org, and individual project
// - need to remove individual org
// - need to remove individual proj
//
// [F] Global Role (r_globalb_97001) - Grant This, Children, individual org, and individual project
// - need to remove individual org
//
// [P] Global Role (r_globalc_97001) - Grant role's self scope ID
// - switch to `this` automagically
//
// [F] Org Role (r_orgaa___97001) - Grant This, Children, and individual project
// - need to remove individual project
//
// [P] Org Role (r_orgab___97001) - Grant role self scope ID
// - switch to `this` automagically
//
// [P] Project Role (r_prjaa___97001) - Grant self scope ID
// - switch to `this` automagically
//
// [NA] Org Role - Grant Children AND individual org (different org)
// - this cannot be created
//
// [NA] Org Role - Grant Children AND individual project (from different org)
// - this cannot be created
//
// [NA] Project Role (p_pA___97001) - Grant individual project (different proj)
// - this cannot be created
//
// [NA] Global Role - Grant Descendants and Children
// - this cannot be created
func TestMigrationHook97001(t *testing.T) {
const (
priorMigration = 96001
)
dialect := dbtest.Postgres
ctx := context.Background()
c, u, _, err := dbtest.StartUsingTemplate(dialect, dbtest.WithTemplate(dbtest.Template1))
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := common.SqlOpen(dialect, u)
require.NoError(t, err)
// migration to the prior migration (before the one we want to test)
m, err := schema.NewManager(ctx, schema.Dialect(dialect), d, schema.WithEditions(
schema.TestCreatePartialEditions(schema.Dialect(dialect), schema.PartialEditions{"oss": priorMigration}),
))
require.NoError(t, err)
_, err = m.ApplyMigrations(ctx)
require.NoError(t, err)
state, err := m.CurrentState(ctx)
require.NoError(t, err)
want := &schema.State{
Initialized: true,
Editions: []schema.EditionState{
{
Name: "oss",
BinarySchemaVersion: priorMigration,
DatabaseSchemaVersion: priorMigration,
DatabaseSchemaState: schema.Equal,
},
},
}
require.Equal(t, want, state)
dbType, err := db.StringToDbType(dialect)
require.NoError(t, err)
conn, err := db.Open(ctx, dbType, u)
require.NoError(t, err)
rw := db.New(conn)
t.Helper()
query := `
insert into iam_scope
(parent_id, type, public_id, name)
values
('global', 'org', 'o_ta___97001', 'Org A Testing Invalid Role Grant Scope Associations'),
('global', 'org', 'o_tb___97001', 'Org B Testing Invalid Role Grant Scope Associations'),
('o_ta___97001', 'project', 'p_pA___97001', 'testing 97001 Project A'),
('o_ta___97001', 'project', 'p_pB___97001', 'testing 97001 Project B'),
('o_tb___97001', 'project', 'p_pC___97001', 'testing 97001 Project C')`
_, err = rw.Exec(ctx, query, nil)
require.NoError(t, err)
query = `
insert into iam_role
(public_id, scope_id, name )
values
('r_globala_97001', 'global', 'testing 97001 global role A'),
('r_globalb_97001', 'global', 'testing 97001 global role B'),
('r_globalc_97001', 'global', 'testing 97001 global role C'),
('r_globald_97001', 'global', 'testing 97001 global role D'),
('r_globale_97001', 'global', 'testing 97001 global role E'),
('r_globalf_97001', 'global', 'testing 97001 global role f'),
('r_orgaa___97001', 'o_ta___97001', 'testing 97001 org A role A'),
('r_orgab___97001', 'o_ta___97001', 'testing 97001 org A role B'),
('r_prjaa___97001', 'p_pA___97001', 'testing 97001 proj A role A'),
('r_prjab___97001', 'p_pA___97001', 'testing 97001 proj A role B')
`
_, err = rw.Exec(ctx, query, nil)
require.NoError(t, err)
query = `
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
-- Global Role (r_globala_97001) - Grant This, Descendants, individual org, and individual project
('r_globala_97001', 'this'),
('r_globala_97001', 'descendants'),
('r_globala_97001', 'o_ta___97001'),
('r_globala_97001', 'p_pA___97001'),
-- Global Role (r_globalb_97001) - Grant This, Children, individual org, and individual project
('r_globalb_97001', 'this'),
('r_globalb_97001', 'children'),
('r_globalb_97001', 'o_ta___97001'),
('r_globalb_97001', 'p_pA___97001'),
-- Org Role (r_orgaa___97001) - Grant This, Children, and individual project
('r_orgaa___97001', 'this'),
('r_orgaa___97001', 'children'),
('r_orgaa___97001', 'p_pA___97001'),
-- below this line are non-problematic roles that should not show up in the final list
-- Global Role (r_globalc_97001) - Grant role's self scope ID
('r_globalc_97001', 'global'),
-- Global Role (r_globald_97001) - Grant children and project
('r_globald_97001', 'children'),
('r_globald_97001', 'p_pA___97001'),
-- Global Role (r_globale_97001) - Grant this and descendants
('r_globale_97001', 'this'),
('r_globale_97001', 'descendants'),
-- Global Role (r_globalf_97001) - Grant this and children
('r_globalf_97001', 'this'),
('r_globalf_97001', 'children'),
-- Org Role (r_orgab___97001) - Grant role self scope ID
('r_orgab___97001', 'o_ta___97001'),
-- Project Role (p_pA___97001) - Grant self scope ID
('r_prjaa___97001', 'p_pA___97001')
`
_, err = rw.Exec(ctx, query, nil)
require.NoError(t, err)
tx, err := d.BeginTx(ctx, nil)
require.NoError(t, err)
// Run hook check
checkReport, err := hook97001.FindInvalidAssociations(ctx, tx)
require.NoError(t, err)
// Run hook repair
repairReport, err := hook97001.RepairInvalidAssociations(ctx, tx)
require.NoError(t, err)
err = tx.Commit()
require.NoError(t, err)
hook97001validateCheckFunc(t, checkReport)
hook97001validateRepairFunc(t, repairReport)
}
func hook97001validateCheckFunc(t *testing.T, checkReport migration.Problems) {
t.Helper()
require := require.New(t)
require.ElementsMatch(checkReport, migration.Problems{
"Role 'r_globala_97001' in scope 'global' has the 'descendants' grant scope which covers 'o_ta___97001'",
"Role 'r_globala_97001' in scope 'global' has the 'descendants' grant scope which covers 'p_pA___97001'",
"Role 'r_globalb_97001' in scope 'global' has the 'children' grant scope which covers 'o_ta___97001'",
"Role 'r_orgaa___97001' in scope 'o_ta___97001' has the 'children' grant scope which covers 'p_pA___97001'",
})
}
func hook97001validateRepairFunc(t *testing.T, checkReport migration.Repairs) {
t.Helper()
require := require.New(t)
require.ElementsMatch(checkReport, migration.Problems{
"Remove redundant grant scopes 'o_ta___97001' association from role 'r_globala_97001' in scope 'global' because it overlaps with 'descendants'",
"Remove redundant grant scopes 'p_pA___97001' association from role 'r_globala_97001' in scope 'global' because it overlaps with 'descendants'",
"Remove redundant grant scopes 'o_ta___97001' association from role 'r_globalb_97001' in scope 'global' because it overlaps with 'children'",
"Remove redundant grant scopes 'p_pA___97001' association from role 'r_orgaa___97001' in scope 'o_ta___97001' because it overlaps with 'children'",
})
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+19 -18
View File
@@ -17,28 +17,29 @@ endif
PROVE_OPTS ?=
TESTS ?= tests/setup/*.sql \
tests/org/*.sql \
tests/wh/*/*.sql \
tests/sentinel/*.sql \
tests/credential/*/*.sql \
tests/session/*.sql \
tests/account/*/*.sql \
tests/target/*.sql \
tests/controller/*.sql \
tests/hcp/*/*.sql \
tests/census/*.sql \
tests/kms/*.sql \
tests/storage/*.sql \
tests/domain/*.sql \
tests/history/*.sql \
tests/recording/*.sql \
tests/alias/*.sql \
tests/auth/*/*.sql \
tests/purge/*.sql \
tests/census/*.sql \
tests/controller/*.sql \
tests/credential/*/*.sql \
tests/domain/*.sql \
tests/hcp/*/*.sql \
tests/history/*.sql \
tests/host/*.sql \
tests/iam/*.sql \
tests/kms/*.sql \
tests/org/*.sql \
tests/pagination/*.sql \
tests/policy/*.sql \
tests/host/*.sql \
tests/server/*.sql
tests/purge/*.sql \
tests/recording/*.sql \
tests/sentinel/*.sql \
tests/server/*.sql \
tests/session/*.sql \
tests/storage/*.sql \
tests/target/*.sql \
tests/wh/*/*.sql \
POSTGRES_DOCKER_IMAGE_BASE ?= postgres
@@ -116,4 +117,4 @@ clean:
docker stop $(SQL_TEST_CONTAINER_NAME) || true
docker rm -v $(SQL_TEST_CONTAINER_NAME) || true
.PHONY: all clean test database-up run-tests
.PHONY: all clean test database-up run-tests
@@ -105,49 +105,62 @@ begin;
('g___cr-group', 'u______carly'),
('g___cg-group', 'u_______cora');
insert into iam_role
(scope_id, public_id, name)
insert into iam_role_global
(scope_id, public_id, name, grant_this_role_scope, grant_scope)
values
('p____bcolors', 'r_pp_bc__mix', 'Color Mixer'),
('p____rcolors', 'r_pp_rc__mix', 'Color Mixer'),
('p____gcolors', 'r_pp_gc__mix', 'Color Mixer'),
('o_____colors', 'r_op_bc__art', 'Blue Color Artist'),
('o_____colors', 'r_op_rc__art', 'Red Color Artist'),
('o_____colors', 'r_op_gc__art', 'Green Color Artist'),
('o_____colors', 'r_oo_____art', 'Color Artist'),
('global', 'r_go____name', 'Color Namer'),
('global', 'r_gp____spec', 'Blue Color Inspector'),
('global', 'r_gg_____buy', 'Purchaser'),
('global', 'r_gg____shop', 'Shopper');
('global', 'r_go____name', 'Color Namer', false, 'individual'),
('global', 'r_gp____spec', 'Blue Color Inspector', false, 'individual'),
('global', 'r_gg_____buy', 'Purchaser', true, 'individual'),
('global', 'r_gg____shop', 'Shopper', true, 'individual');
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
insert into iam_role_global_individual_org_grant_scope
(role_id, scope_id, grant_scope)
values
('r_pp_bc__mix', 'this'),
('r_pp_rc__mix', 'p____rcolors'),
('r_pp_gc__mix', 'this'),
('r_op_bc__art', 'p____bcolors'),
('r_op_rc__art', 'p____rcolors'),
('r_op_gc__art', 'p____gcolors'),
('r_go____name', 'o_____colors'),
('r_gp____spec', 'p____bcolors'),
('r_gg_____buy', 'global'),
('r_gg____shop', 'global');
('r_go____name', 'o_____colors', 'individual');
insert into iam_role_global_individual_project_grant_scope
(role_id, scope_id, grant_scope)
values
('r_gp____spec', 'p____bcolors', 'individual');
insert into iam_role_org
(scope_id, public_id, name, grant_this_role_scope, grant_scope)
values
('o_____colors', 'r_op_bc__art', 'Blue Color Artist', false, 'individual'),
('o_____colors', 'r_op_rc__art', 'Red Color Artist', false, 'individual'),
('o_____colors', 'r_op_gc__art', 'Green Color Artist', false, 'individual'),
('o_____colors', 'r_oo_____art', 'Color Artist', false, 'individual');
insert into iam_role_org_individual_grant_scope
(role_id, scope_id, grant_scope)
values
('r_op_bc__art', 'p____bcolors', 'individual'),
('r_op_rc__art', 'p____rcolors', 'individual'),
('r_op_gc__art', 'p____gcolors', 'individual');
insert into iam_role_project
(scope_id, public_id, name, grant_this_role_scope)
values
('p____bcolors', 'r_pp_bc__mix', 'Color Mixer', true),
('p____rcolors', 'r_pp_rc__mix', 'Color Mixer', true),
('p____gcolors', 'r_pp_gc__mix', 'Color Mixer', true);
insert into iam_role_grant
(role_id, canonical_grant, raw_grant)
(role_id, canonical_grant, raw_grant)
values
('r_gg_____buy', 'type=*;action=purchase', 'purchase anything'),
('r_gg____shop', 'type=*;action=view', 'view anything'),
('r_go____name', 'type=color;action=name', 'name colors'),
('r_gp____spec', 'type=color;action=inspect', 'inspect colors'),
('r_oo_____art', 'type=color;action=create', 'create color'),
('r_op_bc__art', 'type=color;action=create', 'create color'),
('r_op_rc__art', 'type=color;action=create', 'create color'),
('r_op_gc__art', 'type=color;action=create', 'create color'),
('r_pp_bc__mix', 'type=color;action=mix', 'mix color'),
('r_pp_rc__mix', 'type=color;action=mix', 'mix color'),
('r_pp_gc__mix', 'type=color;action=mix', 'mix color');
('r_gg_____buy', 'ids=*;type=*;actions=update', 'ids=*;type=*;actions=update'),
('r_gg____shop', 'ids=*;type=*;actions=read;output_fields=id', 'ids=*;type=*;actions=read;output_fields=id'),
('r_go____name', 'ids=*;type=group;actions=create,update,read,list', 'ids=*;type=group;actions=create,update,read,'),
('r_gp____spec', 'ids=*;type=group;actions=delete', 'ids=*;type=group;actions=delete'),
('r_oo_____art', 'ids=*;type=group;actions=create', 'ids=*;type=group;actions=create'),
('r_op_bc__art', 'ids=*;type=auth-token;actions=create', 'ids=*;type=auth-token;actions=create'),
('r_op_rc__art', 'ids=*;type=target;actions=create', 'ids=*;type=targets;actions=create'),
('r_op_gc__art', 'ids=*;type=auth-method;actions=authenticate', 'ids=*;type=auth-method;actions=create'),
('r_pp_bc__mix', 'ids=*;type=group;actions=add-members', 'ids=*;type=group;actions=add-members'),
('r_pp_rc__mix', 'ids=*;type=group;actions=set-members', 'ids=*;type=group;actions=set-members'),
('r_pp_gc__mix', 'ids=*;type=group;actions=delete-members', 'ids=*;type=group;actions=delete-members');
insert into iam_group_role
(role_id, principal_id)
@@ -54,35 +54,32 @@ begin;
('g___wb-group', 'u_____warren'),
('g___ws-group', 'u_____waylon');
insert into iam_role
(scope_id, public_id, name)
insert into iam_role_org
(scope_id, public_id, name, grant_scope, grant_this_role_scope)
values
-- ('global', 'r_gg_____buy', 'Purchaser'),
-- ('global', 'r_gg____shop', 'Shopper'),
('p____bwidget', 'r_pp_bw__bld', 'Widget Builder'),
('p____swidget', 'r_pp_sw__bld', 'Widget Builder'),
('o_____widget', 'r_op_sw__eng', 'Small Widget Engineer'),
('o_____widget', 'r_oo_____eng', 'Widget Engineer');
('o_____widget', 'r_op_sw__eng', 'Small Widget Engineer', 'individual', false),
('o_____widget', 'r_oo_____eng', 'Widget Engineer', 'individual', true);
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
insert into iam_role_org_individual_grant_scope
(role_id, scope_id, grant_scope)
values
('r_pp_bw__bld', 'p____bwidget'),
('r_pp_sw__bld', 'this'),
('r_op_sw__eng', 'p____swidget'),
('r_oo_____eng', 'o_____widget');
('r_op_sw__eng','p____swidget','individual');
insert into iam_role_project
(scope_id, public_id, name, grant_this_role_scope)
values
('p____bwidget', 'r_pp_bw__bld', 'Widget Builder', true),
('p____swidget', 'r_pp_sw__bld', 'Widget Builder', true);
insert into iam_role_grant
(role_id, canonical_grant, raw_grant)
values
-- ('r_gg_____buy', 'type=*;action=purchase', 'purchase anything'),
-- ('r_gg____shop', 'type=*;action=view', 'view anything'),
('r_oo_____eng', 'type=widget;action=design', 'design widget'),
('r_op_sw__eng', 'type=widget;action=design', 'design widget'),
('r_op_sw__eng', 'type=widget;action=tune', 'tune widget'),
('r_op_sw__eng', 'type=widget;action=clean', 'clean widget'),
('r_pp_bw__bld', 'type=widget;action=build', 'build widget'),
('r_pp_sw__bld', 'type=widget;action=build', 'build widget');
('r_oo_____eng', 'ids=*;type=alias;actions=create,update', 'ids=*;type=alias;actions=create,update'),
('r_op_sw__eng', 'ids=*;type=target;actions=add-credential-sources,remove-credential-sources,set-credential-sources', 'ids=*;type=target;actions=add-credential-sources,remove-credential-sources,set-credential-source'),
('r_op_sw__eng', 'ids=*;type=target;actions=add-host-sources,remove-host-sources,set-host-sources', 'ids=*;type=target;actions=add-host-sources,remove-host-sources,set-host-sources'),
('r_op_sw__eng', 'ids=*;type=host-catalog;actions=read,list', 'ids=*;type=host-catalog;actions=read,list'),
('r_pp_bw__bld', 'ids=*;type=credential-library;actions=create,delete', 'ids=*;type=credential-library;actions=create,delete'),
('r_pp_sw__bld', 'ids=*;type=scope;actions=no-op,list', 'ids=*;type=scope;actions=no-op,list');
insert into iam_group_role
(role_id, principal_id)
@@ -106,15 +106,10 @@ begin;
('kdkv___foodtruck', 'apa______bob', 'tok______bob', 'tok______bob'::bytea);
-- Roles
insert into iam_role
(scope_id, public_id, name)
insert into iam_role_project
(scope_id, public_id, name, grant_this_role_scope)
values
('p______tacos', 'r_pp___tacos', 'Tacos');
insert into iam_role_grant_scope
(role_id, scope_id_or_special )
values
('r_pp___tacos', 'p______tacos');
('p______tacos', 'r_pp___tacos', 'Tacos', true);
insert into iam_role_grant
(role_id, canonical_grant, raw_grant)
@@ -1,278 +0,0 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(27);
-- Remove any existing roles, which should also remove existing grant scopes
delete from iam_role;
-- Set up a series of scopes to test against
insert into iam_scope
(type, public_id, parent_id)
values
('org', 'o_1111111111', 'global'),
('project', 'p_111111111a', 'o_1111111111'),
('project', 'p_111111111b', 'o_1111111111'),
('org', 'o_2222222222', 'global'),
('project', 'p_222222222a', 'o_2222222222'),
('project', 'p_222222222b', 'o_2222222222');
-- Insert a role at each scope
insert into iam_role
(public_id, scope_id) values
('r_globeglobe', 'global'),
('r_1111111111', 'o_1111111111'),
('r_111111111a', 'p_111111111a'),
('r_111111111b', 'p_111111111b'),
('r_2222222222', 'o_2222222222'),
('r_222222222a', 'p_222222222a'),
('r_222222222b', 'p_222222222b');
-- Start validation
-- Reset grant scopes
delete from iam_role_grant_scope;
-- Case 1a: insert the scope's own grant scope on global, org, proj
prepare insert_own_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'global');
select lives_ok('insert_own_global');
prepare insert_own_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'o_1111111111');
select lives_ok('insert_own_org');
prepare insert_own_project as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'p_111111111a');
select lives_ok('insert_own_project');
-- Case 1b: with the current inserted values, ensure we can't insert "this"
prepare insert_this_fail_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'this');
select throws_like(
'insert_this_fail_global',
'invalid to specify both a role''s actual scope id and "this" as a grant scope'
);
prepare insert_this_fail_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'this');
select throws_like(
'insert_this_fail_org',
'invalid to specify both a role''s actual scope id and "this" as a grant scope'
);
prepare insert_this_fail_project as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'this');
select throws_like(
'insert_this_fail_project',
'invalid to specify both a role''s actual scope id and "this" as a grant scope'
);
-- Case 1c: Remove same-scope values, insert "this" for each
delete from iam_role_grant_scope;
prepare insert_this_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'this');
select lives_ok('insert_this_global');
prepare insert_this_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'this');
select lives_ok('insert_this_org');
prepare insert_this_project as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'this');
select lives_ok('insert_this_project');
-- Case 1d: Make sure with "this" that we can't insert same-scope values
prepare insert_own_fail_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'global');
select throws_like(
'insert_own_fail_global',
'invalid to specify both "this" and a role''s actual scope id as a grant scope'
);
prepare insert_own_fail_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'o_1111111111');
select throws_like(
'insert_own_fail_org',
'invalid to specify both "this" and a role''s actual scope id as a grant scope'
);
prepare insert_own_fail_project as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'p_111111111a');
select throws_like(
'insert_own_fail_project',
'invalid to specify both "this" and a role''s actual scope id as a grant scope'
);
-- Reset grant scopes
delete from iam_role_grant_scope;
-- Case 2a: Bare scopes that are not the role's scope succeed if descendant
prepare insert_bare_org_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'o_1111111111');
select lives_ok('insert_bare_org_global');
prepare insert_bare_proj_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'p_111111111a');
select lives_ok('insert_bare_proj_global');
prepare insert_bare_proj_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'p_111111111a');
select lives_ok('insert_bare_proj_org');
-- Case 2b: Bare scopes that are not the role's scope fail if not descendant
prepare insert_bare_org_fail_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'global');
select throws_like(
'insert_bare_org_fail_global',
'expected grant scope id scope type to be project'
);
prepare insert_bare_proj_fail_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'global');
select throws_like(
'insert_bare_proj_fail_global',
'invalid to set a grant scope ID to non-same scope_id_or_special when role scope type is project'
);
prepare insert_bare_proj_fail_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'o_1111111111');
select throws_like(
'insert_bare_proj_fail_org',
'invalid to set a grant scope ID to non-same scope_id_or_special when role scope type is project'
);
prepare insert_bare_proj_wrong_scope_fail_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'p_222222222a');
select throws_like(
'insert_bare_proj_wrong_scope_fail_org',
'grant scope id is not a child project of the role''s org scope'
);
-- Reset grant scopes
delete from iam_role_grant_scope;
-- Case 3: "children" is only allowed in global/org
prepare insert_children_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'children');
select lives_ok('insert_children_global');
prepare insert_children_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'children');
select lives_ok('insert_children_org');
prepare insert_children_fail_proj as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'children');
select throws_like(
'insert_children_fail_proj',
'invalid to set a grant scope ID to non-same scope_id_or_special when role scope type is project'
);
-- Reset grant scopes
delete from iam_role_grant_scope;
-- Case 4: "descendants" is only allowed in global
prepare insert_descendants_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'descendants');
select lives_ok('insert_descendants_global');
prepare insert_descendants_fail_org as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_1111111111', 'descendants');
select throws_like(
'insert_descendants_fail_org',
'invalid to specify "descendants" as a grant scope when the role''s scope ID is not "global"'
);
prepare insert_descendants_fail_proj as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_111111111a', 'descendants');
select throws_like(
'insert_descendants_fail_proj',
'invalid to set a grant scope ID to non-same scope_id_or_special when role scope type is project'
);
-- Reset grant scopes
delete from iam_role_grant_scope;
-- Case 5: "descendants" and "children" are mutually exclusive
prepare insert_descendants_and_children_fail_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'descendants'),
('r_globeglobe', 'children');
select throws_like(
'insert_descendants_and_children_fail_global',
'invalid to specify both "descendants" and "children" as a grant scope'
);
prepare insert_children_and_descendants_fail_global as
insert into iam_role_grant_scope
(role_id, scope_id_or_special)
values
('r_globeglobe', 'children'),
('r_globeglobe', 'descendants');
select throws_like(
'insert_children_and_descendants_fail_global',
'invalid to specify both "children" and "descendants" as a grant scope'
);
select * from finish();
rollback;
+303
View File
@@ -0,0 +1,303 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(29);
select wtt_load('widgets', 'iam');
-- insert canonical_grant with valid resource
-- validate the resource is set to 'scope'
prepare insert_grant_scope as
insert into iam_grant
(canonical_grant)
values
('type=scope;others=stuff');
select lives_ok('insert_grant_scope');
select is(
(select resource
from iam_grant
where canonical_grant = 'type=scope;others=stuff'),
'scope',
'resource should be set to "scope" by set_resource() trigger'
);
-- insert invalid canonical_grant which does not match the wt_canonical_grant domain
-- the insert should fail because the canonical_grant is malformed
prepare insert_malformed_grant as
insert into iam_grant
(canonical_grant)
values
('no_type_at_all');
select throws_like(
'insert_malformed_grant',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a grant that is malformed should fail'
);
-- insert invalid canonical_grant with trailing semicolon
-- the insert should fail because the the canonical_grant has a trailing semicolon
prepare insert_grant_with_trailing_semicolon as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=role;actions=*;');
select throws_like(
'insert_grant_with_trailing_semicolon',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a grant with trailing semicolon should fail'
);
-- insert canonical_grant with type=role,group
-- the insert should fail because the resource is not a single value
prepare insert_grant_role as
insert into iam_grant
(canonical_grant)
values
('type=role,group;foo=bar');
select throws_like(
'insert_grant_role',
'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"',
'inserting a resource not in iam_grant_resource_enm should fail because type is not a single value'
);
-- the set_resource() trigger will set resource='bogus', but we did not insert 'bogus'
-- into resource_enm, so it should fail.
prepare insert_grant_bogus as
insert into iam_grant
(canonical_grant)
values
('type=bogus;some=thing');
select throws_like(
'insert_grant_bogus',
'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"',
'inserting a resource not in iam_grant_resource_enm should fail'
);
-- the set_resource() trigger will set resource='bogus', but we did not insert 'bogus'
-- into resource_enm, so it should fail.
prepare insert_grant_bogus_with_action as
insert into iam_grant
(canonical_grant)
values
('type=bogus;actions=create');
select throws_like(
'insert_grant_bogus_with_action',
'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"',
'inserting a resource not in iam_grant_resource_enm should fail'
);
-- insert a duplicate canonical_grant
-- validate that the primary key constraint is enforced
prepare insert_dup_grant_1 as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=credential-library;actions=create');
select lives_ok('insert_dup_grant_1');
prepare insert_dup_grant_2 as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=credential-library;actions=create');
select throws_like(
'insert_dup_grant_2',
'duplicate key value violates unique constraint "iam_grant_pkey"',
'primary key (canonical_grant) is enforced'
);
-- insert a canonical grant string with wildcards for id, type, actions, and output_fields
-- validate that the resource is set to '*'
prepare insert_grant_wildcard as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=*;actions=*;output_fields=*');
select lives_ok('insert_grant_wildcard');
select is(
(select resource
from iam_grant
where canonical_grant = 'ids=*;type=*;actions=*;output_fields=*'),
'*',
'resource should be set to "*" if type is "*"'
);
-- insert a canonical grant string with single action, single id, single output_field and type=host
-- validate that the resource is set to 'host'
prepare insert_grant_single_action as
insert into iam_grant
(canonical_grant)
values
('ids=o_1234;type=host;actions=create;output_fields=id');
select lives_ok('insert_grant_single_action');
select is(
(select resource
from iam_grant
where canonical_grant = 'ids=o_1234;type=host;actions=create;output_fields=id'),
'host',
'resource should be set to "host" if type is "host"'
);
-- insert a canonical grant string with type=group, multiple actions, single type and multiple output_fields
-- validate that the resource is set to 'group'
prepare insert_grant_role_multiple_actions as
insert into iam_grant
(canonical_grant)
values
('ids=o_1234,o_4567;type=group;actions=create,update;output_fields=id,name');
select lives_ok('insert_grant_role_multiple_actions');
select is(
(select resource
from iam_grant
where canonical_grant = 'ids=o_1234,o_4567;type=group;actions=create,update;output_fields=id,name'),
'group',
'resource should be set to "group" if type is "group"'
);
-- insert a canonical grant string with with multiple types
-- the insert should fail because the resource is not a single value
prepare insert_grant_multiple_types as
insert into iam_grant
(canonical_grant)
values
('ids=o_1234,o_4567;type=target,role,group;actions=create,update;output_fields=id,name');
select throws_like(
'insert_grant_multiple_types',
'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"',
'inserting a resource not in iam_grant_resource_enm should fail because type is not a single value'
);
-- insert a canonical grant string with type with dash
-- validate that the resource is set to 'credential-library'
prepare insert_grant_with_dash as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=credential-library;actions=create;output_fields=id');
select lives_ok('insert_grant_with_dash');
select is(
(select resource
from iam_grant
where canonical_grant = 'ids=*;type=credential-library;actions=create;output_fields=id'),
'credential-library',
'resource should be set to "credential-library" if type is "credential-library"'
);
-- insert a canonical grant string with type with underscore
-- the insert should fail because a resource with underscore is not in the resource_enm table
prepare insert_grant_with_underscore as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=credential_library;actions=create;output_fields=id');
select throws_like(
'insert_grant_with_underscore',
'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"',
'inserting a a resource with underscore should fail'
);
-- insert a canonical grant string with type malformed with no semicolon
-- the insert should fail because the type is malformed
prepare insert_grant_malformed_type_with_no_semicolon as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=credential-library actions=create;output_fields=id');
select throws_like(
'insert_grant_malformed_type_with_no_semicolon',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a resource with a malformed type should fail'
);
-- insert a canonical grant string with type malformed with no equals sign
prepare insert_grant_malformed_type_with_no_equals as
insert into iam_grant
(canonical_grant)
values
('ids=*;type;actions=create;output_fields=id');
select throws_like(
'insert_grant_malformed_type_with_no_equals',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a resource with a malformed type should fail'
);
-- insert a canonical grant string with type malformed with no value
prepare insert_grant_malformed_type_with_no_value as
insert into iam_grant
(canonical_grant)
values
('ids=*;type=;actions=create;output_fields=id');
select throws_like(
'insert_grant_malformed_type_with_no_value',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a resource with a malformed type should fail'
);
-- insert a canonical grant string with with no type
prepare insert_grant_malformed_type_with_no_type as
insert into iam_grant
(canonical_grant)
values
('ids=*;actions=create;output_fields=id');
select lives_ok('insert_grant_malformed_type_with_no_type');
select is(
(select resource
from iam_grant
where canonical_grant = 'ids=*;actions=create;output_fields=id'),
'unknown',
'resource should default to "unknown" if the type is not found'
);
-- insert a canonical grant string with type malformed with double ids semicolon
prepare insert_grant_malformed_type_with_double_ids_semicolon as
insert into iam_grant
(canonical_grant)
values
('ids=*;;type=credential-library;actions=create;output_fields=id');
select throws_like(
'insert_grant_malformed_type_with_double_ids_semicolon',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a resource with a malformed type should fail'
);
-- insert a canonical grant string with type at the end of the string
prepare insert_grant_with_type_at_the_end as
insert into iam_grant
(canonical_grant)
values
('ids=*;actions=create;output_fields=id;type=credential-library');
select lives_ok('insert_grant_with_type_at_the_end');
select is(
(select resource
from iam_grant
where canonical_grant = 'ids=*;actions=create;output_fields=id;type=credential-library'),
'credential-library',
'resource should be set to "credential-library" if type is "credential-library"'
);
-- insert a canonical grant string with type malformed with semicolon after type
prepare insert_grant_malformed_type_with_semicolon_after_type as
insert into iam_grant
(canonical_grant)
values
('ids=*;type;=credential-library;actions=create;output_fields=id;');
select throws_like(
'insert_grant_malformed_type_with_semicolon_after_type',
'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"',
'inserting a resource with a malformed type should fail'
);
-- insert a canonical grant string with multiple type tokens
-- the insert should fail because there are multiple type tokens
prepare insert_grant_multiple_type_specified as
insert into iam_grant
(canonical_grant)
values
('ids=o_1234,o_4567;type=target;type=session;actions=create,update;output_fields=id,name');
select throws_like(
'insert_grant_multiple_type_specified',
'multiple type tokens in grant. only one type expected: ids=o_1234,o_4567;type=target;type=session;actions=create,update;output_fields=id,name',
'inserting a resource with multiple type tokens should fail'
);
select * from finish();
rollback;
@@ -0,0 +1,425 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(44);
select wtt_load('widgets', 'iam');
--------------------------------------------------------------------------------
-- 1) testing iam_role_global table constraints and insert_role_subtype
--------------------------------------------------------------------------------
-- 1a) insert a valid row -> should succeed and insert_role_subtype trigger
-- r_1111111111 is a global role with grant_scope=descendants
-- r_2222222222 is a global role with grant_scope=children
-- r_3333333333 is a global role with grant_scope=individual
prepare insert_valid_global_role as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_1111111111', 'global', true, 'descendants'),
('r_2222222222', 'global', true, 'children'),
('r_3333333333', 'global', true, 'individual');
select lives_ok('insert_valid_global_role');
-- verify it also created a row in base iam_role
select is(count(*), 1::bigint) from iam_role where public_id = 'r_1111111111';
select is(count(*), 1::bigint) from iam_role where public_id = 'r_2222222222';
select is(count(*), 1::bigint) from iam_role where public_id = 'r_3333333333';
-- 1b) try duplicate (public_id, grant_scope) => unique violation
prepare insert_dup_public_id_grant_scope as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_1111111111', 'global', true, 'children');
select throws_like(
'insert_dup_public_id_grant_scope',
'duplicate key value violates unique constraint "iam_role_pkey"',
'unique(public_id) is enforced'
);
-- 1c) invalid grant_scope (not in iam_role_global_grant_scope_enm table)
prepare insert_invalid_grant_scope as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_globeglobe', 'global', true, 'invalid_grant_scope');
select throws_like(
'insert_invalid_grant_scope',
'insert or update on table "iam_role_global" violates foreign key constraint "iam_role_global_grant_scope_enm_fkey"',
'invalid grant_scope must fail foreign key to iam_role_global_grant_scope_enm'
);
-- 1d) invalid scope_id -> must reference iam_scope_global(scope_id)
prepare insert_bad_scope_id as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_globeglobe', 'does_not_exist', true, 'individual');
select throws_like(
'insert_bad_scope_id',
'insert or update on table "iam_role" violates foreign key constraint "iam_scope_scope_id_fkey"',
'scope_id must exist in iam_scope_global(scope_id)'
);
--------------------------------------------------------------------------------
-- 2) testing insert_grant_scope_update_time trigger
--------------------------------------------------------------------------------
-- 2a) insert a new row (grant_this_role_scope, grant_scope) => should initialize
prepare insert_with_grant_scope_update_time_set as
insert into iam_role_global
(public_id, scope_id, grant_scope, grant_scope_update_time)
values
('r_4444444444', 'global', 'descendants', null);
select lives_ok('insert_with_grant_scope_update_time_set');
-- 2b) check if grant_scope_update_time is set
select is(
(select grant_scope_update_time is not null from iam_role_global where public_id = 'r_4444444444'),
true,
'grant_scope_update_time should be set with the default timestamp right after insert'
);
-- 2c) update grant_this_role_scope => trigger should update grant_scope_update_time timestamp
prepare update_grant_this_role_scope as
update iam_role_global
set grant_this_role_scope = true
where public_id = 'r_4444444444';
select lives_ok('update_grant_this_role_scope');
select is(
(select grant_scope_update_time is not null from iam_role_global where public_id = 'r_4444444444'),
true,
'grant_scope_update_time should be set with the default timestamp right after insert'
);
--------------------------------------------------------------------------------
-- 3) testing iam_role_global_individual_org_grant_scope table constraints
--------------------------------------------------------------------------------
-- 3a) insert invalid row: grant_scope = 'descendants'
prepare insert_invalid_individual_org_grant_scope_descendants as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'descendants', 'p____bwidget');
select throws_like(
'insert_invalid_individual_org_grant_scope_descendants',
'new row for relation "iam_role_global_individual_org_grant_scope" violates check constraint "only_individual_grant_scope_allowed"',
'check(grant_scope = "individual") is enforced'
);
-- 3b) insert invalid row: grant_scope = 'children'
prepare insert_invalid_individual_org_grant_scope_children as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'children', 'p____bwidget');
select throws_like(
'insert_invalid_individual_org_grant_scope_children',
'new row for relation "iam_role_global_individual_org_grant_scope" violates check constraint "only_individual_grant_scope_allowed"',
'check(grant_scope = "individual") is enforced'
);
-- 3c) insert invalid row with a scope_id that is not global
prepare insert_invalid_iam_role_global_individual_org_grant_scope as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'o_1111111111');
select throws_like(
'insert_invalid_iam_role_global_individual_org_grant_scope',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey"',
'foreign key also enforces matching grant_scope=individual in iam_role_global'
);
-- 3d) insert invalid row where scope_id is 'global'
prepare insert_iam_role_global_individual_scope_id as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'global');
select throws_like(
'insert_iam_role_global_individual_scope_id',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey"',
'check(scope_id != ''global'') is enforced'
);
-- 3e) insert invalid row where scope_id is a project
prepare insert_invalid_project_into_individual_org_scope as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'p____bwidgetp____bwidget');
select throws_like(
'insert_iam_role_global_individual_scope_id',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey"',
'foreign key also enforces matching grant_scope=individual in iam_role_global'
);
-- 3f) insert into iam_role_global_individual_org_grant_scope when role grant_scope is 'children'
prepare insert_invalid_individual_org_scope as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_2222222222', 'individual', 'o_____widget');
select throws_like(
'insert_invalid_individual_org_scope',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"',
'foreign key also enforces matching grant_scope=individual in iam_role_global'
);
-- 3g) insert into iam_role_global_individual_org_grant_scope when role grant_scope is descendants is not allowed
-- r_1111111111 is a global role with grant_scope=descendants
prepare iam_role_global_individual_org_grant_scope_role_descendants as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_1111111111', 'individual', 'o_____widget');
select throws_like(
'iam_role_global_individual_org_grant_scope_role_descendants',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"',
'foreign key also enforces matching grant_scope=individual in iam_role_global'
);
-- 3h) insert into iam_role_global_individual_org_grant_scope when role grant_scope is children is not allowed
-- r_2222222222 is a global role with grant_scope=children
prepare iam_role_global_individual_org_grant_scope_role_children as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_2222222222', 'individual', 'o_____widget');
select throws_like(
'iam_role_global_individual_org_grant_scope_role_children',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"',
'foreign key also enforces matching grant_scope=individual in iam_role_global'
);
--3i) insert entry with role_id that does not exist in iam_role_global
prepare insert_invalid_role_id as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_1231231231', 'individual', 'p____bwidget');
select throws_like(
'insert_invalid_role_id',
'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_fkey"',
'foreign key enforces that role exists in iam_role_global'
);
-- 3j) insert into iam_role_global_individual_org_grant_scope when role grant_scope is 'individual'
-- r_3333333333 is a global role with grant_scope=individual
prepare insert_valid_individual_org_scope as
insert into iam_role_global_individual_org_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'o_____widget');
select lives_ok('insert_valid_individual_org_scope');
--------------------------------------------------------------------------------
-- 4) testing iam_role_global_individual_project_grant_scope table constraints
--------------------------------------------------------------------------------
-- 4a) insert invalid row: grant_scope = 'descendants'
prepare insert_invalid_individual_project_grant_scope_descendants as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'descendants', 'p____bwidget');
select throws_like(
'insert_invalid_individual_project_grant_scope_descendants',
'new row for relation "iam_role_global_individual_project_grant_scope" violates check constraint "only_individual_or_children_grant_scope_allowed"',
'check(grant_scope in ["children", "individual"]) is enforced'
);
-- 4b) insert invalid row: grant_scope = 'children'
prepare insert_invalid_individual_project_grant_scope_children as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'children', 'p____bwidget');
select throws_like(
'insert_invalid_individual_project_grant_scope_children',
'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"',
'foreign key to grant_scope in iam_role_global is enforced'
);
-- 4c) insert invalid row with a scope_id project does not exist
prepare insert_invalid_iam_role_global_individual_project_grant_scope as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'p_1111111111');
select throws_like(
'insert_invalid_iam_role_global_individual_project_grant_scope',
'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"',
'foreign key also enforces matching grant_scope=individual in iam_role_global'
);
-- 4d) insert invalid row where scope_id is 'global'
prepare insert_invalid_project_grant_scope_global as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'global');
select throws_like(
'insert_invalid_project_grant_scope_global',
'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"',
'check(scope_id != ''global'') is enforced'
);
-- 4e) insert invalid row where scope_id is an org
prepare insert_invalid_org_into_individual_proj_scope as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'o_____widget');
select throws_like(
'insert_invalid_org_into_individual_proj_scope',
'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"',
'foreign key enforces that scope_id is a project scope'
);
-- 4f) insert into iam_role_global_individual_project_grant_scope when role grant_scope is descendants is not allowed
-- r_1111111111 is a global role with grant_scope=descendants
prepare iam_role_global_individual_project_grant_scope_role_descendants as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_1111111111', 'individual', 'p_____widget');
select throws_like(
'iam_role_global_individual_project_grant_scope_role_descendants',
'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"',
'foreign key enforces that scope_id is a project scope'
);
-- 4g) insert entry with role_id that does not exist in iam_role_global
prepare insert_invalid_role_id_proj_grants_scope as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_1231231231', 'individual', 'p____bwidget');
select throws_like(
'insert_invalid_role_id_proj_grants_scope',
'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_fkey"',
'foreign key enforces that role exists in iam_role_global'
);
-- 4h) insert into iam_role_global_individual_project_grant_scope when role grant_scope is 'children' is valid
-- r_2222222222 is a global role with grant_scope=children
prepare insert_valid_individual_project_scope_children_grants as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_2222222222', 'children', 'p____bwidget');
select lives_ok('insert_valid_individual_project_scope_children_grants');
-- 4i) insert into iam_role_global_individual_project_grant_scope when role grant_scope is 'individual'
-- r_3333333333 is a global role with grant_scope=individual
prepare insert_valid_individual_project_scope as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_3333333333', 'individual', 'p____bwidget');
select lives_ok('insert_valid_individual_project_scope');
-- 4j) update to iam_role_global.grant_scope from children to individual cascades to iam_role_global_individual_project_grant_scope.grant_scope
-- r_5555555555 is a global role with grant_scope=children
prepare insert_valid_global_role_children_grants as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_5555555555', 'global', true, 'children');
select lives_ok('insert_valid_global_role_children_grants');
-- create a row in iam_role_global_individual_project_grant_scope with grant_scope=children
prepare insert_valid_project_scope_to_children_grants as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_5555555555', 'children', 'p____bwidget');
select lives_ok('insert_valid_project_scope_to_children_grants');
-- take away children grants by updating to iam_role_global to individual
prepare update_grant_scope_to_individual as
update iam_role_global
set grant_scope = 'individual'
where public_id = 'r_5555555555';
select lives_ok('update_grant_scope_to_individual');
-- check that the update cascaded to iam_role_global_individual_project_grant_scope
-- and that the grant_scope is now individual
select is(count(*), 1::bigint) from iam_role_global_individual_project_grant_scope where role_id = 'r_5555555555' and grant_scope = 'individual';
-- 4k) update to iam_role_global.grant_scope from children to individual cascades to iam_role_global_individual_project_grant_scope.grant_scope
-- r_6666666666 is a global role with grant_scope=individual and is granted individual org and project
prepare insert_valid_global_role_individual_grants as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_6666666666', 'global', true, 'individual');
select lives_ok('insert_valid_global_role_individual_grants');
-- create a row in iam_role_global_individual_project_grant_scope with grant_scope=individual
prepare insert_valid_project_scope_to_individual_grants as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_6666666666', 'individual', 'p____bwidget');
select lives_ok('insert_valid_project_scope_to_individual_grants');
-- take away children grants by updating to iam_role_global to children
prepare update_grant_scope_to_children as
update iam_role_global
set grant_scope = 'children'
where public_id = 'r_6666666666';
select lives_ok('update_grant_scope_to_children');
-- verify that iam_role_global.grant_scope is updated to children
select is(count(*), 1::bigint) from iam_role_global where public_id = 'r_6666666666' and grant_scope = 'children';
-- check that the update cascaded to iam_role_global_individual_project_grant_scope and iam_role_global_individual_org_grant_scope
-- and that the grant_scope is now children
select is(count(*), 1::bigint) from iam_role_global_individual_project_grant_scope where role_id = 'r_6666666666' and scope_id = 'p____bwidget' and grant_scope = 'children';
-- 4l) update to iam_role_global.grant_scope from children to individual sets
-- individually granted project scope in iam_role_global_individual_project_grant_scope grant_scope to children
prepare insert_r8_valid_global_scope_to_individual_grants as
insert into iam_role_global
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_8888888888', 'global', true, 'children');
select lives_ok('insert_r8_valid_global_scope_to_individual_grants');
-- create a row in iam_role_global_individual_project_grant_scope with grant_scope=children
prepare insert_r8_valid_project_scope_to_children_grants as
insert into iam_role_global_individual_project_grant_scope
(role_id, grant_scope, scope_id)
values
('r_8888888888', 'children', 'p____bwidget');
select lives_ok('insert_r8_valid_project_scope_to_children_grants');
-- take away children grants by updating to iam_role_global to individual
prepare update_r8_grant_scope_to_individual as
update iam_role_global
set grant_scope = 'individual'
where public_id = 'r_8888888888';
select lives_ok('update_r8_grant_scope_to_individual');
-- verify that iam_role_global.grant_scope is updated to individual
select is(count(*), 1::bigint) from iam_role_global where public_id = 'r_8888888888' and grant_scope = 'individual';
-- check that the update deletes all individual grant scopes in iam_role_global_individual_org_grant_scope and iam_role_global_individual_project_grant_scope
select is(count(*), 1::bigint) from iam_role_global_individual_project_grant_scope where role_id = 'r_8888888888' and grant_scope = 'individual' and scope_id = 'p____bwidget';
select * from finish();
rollback;
@@ -0,0 +1,218 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(22);
select wtt_load('widgets', 'iam');
------------------------------------------------------------------------------
-- 1) testing iam_role_org table constraints and insert_role_subtype
------------------------------------------------------------------------------
-- 1a) insert a valid row -> should succeed and fire insert_role_subtype trigger
prepare insert_valid_org_role as
insert into iam_role_org
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_org_1111111111',
'o_____widget',
true,
'children'
);
select lives_ok('insert_valid_org_role');
-- verify it also created a row in base iam_role
select is(
(select count(*) from iam_role where public_id = 'r_org_1111111111'),
1::bigint,
'insert_role_subtype trigger inserted a row into iam_role'
);
-- 1b) try duplicate (public_id, grant_scope) => unique violation
prepare insert_dup_public_id_grant_scope as
insert into iam_role_org
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_org_1111111111', 'o_____widget', true, 'children');
select throws_like(
'insert_dup_public_id_grant_scope',
'duplicate key value violates unique constraint "iam_role_pkey"',
'unique(public_id, grant_scope) is enforced on iam_role_org'
);
-- 1c) invalid grant_scope (not in iam_role_org_grant_scope_enm)
prepare insert_invalid_grant_scope as
insert into iam_role_org
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_org_bad_grscope', 'o_____widget', true, 'invalid_scope');
select throws_like(
'insert_invalid_grant_scope',
'insert or update on table "iam_role_org" violates foreign key constraint "iam_role_org_grant_scope_enm_fkey"',
'invalid grant_scope must fail foreign key to iam_role_org_grant_scope_enm'
);
-- 1d) invalid scope_id -> must reference iam_scope_org(scope_id)
prepare insert_bad_scope_id as
insert into iam_role_org
(public_id, scope_id, grant_this_role_scope, grant_scope)
values
('r_org_bad_scope', 'x_no_such_scope', true, 'children');
select throws_like(
'insert_bad_scope_id',
'insert or update on table "iam_role" violates foreign key constraint "iam_scope_scope_id_fkey"',
'scope_id must exist in iam_scope_org(scope_id)'
);
-- 1e) attempt referencing a project scope from iam_scope_project, expecting an error
prepare insert_wrong_scope_type as
insert into iam_role_org
(public_id, scope_id, grant_scope, grant_this_role_scope)
values
('r_org_wrong_scope', 'p____bwidget', 'children', true);
select throws_like(
'insert_wrong_scope_type',
'insert or update on table "iam_role_org" violates foreign key constraint "iam_scope_org_fkey"',
'must reference an org scope, not a project scope'
);
------------------------------------------------------------------------------
-- 2) testing grant_scope_update_time trigger
------------------------------------------------------------------------------
-- 2a) insert a row -> expect it to set grant_scope_update_time (if triggered on insert)
prepare insert_with_grant_scope_update_time as
insert into iam_role_org
(public_id, scope_id, grant_scope, grant_scope_update_time)
values
('r_org_2222222222', 'o_____widget', 'individual', null);
select lives_ok('insert_with_grant_scope_update_time');
select is(
(select grant_scope_update_time is not null
from iam_role_org
where public_id = 'r_org_2222222222'),
true,
'grant_scope_update_time should be set right after insert if the trigger sets it'
);
-- 2b) update grant_this_role_scope => trigger should update grant_this_role_scope_update_time
-- update grant_this_role_scope_update_time to default
prepare reset_grant_this_role_scope_update_time as
update iam_role_org
set grant_this_role_scope_update_time = '1970-01-01 00:00:00'
where public_id = 'r_org_2222222222';
select lives_ok('reset_grant_this_role_scope_update_time');
prepare update_grant_this_role_scope as
update iam_role_org
set grant_this_role_scope = true
where public_id = 'r_org_2222222222';
select lives_ok('update_grant_this_role_scope');
select is(
(select grant_this_role_scope_update_time is not null
from iam_role_org
where public_id = 'r_org_2222222222'),
true,
'grant_this_role_scope_update_time should be updated after changing grant_this_role_scope'
);
select is(
(select grant_this_role_scope_update_time = now()
from iam_role_org
where public_id = 'r_org_2222222222'),
true,
'grant_this_role_scope_update_time should be updated after changing grant_this_role_scope'
);
-- 2c) update grant_scope => trigger should update grant_this_role_scope_update_time
prepare reset_grant_scope_update_time as
update iam_role_org
set grant_scope_update_time = '1970-01-01 00:00:00'
where public_id = 'r_org_2222222222';
select lives_ok('reset_grant_scope_update_time');
prepare update_grant_scope as
update iam_role_org
set grant_scope = 'children'
where public_id = 'r_org_2222222222';
select lives_ok('update_grant_scope');
select is(
(select grant_scope_update_time is not null
from iam_role_org
where public_id = 'r_org_2222222222'),
true,
'grant_scope_update_time should be updated after changing grant_scope'
);
select is(
(select grant_scope_update_time = now()
from iam_role_org
where public_id = 'r_org_2222222222'),
true,
'grant_scope_update_time should be updated after changing grant_scope'
);
------------------------------------------------------------------------------
-- 3) testing iam_role_global_individual_grant_scope table
------------------------------------------------------------------------------
-- 3a) insert a valid row -> should succeed
prepare update_iam_role_org_to_individual_grant_scope as
update iam_role_org
set grant_scope = 'individual'
where public_id = 'r_op_sw__eng';
select lives_ok('update_iam_role_org_to_individual_grant_scope');
prepare insert_valid_row as
insert into iam_role_org_individual_grant_scope (role_id, grant_scope, scope_id)
values ('r_op_sw__eng', 'individual', 'p____bwidget');
select lives_ok('insert_valid_row');
-- 3b) verify individual grant scope was inserted
select is(
(select count(*) from iam_role_org_individual_grant_scope
where role_id = 'r_op_sw__eng'
and grant_scope = 'individual'
and scope_id = 'p____bwidget'),
1::bigint,
'individual grant scope was inserted'
);
-- 3c) verify create_time is set by trigger
select isnt(
(select create_time from iam_role_org_individual_grant_scope
where role_id = 'r_op_sw__eng'
and scope_id = 'p____bwidget'),
null,
'create_time should be set on insert'
);
-- 3d) negative test: grant_scope != 'individual'
prepare insert_bad_grant_scope as
insert into iam_role_org_individual_grant_scope (role_id, grant_scope, scope_id)
values ('r_op_sw__eng', 'children', 'p____bwidget');
select throws_like(
'insert_bad_grant_scope',
'new row for relation "iam_role_org_individual_grant_scope" violates check constraint "only_individual_grant_scope_allowed"',
'grant_scope must be "individual"'
);
-- 3e) negative test: referencing a project scope that belongs to another org
prepare insert_wrong_role_project as
insert into iam_role_org_individual_grant_scope (role_id, grant_scope, scope_id)
values ('o_____widget', 'children', 'invalid_project');
select throws_like(
'insert_wrong_role_project',
'project scope_id invalid_project not found in org',
'ensure_project_belongs_to_role_org trigger enforces matching org'
);
select * from finish();
rollback;
@@ -0,0 +1,145 @@
-- Copyright (c) HashiCorp, Inc.
-- SPDX-License-Identifier: BUSL-1.1
begin;
select plan(14);
select wtt_load('widgets', 'iam');
--------------------------------------------------------------------------------
-- 1) test valid inserts
--------------------------------------------------------------------------------
prepare insert_valid_project_role as
insert into iam_role_project
(public_id, scope_id)
values
('r_proj_1111111111', 'p____bwidget');
select lives_ok('insert_valid_project_role');
-- verify the row actually got inserted in iam_role_project
select is(
(select count(*) from iam_role_project where public_id = 'r_proj_1111111111'),
1::bigint,
'one valid row inserted into iam_role_project'
);
-- check that insert_role_subtype trigger created a corresponding row in iam_role
select is(
(select count(*) from iam_role where public_id = 'r_proj_1111111111'),
1::bigint,
'insert_role_subtype trigger inserted a row into iam_role'
);
-- verify create_time is set (default_create_time_column trigger)
select isnt(
(select create_time from iam_role_project where public_id = 'r_proj_1111111111'),
null,
'create_time is auto-set on insert'
);
--------------------------------------------------------------------------------
-- 2) test invalid inserts
--------------------------------------------------------------------------------
-- 2a) invalid project scope (not in iam_scope_project)
prepare insert_invalid_scope as
insert into iam_role_project
(public_id, scope_id)
values
('r_proj_2222222222', 'o_1111111111');
select throws_like(
'insert_invalid_scope',
'insert or update on table "iam_role" violates foreign key constraint "iam_scope_scope_id_fkey"',
'must reference a valid project scope'
);
-- 2b) duplicate primary ke
prepare insert_duplicate_role_id as
insert into iam_role_project
(public_id, scope_id)
values
('r_proj_1111111111', 'p____bwidget');
select throws_like(
'insert_duplicate_role_id',
'duplicate key value violates unique constraint "iam_role_pkey"',
'primary key (public_id) is enforced'
);
--------------------------------------------------------------------------------
-- 3) test triggers for immutable_columns
--------------------------------------------------------------------------------
-- 3a) try updating immutable columns: scope_id, create_time
prepare update_scope_id as
update iam_role_project
set scope_id = 'p____bwidget2'
where public_id = 'r_proj_1111111111';
select throws_like(
'update_scope_id',
'immutable column: iam_role_project.scope_id',
'immutable_columns trigger prevents changing scope_id'
);
prepare update_create_time as
update iam_role_project
set create_time = null
where public_id = 'r_proj_1111111111';
select throws_like(
'update_create_time',
'immutable column: iam_role_project.create_time',
'immutable_columns trigger prevents changing create_time'
);
------------------------------------------------------------------------------
-- 4) testing grant_scope_update_time trigger
------------------------------------------------------------------------------
-- 4a) insert a row -> expect it to set grant_scope_update_time (if triggered on insert)
prepare insert_with_grant_this_role_scope_update_time as
insert into iam_role_project
(public_id, scope_id, grant_this_role_scope, grant_this_role_scope_update_time)
values
('r_proj_22222222', 'p____bwidget', false, null);
select lives_ok('insert_with_grant_this_role_scope_update_time');
select is(
(select grant_this_role_scope_update_time is not null
from iam_role_project
where public_id = 'r_proj_22222222'),
true,
'grant_this_role_scope_update_time should be set right after insert if the trigger sets it'
);
-- 4b) update grant_this_role_scope => trigger should update grant_this_role_scope_update_time
-- update grant_this_role_scope_update_time to default
prepare reset_grant_this_role_scope_update_time as
update iam_role_project
set grant_this_role_scope_update_time = '1970-01-01 00:00:00'
where public_id = 'r_proj_22222222';
select lives_ok('reset_grant_this_role_scope_update_time');
prepare update_grant_this_role_scope as
update iam_role_project
set grant_this_role_scope = true
where public_id = 'r_proj_22222222';
select lives_ok('update_grant_this_role_scope');
select is(
(select grant_this_role_scope_update_time is not null
from iam_role_project
where public_id = 'r_proj_22222222'),
true,
'grant_this_role_scope_update_time should be updated after changing grant_this_role_scope'
);
select is(
(select grant_this_role_scope_update_time = now()
from iam_role_project
where public_id = 'r_proj_22222222'),
true,
'grant_this_role_scope_update_time should be updated after changing grant_this_role_scope'
);
select * from finish();
rollback;
+50
View File
@@ -12,6 +12,31 @@ import (
"github.com/stretchr/testify/assert"
)
// TestCatalog creates a static host catalog to the provided DB
// with the provided project id. If any errors are encountered during the creation of
// the host catalog, the test will fail.
// Name and description are the only valid options. All other options are
// ignored.
func TestCatalog(t testing.TB, conn *db.DB, projectId string, opt ...Option) *HostCatalog {
t.Helper()
ctx := context.Background()
assert := assert.New(t)
cat, err := NewHostCatalog(ctx, projectId, opt...)
assert.NoError(err)
assert.NotNil(cat)
id, err := newHostCatalogId(ctx)
assert.NoError(err)
assert.NotEmpty(id)
cat.PublicId = id
w := db.New(conn)
err2 := w.Create(ctx, cat)
assert.NoError(err2)
return cat
}
// TestCatalogs creates count number of static host catalogs to the provided DB
// with the provided project id. If any errors are encountered during the creation of
// the host catalog, the test will fail.
@@ -88,6 +113,31 @@ func TestHosts(t testing.TB, conn *db.DB, catalogId string, count int) []*Host {
return hosts
}
// TestSet creates a static host set in the provided DB
// with the provided catalog id. The catalog must have been created
// previously. Name and description are the only valid options. All other options are
// ignored. The test will fail if any errors are encountered.
func TestSet(t testing.TB, conn *db.DB, catalogId string, opt ...Option) *HostSet {
t.Helper()
ctx := context.Background()
assert := assert.New(t)
set, err := NewHostSet(ctx, catalogId, opt...)
assert.NoError(err)
assert.NotNil(set)
id, err := newHostSetId(ctx)
assert.NoError(err)
assert.NotEmpty(id)
set.PublicId = id
w := db.New(conn)
err2 := w.Create(ctx, set)
assert.NoError(err2)
return set
}
// TestSets creates count number of static host sets in the provided DB
// with the provided catalog id. The catalog must have been created
// previously. The test will fail if any errors are encountered.
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/types/resource"
"google.golang.org/protobuf/proto"
)
@@ -57,8 +57,8 @@ func (a *authAccount) VetForWrite(ctx context.Context, r db.Reader, opType db.Op
return nil
}
func (a *authAccount) validScopeTypes() []scope.Type {
return []scope.Type{scope.Global, scope.Org}
func (a *authAccount) getResourceType() resource.Type {
return resource.Account
}
// GetScope returns the scope for the auth account.
+2 -3
View File
@@ -12,7 +12,6 @@ import (
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"google.golang.org/protobuf/proto"
)
@@ -78,8 +77,8 @@ func (g *Group) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType,
return nil
}
func (g *Group) validScopeTypes() []scope.Type {
return []scope.Type{scope.Global, scope.Org, scope.Project}
func (g *Group) getResourceType() resource.Type {
return resource.Group
}
// GetScope returns the scope for the Group.
+185 -19
View File
@@ -8,8 +8,10 @@ import (
"strings"
"testing"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
iamstore "github.com/hashicorp/boundary/internal/iam/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
@@ -69,7 +71,6 @@ func TestScope_ImmutableFields(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := new.Clone()
@@ -129,7 +130,6 @@ func TestConcreteScope_ImmutableFields(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
updateStmt := strings.Replace(update, "{{rep}}", tt.tableName, -1)
@@ -192,7 +192,6 @@ func TestUser_ImmutableFields(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := new.Clone()
@@ -212,7 +211,85 @@ func TestUser_ImmutableFields(t *testing.T) {
}
}
func TestRole_ImmutableFields(t *testing.T) {
func Test_globalRole_ImmutableFields(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
repo := TestRepo(t, conn, wrapper)
w := db.New(conn)
ts := timestamp.Timestamp{Timestamp: &timestamppb.Timestamp{Seconds: 0, Nanos: 0}}
_, proj := TestScopes(t, repo)
ctx := context.Background()
roleId, err := newRoleId(ctx)
require.NoError(t, err)
testGlobalRole := &globalRole{
GlobalRole: &iamstore.GlobalRole{
PublicId: roleId,
ScopeId: globals.GlobalPrefix,
GrantScope: globals.GrantScopeIndividual,
GrantThisRoleScope: true,
},
}
require.NoError(t, w.Create(ctx, testGlobalRole))
require.NotEmpty(t, testGlobalRole.PublicId)
tests := []struct {
name string
update *globalRole
fieldMask []string
}{
{
name: "public_id",
update: func() *globalRole {
c := testGlobalRole.Clone().(*globalRole)
c.PublicId = "r_thisIsNotAValidId"
return c
}(),
fieldMask: []string{"PublicId"},
},
{
name: "create_time",
update: func() *globalRole {
c := testGlobalRole.Clone().(*globalRole)
c.CreateTime = &ts
return c
}(),
fieldMask: []string{"CreateTime"},
},
{
name: "scope_id",
update: func() *globalRole {
c := testGlobalRole.Clone().(*globalRole)
c.ScopeId = proj.PublicId
return c
}(),
fieldMask: []string{"ScopeId"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := testGlobalRole.Clone()
err := w.LookupById(context.Background(), orig)
require.NoError(err)
rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true))
require.Error(err)
assert.Equal(0, rowsUpdated)
after := testGlobalRole.Clone()
err = w.LookupById(context.Background(), after)
require.NoError(err)
assert.True(proto.Equal(orig.(*globalRole), after.(*globalRole)))
})
}
}
func Test_orgRole_ImmutableFields(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
@@ -222,35 +299,49 @@ func TestRole_ImmutableFields(t *testing.T) {
ts := timestamp.Timestamp{Timestamp: &timestamppb.Timestamp{Seconds: 0, Nanos: 0}}
org, proj := TestScopes(t, repo)
new := TestRole(t, conn, org.PublicId)
ctx := context.Background()
orgRoleId, err := newRoleId(ctx)
require.NoError(t, err)
testOrgRole := &orgRole{
OrgRole: &iamstore.OrgRole{
PublicId: orgRoleId,
ScopeId: org.PublicId,
GrantScope: globals.GrantScopeIndividual,
GrantThisRoleScope: true,
},
}
require.NoError(t, w.Create(ctx, testOrgRole))
require.NotEmpty(t, testOrgRole.PublicId)
tests := []struct {
name string
update *Role
update *orgRole
fieldMask []string
}{
{
name: "public_id",
update: func() *Role {
c := new.Clone().(*Role)
update: func() *orgRole {
c := testOrgRole.Clone().(*orgRole)
c.PublicId = "r_thisIsNotAValidId"
return c
}(),
fieldMask: []string{"PublicId"},
},
{
name: "create time",
update: func() *Role {
c := new.Clone().(*Role)
name: "create_time",
update: func() *orgRole {
c := testOrgRole.Clone().(*orgRole)
c.CreateTime = &ts
return c
}(),
fieldMask: []string{"CreateTime"},
},
{
name: "scope id",
update: func() *Role {
c := new.Clone().(*Role)
name: "scope_id",
update: func() *orgRole {
c := testOrgRole.Clone().(*orgRole)
c.ScopeId = proj.PublicId
return c
}(),
@@ -258,10 +349,9 @@ func TestRole_ImmutableFields(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := new.Clone()
orig := testOrgRole.Clone()
err := w.LookupById(context.Background(), orig)
require.NoError(err)
@@ -269,11 +359,88 @@ func TestRole_ImmutableFields(t *testing.T) {
require.Error(err)
assert.Equal(0, rowsUpdated)
after := new.Clone()
after := testOrgRole.Clone()
err = w.LookupById(context.Background(), after)
require.NoError(err)
assert.True(proto.Equal(orig.(*Role), after.(*Role)))
assert.True(proto.Equal(orig.(*orgRole), after.(*orgRole)))
})
}
}
func Test_projRole_ImmutableFields(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
repo := TestRepo(t, conn, wrapper)
w := db.New(conn)
ts := timestamp.Timestamp{Timestamp: &timestamppb.Timestamp{Seconds: 0, Nanos: 0}}
_, proj := TestScopes(t, repo)
_, proj2 := TestScopes(t, repo)
ctx := context.Background()
projRoleId, err := newRoleId(ctx)
require.NoError(t, err)
testProjectRole := &projectRole{
ProjectRole: &iamstore.ProjectRole{
PublicId: projRoleId,
ScopeId: proj.PublicId,
},
}
require.NoError(t, w.Create(ctx, testProjectRole))
require.NotEmpty(t, testProjectRole.PublicId)
tests := []struct {
name string
update *projectRole
fieldMask []string
}{
{
name: "public_id",
update: func() *projectRole {
c := testProjectRole.Clone().(*projectRole)
c.PublicId = "r_thisIsNotAValidId"
return c
}(),
fieldMask: []string{"PublicId"},
},
{
name: "create_time",
update: func() *projectRole {
c := testProjectRole.Clone().(*projectRole)
c.CreateTime = &ts
return c
}(),
fieldMask: []string{"CreateTime"},
},
{
name: "scope_id",
update: func() *projectRole {
c := testProjectRole.Clone().(*projectRole)
c.ScopeId = proj2.PublicId
return c
}(),
fieldMask: []string{"ScopeId"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := testProjectRole.Clone()
err := w.LookupById(context.Background(), orig)
require.NoError(err)
rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true))
require.Error(err)
assert.Equal(0, rowsUpdated)
after := testProjectRole.Clone()
err = w.LookupById(context.Background(), after)
require.NoError(err)
assert.True(proto.Equal(orig.(*projectRole), after.(*projectRole)))
})
}
}
@@ -324,7 +491,6 @@ func TestGroup_ImmutableFields(t *testing.T) {
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
orig := new.Clone()
+25 -23
View File
@@ -26,23 +26,23 @@ type Option func(*options)
// options = how options are represented
type options struct {
withPublicId string
withName string
withDescription string
withLimit int
withGrantScopeIds []string
withSkipVetForWrite bool
withDisassociate bool
withSkipAdminRoleCreation bool
withSkipDefaultRoleCreation bool
withUserId string
withRandomReader io.Reader
withAccountIds []string
withPrimaryAuthMethodId string
withReader db.Reader
withWriter db.Writer
withStartPageAfterItem pagination.Item
withTestCacheMultiGrantTuples *[]multiGrantTuple
withPublicId string
withName string
withDescription string
withLimit int
withRecursive bool
withGrantScopeIds []string
withSkipVetForWrite bool
withDisassociate bool
withSkipAdminRoleCreation bool
withSkipDefaultRoleCreation bool
withUserId string
withRandomReader io.Reader
withAccountIds []string
withPrimaryAuthMethodId string
withReader db.Reader
withWriter db.Writer
withStartPageAfterItem pagination.Item
}
func getDefaultOptions() options {
@@ -52,6 +52,7 @@ func getDefaultOptions() options {
withDescription: "",
withLimit: 0,
withSkipVetForWrite: false,
withRecursive: false,
}
}
@@ -69,6 +70,13 @@ func WithDescription(desc string) Option {
}
}
// WithRecursive indicates that this request is a recursive request
func WithRecursive(isRecursive bool) Option {
return func(o *options) {
o.withRecursive = isRecursive
}
}
// WithName provides an option to search by a friendly name
func WithName(name string) Option {
return func(o *options) {
@@ -176,9 +184,3 @@ func WithStartPageAfterItem(item pagination.Item) Option {
o.withStartPageAfterItem = item
}
}
func withTestCacheMultiGrantTuples(cache *[]multiGrantTuple) Option {
return func(o *options) {
o.withTestCacheMultiGrantTuples = cache
}
}
+9
View File
@@ -99,4 +99,13 @@ func Test_GetOpts(t *testing.T) {
assert.Equal(opts.withStartPageAfterItem.GetPublicId(), "s_1")
assert.Equal(opts.withStartPageAfterItem.GetUpdateTime(), timestamp.New(updateTime))
})
t.Run("WithRecursive", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts(WithRecursive(true))
defaultOpts := getDefaultOptions()
assert.False(defaultOpts.withRecursive)
assert.True(opts.withRecursive)
falseRecurse := getOpts(WithRecursive(false))
assert.False(falseRecurse.withRecursive)
})
}
+564 -51
View File
@@ -112,98 +112,456 @@ const (
order by action, member_id;
`
grantsForUserQuery = `
resourceRoleGrantsForUsers = `
with
users (id) as (
select public_id
from iam_user
%s -- anonUser || authUser
where public_id = any(@user_ids)
),
user_groups (id) as (
select group_id
from iam_group_member_user
where member_id in (select id from users)
where member_id in (select id
from users)
),
user_accounts (id) as (
select public_id
from auth_account
where iam_user_id in (select id from users)
where iam_user_id in (select id
from users)
),
user_oidc_managed_groups (id) as (
select managed_group_id
from auth_oidc_managed_group_member_account
where member_id in (select id from user_accounts)
where member_id in (select id
from user_accounts)
),
user_ldap_managed_groups (id) as (
select managed_group_id
from auth_ldap_managed_group_member_account
where member_id in (select id from user_accounts)
where member_id in (select id
from user_accounts)
),
managed_group_roles (role_id) as (
select distinct role_id
from iam_managed_group_role
where principal_id in (select id from user_oidc_managed_groups)
or principal_id in (select id from user_ldap_managed_groups)
where principal_id in (select id
from user_oidc_managed_groups)
or principal_id in (select id
from user_ldap_managed_groups)
),
group_roles (role_id) as (
select role_id
from iam_group_role
where principal_id in (select id from user_groups)
where principal_id in (select id
from user_groups)
),
user_roles (role_id) as (
select role_id
from iam_user_role
where principal_id in (select id from users)
where principal_id in (select id
from users)
),
user_group_roles (role_id) as (
all_associated_roles (role_id) as (
select role_id
from group_roles
union
union
select role_id
from user_roles
union
union
select role_id
from managed_group_roles
),
-- Now that we have the role IDs, expand the information to include scope
roles (role_id, role_scope_id, role_parent_scope_id) as (
select iam_role.public_id,
iam_role.scope_id,
iam_scope.parent_id
from iam_role
join iam_scope
on iam_scope.public_id = iam_role.scope_id
where iam_role.public_id in (select role_id from user_group_roles)
),
grant_scopes (role_id, grant_scope_ids) as (
select roles.role_id,
string_agg(iam_role_grant_scope.scope_id_or_special, '^') as grant_scope_ids
from roles
join iam_role_grant_scope
on iam_role_grant_scope.role_id = roles.role_id
group by roles.role_id
),
grants (role_id, grants) as (
select roles.role_id,
string_agg(iam_role_grant.canonical_grant, '^') as grants
from roles
join iam_role_grant
on iam_role_grant.role_id = roles.role_id
group by roles.role_id
roles_with_grants (role_id, canonical_grant) as (
select iam_role_grant.role_id,
iam_role_grant.canonical_grant
from iam_role_grant
join iam_role
on iam_role.public_id = iam_role_grant.role_id
join iam_grant
on iam_grant.canonical_grant = iam_role_grant.canonical_grant
where iam_role.public_id in (select role_id
from all_associated_roles)
and iam_grant.resource = any(@resources)
)`
// grantsForUserGlobalResourcesQuery gets a user's grants for resources only applicable to global scopes.
grantsForUserGlobalResourcesQuery = resourceRoleGrantsForUsers + `,
global_roles_this_grant_scope as (
select iam_role_global.public_id as role_id,
iam_role_global.scope_id as role_scope_id,
'' as role_parent_scope_id,
'individual' as grant_scope,
iam_role_global.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope, -- individual_grant_scopes are not applicable to global roles
roles_with_grants.canonical_grant as canonical_grant
from iam_role_global
join roles_with_grants
on roles_with_grants.role_id = iam_role_global.public_id
where iam_role_global.grant_this_role_scope
)
-- Finally, take the resulting roles and pull grant scope IDs and canonical grants.
-- We will split these out in application logic to keep the result set size low.
select
roles.role_id as role_id,
roles.role_scope_id as role_scope_id,
roles.role_parent_scope_id as role_parent_scope_id,
grant_scopes.grant_scope_ids as grant_scope_ids,
grants.grants as grants
from roles
join grant_scopes
on grant_scopes.role_id = roles.role_id
join grants
on grants.role_id = roles.role_id;
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
array_agg(distinct(individual_grant_scope)) filter (where individual_grant_scope is not null) as individual_grant_scopes,
array_agg(distinct(canonical_grant)) as canonical_grants
from global_roles_this_grant_scope
group by role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope;
`
// grantsForUserOrgResourcesQuery gets a user's grants for resources only applicable to org scopes.
grantsForUserOrgResourcesQuery = resourceRoleGrantsForUsers + `,
global_roles_with_individual_or_special_grant_scopes as (
select iam_role_global.public_id as role_id,
iam_role_global.scope_id as role_scope_id,
'' as role_parent_scope_id,
iam_role_global.grant_scope as grant_scope,
iam_role_global.grant_this_role_scope as grant_this_role_scope,
individual.scope_id as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_global
join roles_with_grants
on roles_with_grants.role_id = iam_role_global.public_id
left join iam_role_global_individual_org_grant_scope individual
on individual.role_id = iam_role_global.public_id
where iam_role_global.grant_scope = any('{ children, descendants }')
or individual.scope_id = @request_scope_id
),
org_roles_this_grant_scope as (
select iam_role_org.public_id as role_id,
iam_role_org.scope_id as role_scope_id,
'global' as role_parent_scope_id,
'individual' as grant_scope,
iam_role_org.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_org
join roles_with_grants
on roles_with_grants.role_id = iam_role_org.public_id
where iam_role_org.grant_this_role_scope
and iam_role_org.scope_id = @request_scope_id
),
global_and_org_roles as (
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from global_roles_with_individual_or_special_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from org_roles_this_grant_scope
)
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
array_agg(distinct(individual_grant_scope)) filter (where individual_grant_scope is not null) as individual_grant_scopes,
array_agg(distinct(canonical_grant)) as canonical_grants
from global_and_org_roles
group by role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope;
`
// grantsForUserProjectResourcesQuery gets a user's grants for resources only applicable to project scopes.
grantsForUserProjectResourcesQuery = resourceRoleGrantsForUsers + `,
global_roles_with_individual_or_descendant_grant_scopes as (
select iam_role_global.public_id as role_id,
iam_role_global.scope_id as role_scope_id,
'' as role_parent_scope_id,
iam_role_global.grant_scope as grant_scope,
iam_role_global.grant_this_role_scope as grant_this_role_scope,
individual.scope_id as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_global
join roles_with_grants
on roles_with_grants.role_id = iam_role_global.public_id
left join iam_role_global_individual_project_grant_scope individual
on individual.role_id = iam_role_global.public_id
where iam_role_global.grant_scope = 'descendants'
or individual.scope_id = @request_scope_id
),
org_roles_with_individual_or_children_grant_scopes as (
select iam_role_org.public_id as role_id,
iam_role_org.scope_id as role_scope_id,
'global' as role_parent_scope_id,
iam_role_org.grant_scope as grant_scope,
iam_role_org.grant_this_role_scope as grant_this_role_scope,
individual.scope_id as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_org
join roles_with_grants
on roles_with_grants.role_id = iam_role_org.public_id
join iam_scope_project
on iam_scope_project.parent_id = iam_role_org.scope_id
left join iam_role_org_individual_grant_scope individual
on individual.role_id = iam_role_org.public_id
where individual.scope_id = @request_scope_id
or (
iam_role_org.grant_scope = 'children' and
iam_scope_project.scope_id = @request_scope_id
)
),
project_roles_this_grant_scope as (
select iam_role_project.public_id as role_id,
iam_role_project.scope_id as role_scope_id,
iam_scope_project.parent_id as role_parent_scope_id,
'individual' as grant_scope,
iam_role_project.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_project
join roles_with_grants
on roles_with_grants.role_id = iam_role_project.public_id
join iam_scope_project
on iam_scope_project.scope_id = iam_role_project.scope_id
where iam_role_project.grant_this_role_scope
and iam_role_project.scope_id = @request_scope_id
),
all_roles as (
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from global_roles_with_individual_or_descendant_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from org_roles_with_individual_or_children_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from project_roles_this_grant_scope
)
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
array_agg(distinct(individual_grant_scope)) filter (where individual_grant_scope is not null) as individual_grant_scopes,
array_agg(distinct(canonical_grant)) as canonical_grants
from all_roles
group by role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope;
`
// grantsForUserRecursiveQuery gets a user's grants for resources
// applicable to all scopes at the global request scope.
grantsForUserRecursiveQuery = resourceRoleGrantsForUsers + `,
global_individual_grant_scopes (role_id, scope_id) as (
select role_id, scope_id
from iam_role_global_individual_org_grant_scope
union
select role_id, scope_id
from iam_role_global_individual_project_grant_scope
),
global_roles_this_grant_scope as (
select iam_role_global.public_id as role_id,
iam_role_global.scope_id as role_scope_id,
'' as role_parent_scope_id,
iam_role_global.grant_scope as grant_scope,
iam_role_global.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_global
join roles_with_grants
on roles_with_grants.role_id = iam_role_global.public_id
where iam_role_global.grant_this_role_scope
),
global_roles_with_special_grant_scopes as (
select iam_role_global.public_id as role_id,
iam_role_global.scope_id as role_scope_id,
'' as role_parent_scope_id,
iam_role_global.grant_scope as grant_scope,
iam_role_global.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_global
join roles_with_grants
on roles_with_grants.role_id = iam_role_global.public_id
where iam_role_global.grant_scope = any('{ children, descendants }')
),
global_roles_with_individual_grant_scopes as (
select iam_role_global.public_id as role_id,
iam_role_global.scope_id as role_scope_id,
'' as role_parent_scope_id,
iam_role_global.grant_scope as grant_scope,
iam_role_global.grant_this_role_scope as grant_this_role_scope,
individual.scope_id as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_global
join roles_with_grants
on roles_with_grants.role_id = iam_role_global.public_id
join global_individual_grant_scopes individual
on individual.role_id = iam_role_global.public_id
),
org_roles_this_grant_scope as (
select iam_role_org.public_id as role_id,
iam_role_org.scope_id as role_scope_id,
'global' as role_parent_scope_id,
iam_role_org.grant_scope as grant_scope,
iam_role_org.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_org
join roles_with_grants
on roles_with_grants.role_id = iam_role_org.public_id
where iam_role_org.grant_this_role_scope
),
org_roles_with_children_grant_scopes as (
select iam_role_org.public_id as role_id,
iam_role_org.scope_id as role_scope_id,
'global' as role_parent_scope_id,
iam_role_org.grant_scope as grant_scope,
iam_role_org.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_org
join roles_with_grants
on roles_with_grants.role_id = iam_role_org.public_id
where iam_role_org.grant_scope = 'children'
),
org_roles_with_individual_grant_scopes as (
select iam_role_org.public_id as role_id,
iam_role_org.scope_id as role_scope_id,
'global' as role_parent_scope_id,
iam_role_org.grant_scope as grant_scope,
iam_role_org.grant_this_role_scope as grant_this_role_scope,
individual.scope_id as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_org
join roles_with_grants
on roles_with_grants.role_id = iam_role_org.public_id
join iam_role_org_individual_grant_scope individual
on individual.role_id = iam_role_org.public_id
),
project_roles_this_grant_scope as (
select iam_role_project.public_id as role_id,
iam_role_project.scope_id as role_scope_id,
iam_scope_project.parent_id as role_parent_scope_id,
'individual' as grant_scope,
iam_role_project.grant_this_role_scope as grant_this_role_scope,
null as individual_grant_scope,
roles_with_grants.canonical_grant as canonical_grant
from iam_role_project
join roles_with_grants
on roles_with_grants.role_id = iam_role_project.public_id
join iam_scope_project
on iam_scope_project.scope_id = iam_role_project.scope_id
where iam_role_project.grant_this_role_scope
),
all_roles as (
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from global_roles_this_grant_scope
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from global_roles_with_special_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from global_roles_with_individual_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from org_roles_this_grant_scope
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from org_roles_with_children_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from org_roles_with_individual_grant_scopes
union
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
individual_grant_scope,
canonical_grant
from project_roles_this_grant_scope
)
select role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope,
array_agg(distinct(individual_grant_scope)) filter (where individual_grant_scope is not null) as individual_grant_scopes,
array_agg(distinct(canonical_grant)) as canonical_grants
from all_roles
group by role_id,
role_scope_id,
role_parent_scope_id,
grant_scope,
grant_this_role_scope;
`
estimateCountRoles = `
@@ -221,4 +579,159 @@ const (
estimateCountScopes = `
select reltuples::bigint as estimate from pg_class where oid in ('iam_scope'::regclass)
`
scopeIdFromRoleIdQuery = `
select scope_id
from iam_role
where public_id = @public_id;`
listRolesQuery = `
with
combined_role_types (role_id) as (
select public_id
from iam_role
where %s -- the where clause is programmatically generated
order by create_time desc, public_id desc
limit @limit
)
select public_id,
scope_id,
name,
description,
create_time,
update_time,
version
from iam_role_global
where public_id = any(select role_id from combined_role_types)
union all
select public_id,
scope_id,
name,
description,
create_time,
update_time,
version
from iam_role_org
where public_id = any(select role_id from combined_role_types)
union all
select public_id,
scope_id,
name,
description,
create_time,
update_time,
version
from iam_role_project
where public_id = any(select role_id from combined_role_types)
order by update_time desc, public_id desc
`
roleGrantsScopeQuery = `
with
global_roles (role_id) as (
select public_id as role_id
from iam_role_global
where public_id = any($1)
),
org_roles (role_id) as (
select public_id as role_id
from iam_role_org
where public_id = any($1)
),
proj_roles (role_id) as (
select public_id as role_id
from iam_role_project
where public_id = any($1)
),
global_role_this_grants (role_id, scope_id_or_special, create_time) as (
select public_id as role_id,
'this' as scope_id_or_special,
grant_this_role_scope_update_time as create_time
from iam_role_global
where public_id = any (select role_id from global_roles)
and grant_this_role_scope = true
),
org_role_this_grants (role_id, scope_id_or_special, create_time) as (
select public_id as role_id,
'this' as scope_id_or_special,
grant_this_role_scope_update_time as create_time
from iam_role_org
where public_id = any (select role_id from org_roles)
and grant_this_role_scope = true
),
proj_role_this_grants (role_id, scope_id_or_special, create_time) as (
select public_id as role_id,
'this' as scope_id_or_special,
create_time as create_time
from iam_role_project
where public_id = any (select role_id from proj_roles)
and grant_this_role_scope = true
),
global_role_special_grants (role_id, scope_id_or_special, create_time) as (
select public_id as role_id,
grant_scope as scope_id_or_special,
grant_this_role_scope_update_time as create_time
from iam_role_global
where public_id = any (select role_id from global_roles)
and grant_scope != 'individual'
),
org_role_special_grants (role_id, scope_id_or_special, create_time) as (
select public_id as role_id,
grant_scope as scope_id_or_special,
grant_this_role_scope_update_time as create_time
from iam_role_org
where public_id = any (select role_id from org_roles)
and grant_scope != 'individual'
),
global_role_individual_org_grants (role_id, scope_id_or_special, create_time) as (
select role_id as role_id,
scope_id as scope_id_or_special,
create_time as create_time
from iam_role_global_individual_org_grant_scope
where role_id = any (select role_id from global_roles)
),
global_role_individual_proj_grants (role_id, scope_id_or_special, create_time) as (
select role_id as role_id,
scope_id as scope_id_or_special,
create_time as create_time
from iam_role_global_individual_project_grant_scope
where role_id = any (select role_id from global_roles)
),
org_role_individual_grants (role_id, scope_id_or_special, create_time) as (
select role_id as role_id,
scope_id as scope_id_or_special,
create_time as create_time
from iam_role_org_individual_grant_scope
where role_id = any (select role_id from org_roles)
),
final (role_id, scope_id_or_special, create_time) as (
select role_id, scope_id_or_special, create_time
from global_role_this_grants
union
select role_id, scope_id_or_special, create_time
from org_role_this_grants
union
select role_id, scope_id_or_special, create_time
from proj_role_this_grants
union
select role_id, scope_id_or_special, create_time
from global_role_special_grants
union
select role_id, scope_id_or_special, create_time
from org_role_special_grants
union
select role_id, scope_id_or_special, create_time
from global_role_individual_org_grants
union
select role_id, scope_id_or_special, create_time
from global_role_individual_proj_grants
union
select role_id, scope_id_or_special, create_time
from org_role_individual_grants
)
select role_id,
scope_id_or_special,
create_time
from final;
`
)
-381
View File
@@ -1,381 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package iam
import (
"context"
"fmt"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/util"
)
// AddRoleGrantScopes will add role grant scopes associated with the role ID in
// the repository. No options are currently supported. Zero is not a valid value
// for the WithVersion option and will return an error.
func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, _ ...Option) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).AddRoleGrantScopes"
switch {
case roleId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case len(grantScopes) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grant scopes")
case roleVersion == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
scope, err := role.GetScope(ctx, r.reader)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
role.ScopeId = scope.PublicId
// Find existing grant scopes
roleGrantScopes := []*RoleGrantScope{}
if err := r.reader.SearchWhere(ctx, &roleGrantScopes, "role_id = ?", []any{roleId}); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grant scopes"))
}
found := map[string]*RoleGrantScope{}
for _, rgs := range roleGrantScopes {
found[rgs.ScopeIdOrSpecial] = rgs
}
// Check incoming grant scopes to see if they exist so we don't try to add
// again and cause an integrity error
addRoleGrantScopes := make([]any, 0, len(grantScopes))
for _, grantScope := range grantScopes {
if _, ok := found[grantScope]; !ok {
addRoleGrantScopes = append(addRoleGrantScopes, grantScope)
}
}
newRoleGrantScopes := make([]*RoleGrantScope, 0, len(addRoleGrantScopes))
for _, grantScope := range grantScopes {
roleGrantScope, err := NewRoleGrantScope(ctx, role.GetPublicId(), grantScope)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope"))
}
newRoleGrantScopes = append(newRoleGrantScopes, roleGrantScope)
}
oplogWrapper, err := r.kms.GetWrapper(ctx, role.GetScopeId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
// We need to update the role version as that's the aggregate
updatedRole := allocRole()
updatedRole.PublicId = role.GetPublicId()
updatedRole.Version = uint32(roleVersion + 1)
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(newRoleGrantScopes))
if err := w.CreateItems(ctx, newRoleGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grants"))
}
msgs = append(msgs, roleGrantScopesOplogMsgs...)
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-public-id": []string{role.PublicId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return newRoleGrantScopes, nil
}
// DeleteRoleGrantScopes will delete role grant scopes associated with the role ID in
// the repository. No options are currently supported. Zero is not a valid value
// for the WithVersion option and will return an error.
func (r *Repository) DeleteRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, _ ...Option) (int, error) {
const op = "iam.(Repository).DeleteRoleGrantScopes"
switch {
case roleId == "":
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case len(grantScopes) == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grant scopes")
case roleVersion == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
scope, err := role.GetScope(ctx, r.reader)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
role.ScopeId = scope.PublicId
oplogWrapper, err := r.kms.GetWrapper(ctx, role.GetScopeId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var totalRowsDeleted int
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
// We need to update the role version as that's the aggregate
updatedRole := allocRole()
updatedRole.PublicId = role.GetPublicId()
updatedRole.Version = uint32(roleVersion + 1)
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
deleteRoleGrantScopes := make([]*RoleGrantScope, 0, len(grantScopes))
for _, grantScope := range grantScopes {
roleGrantScope, err := NewRoleGrantScope(ctx, roleId, grantScope)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope"))
}
deleteRoleGrantScopes = append(deleteRoleGrantScopes, roleGrantScope)
}
if len(deleteRoleGrantScopes) == 0 {
return nil
}
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(deleteRoleGrantScopes))
rowsDeleted, err := w.DeleteItems(ctx, deleteRoleGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grants"))
}
totalRowsDeleted = rowsDeleted
msgs = append(msgs, roleGrantScopesOplogMsgs...)
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-public-id": []string{role.PublicId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return totalRowsDeleted, nil
}
// SetRoleGrantScopes sets grant scopes on a role (roleId). The role's current
// db version
// must match the roleVersion or an error will be returned. Zero is not a valid
// value for the WithVersion option and will return an error.
func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, opt ...Option) ([]*RoleGrantScope, int, error) {
const op = "iam.(Repository).SetRoleGrantScopes"
switch {
case roleId == "":
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case roleVersion == 0:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
case grantScopes == nil:
// Explicitly set to zero clears, but treat nil as a mistake
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
reader := r.reader
writer := r.writer
needFreshReaderWriter := true
opts := getOpts(opt...)
if !util.IsNil(opts.withReader) && !util.IsNil(opts.withWriter) {
reader = opts.withReader
writer = opts.withWriter
needFreshReaderWriter = false
}
role := allocRole()
role.PublicId = roleId
// NOTE: Set calculation can safely take place out of the transaction since
// we are using roleVersion to ensure that we end up operating on the same
// set of data from this query to the final set in the transaction function
// Find existing grant scopes
roleGrantScopes := []*RoleGrantScope{}
if err := reader.SearchWhere(ctx, &roleGrantScopes, "role_id = ?", []any{roleId}); err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grant scopes"))
}
found := map[string]*RoleGrantScope{}
for _, rgs := range roleGrantScopes {
found[rgs.ScopeIdOrSpecial] = rgs
}
// Check incoming grant scopes to see if they exist and if so act appropriately
currentRoleGrantScopes := make([]*RoleGrantScope, 0, len(grantScopes)+len(found))
addRoleGrantScopes := make([]*RoleGrantScope, 0, len(grantScopes))
deleteRoleGrantScopes := make([]*RoleGrantScope, 0, len(grantScopes))
for _, grantScope := range grantScopes {
rgs, ok := found[grantScope]
if ok {
// If we have an exact match, do nothing, we want to keep
// it, but remove from found
currentRoleGrantScopes = append(currentRoleGrantScopes, rgs)
delete(found, grantScope)
continue
}
// Not found, so add
rgs, err := NewRoleGrantScope(ctx, roleId, grantScope)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant"))
}
addRoleGrantScopes = append(addRoleGrantScopes, rgs)
currentRoleGrantScopes = append(currentRoleGrantScopes, rgs)
}
if len(found) > 0 {
for _, rgs := range found {
deleteRoleGrantScopes = append(deleteRoleGrantScopes, rgs)
}
}
if len(addRoleGrantScopes) == 0 && len(deleteRoleGrantScopes) == 0 {
return currentRoleGrantScopes, db.NoRowsAffected, nil
}
scope, err := role.GetScope(ctx, reader)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var totalRowsDeleted int
currentRoleGrantScopes = currentRoleGrantScopes[:0]
txFunc := func(rdr db.Reader, wtr db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := wtr.GetTicket(ctx, &role)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = roleVersion + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := wtr.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
// Anything we didn't take out of found needs to be removed. This needs
// to come before writing in new ones because otherwise we may hit some
// validation issues.
if len(deleteRoleGrantScopes) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(deleteRoleGrantScopes))
rowsDeleted, err := wtr.DeleteItems(ctx, deleteRoleGrantScopes, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete role grant scope"))
}
if rowsDeleted != len(deleteRoleGrantScopes) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(deleteRoleGrantScopes)))
}
totalRowsDeleted = rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
// Write the new ones in
if len(addRoleGrantScopes) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(addRoleGrantScopes))
if err := wtr.CreateItems(ctx, addRoleGrantScopes, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grant scope during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-public-id": []string{roleId},
}
if err := wtr.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
if err := r.list(ctx, &currentRoleGrantScopes, "role_id = ?", []any{roleId}, opt...); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve current role grant scopes after set"))
}
return nil
}
if !needFreshReaderWriter {
err = txFunc(reader, writer)
} else {
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
txFunc,
)
}
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return currentRoleGrantScopes, totalRowsDeleted, nil
}
+85 -40
View File
@@ -13,6 +13,7 @@ import (
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/scope"
)
// AddPrincipalRoles provides the ability to add principals (userIds and
@@ -35,7 +36,6 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV
if len(userIds) == 0 && len(groupIds) == 0 && len(managedGroupIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing any of users, groups, or managed groups to add")
}
newUserRoles := make([]*UserRole, 0, len(userIds))
for _, id := range userIds {
usrRole, err := NewUserRole(ctx, roleId, id)
@@ -60,15 +60,12 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV
}
newManagedGrpRoles = append(newManagedGrpRoles, managedGrpRole)
}
role := allocRole()
role.PublicId = roleId
scope, err := role.GetScope(ctx, r.reader)
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
@@ -80,15 +77,33 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
var updatedRole Resource
switch scp.GetType() {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
g.Version = roleVersion + 1
updatedRole = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
o.Version = roleVersion + 1
updatedRole = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
p.Version = roleVersion + 1
updatedRole = &p
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId()))
}
roleTicket, err := w.GetTicket(ctx, updatedRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = roleVersion + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
@@ -119,8 +134,8 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
@@ -160,9 +175,6 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV
if roleVersion == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
// it's "safe" to do this lookup outside the DoTx transaction because we
// have a roleVersion so the principals can’t change without the version
// changing.
@@ -170,7 +182,7 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
toSet, err := r.PrincipalsToSet(ctx, &role, userIds, groupIds, managedGroupIds)
toSet, err := r.PrincipalsToSet(ctx, &Role{PublicId: roleId}, userIds, groupIds, managedGroupIds)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
@@ -180,11 +192,11 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV
return toSet.UnchangedPrincipalRoles, db.NoRowsAffected, nil
}
scope, err := role.GetScope(ctx, r.reader)
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
@@ -199,15 +211,32 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV
// we need a roleTicket, which won't be redeemed until all the other
// writes are successful. We can't just use a single ticket because
// we need to write oplog entries for deletes and adds
roleTicket, err := w.GetTicket(ctx, &role)
var updatedRole Resource
switch scp.GetType() {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
g.Version = roleVersion + 1
updatedRole = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
o.Version = roleVersion + 1
updatedRole = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
p.Version = roleVersion + 1
updatedRole = &p
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId()))
}
roleTicket, err := w.GetTicket(ctx, updatedRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket for role"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = roleVersion + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -217,8 +246,8 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV
msgs := make([]*oplog.Message, 0, 5)
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
msgs = append(msgs, &roleOplogMsg)
@@ -335,9 +364,12 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro
if roleVersion == 0 {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
var roleResource Resource
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
deleteUserRoles := make([]*UserRole, 0, len(userIds))
for _, id := range userIds {
usrRole, err := NewUserRole(ctx, roleId, id)
@@ -363,11 +395,7 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro
deleteManagedGrpRoles = append(deleteManagedGrpRoles, managedGrpRole)
}
scope, err := role.GetScope(ctx, r.reader)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope to create metadata", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
@@ -379,15 +407,32 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
var updatedRole Resource
switch scp.Type {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
g.Version = roleVersion + 1
updatedRole = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
o.Version = roleVersion + 1
updatedRole = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
p.Version = roleVersion + 1
updatedRole = &p
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown role resource type %T", roleResource))
}
roleTicket, err := w.GetTicket(ctx, updatedRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = roleVersion + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
@@ -433,8 +478,8 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
+172 -10
View File
@@ -6,6 +6,7 @@ package iam
import (
"context"
"sort"
"strings"
"testing"
"time"
@@ -24,6 +25,7 @@ func TestRepository_AddPrincipalRoles(t *testing.T) {
wrapper := db.TestWrapper(t)
repo := TestRepo(t, conn, wrapper)
staticOrg, staticProj := TestScopes(t, repo)
globalRole := TestRole(t, conn, globals.GlobalPrefix)
orgRole := TestRole(t, conn, staticOrg.PublicId)
projRole := TestRole(t, conn, staticProj.PublicId)
createScopesFn := func() (orgs []string, projects []string) {
@@ -134,7 +136,7 @@ func TestRepository_AddPrincipalRoles(t *testing.T) {
orgs, projects := createScopesFn()
var userIds, groupIds []string
for _, roleId := range []string{orgRole.PublicId, projRole.PublicId} {
for _, roleId := range []string{globalRole.PublicId, orgRole.PublicId, projRole.PublicId} {
origRole, _, _, _, err := repo.LookupRole(context.Background(), roleId)
require.NoError(err)
@@ -260,7 +262,9 @@ func TestRepository_ListPrincipalRoles(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
db.TestDeleteWhere(t, conn, func() any { r := allocRole(); return &r }(), "1=1")
db.TestDeleteWhere(t, conn, func() any { i := allocGlobalRole(); return &i }(), "1=1")
db.TestDeleteWhere(t, conn, func() any { i := allocOrgRole(); return &i }(), "1=1")
db.TestDeleteWhere(t, conn, func() any { i := allocProjectRole(); return &i }(), "1=1")
role := TestRole(t, conn, tt.createScopeId)
userRoles := make([]string, 0, tt.createCnt)
groupRoles := make([]string, 0, tt.createCnt)
@@ -299,7 +303,7 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) {
rw := db.New(conn)
wrapper := db.TestWrapper(t)
repo := TestRepo(t, conn, wrapper)
org, _ := TestScopes(t, repo)
org, proj := TestScopes(t, repo)
type args struct {
role *Role
@@ -319,7 +323,19 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) {
wantIsErr errors.Code
}{
{
name: "valid",
name: "valid-global",
args: args{
role: TestRole(t, conn, globals.GlobalPrefix),
createUserCnt: 5,
createGroupCnt: 5,
deleteUserCnt: 5,
deleteGroupCnt: 5,
},
wantRowsDeleted: 10,
wantErr: false,
},
{
name: "valid-org",
args: args{
role: TestRole(t, conn, org.PublicId),
createUserCnt: 5,
@@ -330,6 +346,18 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) {
wantRowsDeleted: 10,
wantErr: false,
},
{
name: "valid-proj",
args: args{
role: TestRole(t, conn, proj.PublicId),
createUserCnt: 5,
createGroupCnt: 5,
deleteUserCnt: 5,
deleteGroupCnt: 5,
},
wantRowsDeleted: 10,
wantErr: false,
},
{
name: "valid-keeping-some",
args: args{
@@ -558,7 +586,31 @@ func TestRepository_SetPrincipalRoles(t *testing.T) {
wantErr bool
}{
{
name: "clear",
name: "clear-global",
setup: setupFn,
args: args{
role: TestRole(t, conn, globals.GlobalPrefix),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{},
groupIds: []string{},
},
wantErr: false,
wantAffectedRows: 12,
},
{
name: "clear-org",
setup: setupFn,
args: args{
role: TestRole(t, conn, org.PublicId),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{},
groupIds: []string{},
},
wantErr: false,
wantAffectedRows: 12,
},
{
name: "clear-proj",
setup: setupFn,
args: args{
role: TestRole(t, conn, proj.PublicId),
@@ -570,7 +622,35 @@ func TestRepository_SetPrincipalRoles(t *testing.T) {
wantAffectedRows: 12,
},
{
name: "no change",
name: "global no change",
setup: setupFn,
args: args{
role: TestRole(t, conn, globals.GlobalPrefix),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{},
groupIds: []string{},
addToOrigUsers: true,
addToOrigGrps: true,
},
wantErr: false,
wantAffectedRows: 0,
},
{
name: "org no change",
setup: setupFn,
args: args{
role: TestRole(t, conn, org.PublicId),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{},
groupIds: []string{},
addToOrigUsers: true,
addToOrigGrps: true,
},
wantErr: false,
wantAffectedRows: 0,
},
{
name: "proj no change",
setup: setupFn,
args: args{
role: TestRole(t, conn, proj.PublicId),
@@ -584,7 +664,35 @@ func TestRepository_SetPrincipalRoles(t *testing.T) {
wantAffectedRows: 0,
},
{
name: "add users and grps",
name: "global add users and grps",
setup: setupFn,
args: args{
role: TestRole(t, conn, globals.GlobalPrefix),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{testUser.PublicId},
groupIds: []string{testGrp.PublicId},
addToOrigUsers: true,
addToOrigGrps: true,
},
wantErr: false,
wantAffectedRows: 2,
},
{
name: "org add users and grps",
setup: setupFn,
args: args{
role: TestRole(t, conn, org.PublicId),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{testUser.PublicId},
groupIds: []string{testGrp.PublicId},
addToOrigUsers: true,
addToOrigGrps: true,
},
wantErr: false,
wantAffectedRows: 2,
},
{
name: "proj add users and grps",
setup: setupFn,
args: args{
role: TestRole(t, conn, proj.PublicId),
@@ -598,7 +706,33 @@ func TestRepository_SetPrincipalRoles(t *testing.T) {
wantAffectedRows: 2,
},
{
name: "add users and grps with zero version",
name: "global add users and grps with zero version",
setup: setupFn,
args: args{
role: TestRole(t, conn, globals.GlobalPrefix),
roleVersion: 0, // yep, since setupFn will increment it to 2
userIds: []string{testUser.PublicId},
groupIds: []string{testGrp.PublicId},
addToOrigUsers: true,
addToOrigGrps: true,
},
wantErr: true,
},
{
name: "org add users and grps with zero version",
setup: setupFn,
args: args{
role: TestRole(t, conn, org.PublicId),
roleVersion: 0, // yep, since setupFn will increment it to 2
userIds: []string{testUser.PublicId},
groupIds: []string{testGrp.PublicId},
addToOrigUsers: true,
addToOrigGrps: true,
},
wantErr: true,
},
{
name: "proj add users and grps with zero version",
setup: setupFn,
args: args{
role: TestRole(t, conn, proj.PublicId),
@@ -611,7 +745,35 @@ func TestRepository_SetPrincipalRoles(t *testing.T) {
wantErr: true,
},
{
name: "remove existing and add users and grps",
name: "global remove existing and add users and grps",
setup: setupFn,
args: args{
role: TestRole(t, conn, globals.GlobalPrefix),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{testUser.PublicId},
groupIds: []string{testGrp.PublicId},
addToOrigUsers: false,
addToOrigGrps: false,
},
wantErr: false,
wantAffectedRows: 14,
},
{
name: "org remove existing and add users and grps",
setup: setupFn,
args: args{
role: TestRole(t, conn, org.PublicId),
roleVersion: 2, // yep, since setupFn will increment it to 2
userIds: []string{testUser.PublicId},
groupIds: []string{testGrp.PublicId},
addToOrigUsers: false,
addToOrigGrps: false,
},
wantErr: false,
wantAffectedRows: 14,
},
{
name: "proj remove existing and add users and grps",
setup: setupFn,
args: args{
role: TestRole(t, conn, proj.PublicId),
@@ -665,7 +827,7 @@ func TestRepository_SetPrincipalRoles(t *testing.T) {
r, _, _, _, err := repo.LookupRole(context.Background(), tt.args.role.PublicId)
require.NoError(err)
if tt.name != "no change" {
if !strings.Contains(tt.name, "no change") {
assert.Equalf(tt.args.roleVersion+1, r.Version, "%s unexpected version: %d/%d", tt.name, tt.args.roleVersion+1, r.Version)
assert.Equalf(origRole.Version, r.Version-1, "%s unexpected version: %d/%d", tt.name, origRole.Version, r.Version-1)
}
+251 -53
View File
@@ -14,6 +14,8 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/util"
"github.com/hashicorp/go-dbw"
)
@@ -26,8 +28,6 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option)
switch {
case role == nil:
return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing role")
case role.Role == nil:
return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing role store")
case role.PublicId != "":
return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty")
case role.ScopeId == "":
@@ -38,10 +38,46 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option)
if err != nil {
return nil, nil, nil, nil, errors.Wrap(ctx, err, op)
}
c := role.Clone().(*Role)
c.PublicId = id
var resource Resource
var roleToCreate Resource
switch {
case strings.HasPrefix(role.GetScopeId(), globals.GlobalPrefix):
roleToCreate = &globalRole{
GlobalRole: &store.GlobalRole{
PublicId: id,
ScopeId: role.ScopeId,
Name: role.Name,
Description: role.Description,
GrantThisRoleScope: true,
GrantScope: globals.GrantScopeIndividual,
},
}
case strings.HasPrefix(role.GetScopeId(), globals.OrgPrefix):
roleToCreate = &orgRole{
OrgRole: &store.OrgRole{
PublicId: id,
ScopeId: role.ScopeId,
Name: role.Name,
Description: role.Description,
GrantThisRoleScope: true,
GrantScope: globals.GrantScopeIndividual,
},
}
case strings.HasPrefix(role.GetScopeId(), globals.ProjectPrefix):
roleToCreate = &projectRole{
ProjectRole: &store.ProjectRole{
PublicId: id,
ScopeId: role.ScopeId,
Name: role.Name,
Description: role.Description,
GrantThisRoleScope: true,
},
}
default:
return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "invalid scope type")
}
var createdRole *Role
var pr []*PrincipalRole
var rg []*RoleGrant
var grantScopes []*RoleGrantScope
@@ -50,16 +86,12 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option)
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, writer db.Writer) error {
resource, err = r.create(ctx, c, WithReaderWriter(reader, writer))
res, err := r.create(ctx, roleToCreate, WithReaderWriter(reader, writer))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("while creating role"))
}
_, _, err = r.SetRoleGrantScopes(ctx, id, resource.(*Role).Version, []string{globals.GrantScopeThis}, WithReaderWriter(reader, writer))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("while setting grant scopes"))
}
// Do a fresh lookup to get all return values
resource, pr, rg, grantScopes, err = r.LookupRole(ctx, resource.(*Role).PublicId, WithReaderWriter(reader, writer))
createdRole, pr, rg, grantScopes, err = r.LookupRole(ctx, res.GetPublicId(), WithReaderWriter(reader, writer))
if err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -69,9 +101,9 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option)
if errors.IsUniqueError(err) {
return nil, nil, nil, nil, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("role %s already exists in scope %s", role.Name, role.ScopeId))
}
return nil, nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", c.PublicId)))
return nil, nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", roleToCreate.GetPublicId())))
}
return resource.(*Role), pr, rg, grantScopes, nil
return createdRole, pr, rg, grantScopes, nil
}
// UpdateRole will update a role in the repository and return the written role.
@@ -85,9 +117,6 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, version uint32,
if role == nil {
return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role")
}
if role.Role == nil {
return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role store")
}
if role.PublicId == "" {
return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
@@ -125,15 +154,46 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, version uint32,
db.StdRetryCnt,
db.ExpBackoff{},
func(read db.Reader, w db.Writer) error {
var err error
c := role.Clone().(*Role)
resource = c // If we don't have dbMask or nullFields, we'll return this
scopeType, err := getRoleScopeType(ctx, read, role.PublicId)
if err != nil {
return errors.Wrap(ctx, err, op)
}
var res Resource
switch scopeType {
case scope.Global:
res = &globalRole{GlobalRole: &store.GlobalRole{
PublicId: role.GetPublicId(),
ScopeId: role.GetScopeId(),
Name: role.GetName(),
Description: role.GetDescription(),
Version: role.GetVersion(),
}}
case scope.Org:
res = &orgRole{OrgRole: &store.OrgRole{
PublicId: role.GetPublicId(),
ScopeId: role.GetScopeId(),
Name: role.GetName(),
Description: role.GetDescription(),
Version: role.GetVersion(),
}}
case scope.Project:
res = &projectRole{ProjectRole: &store.ProjectRole{
PublicId: role.GetPublicId(),
ScopeId: role.GetScopeId(),
Name: role.GetName(),
Description: role.GetDescription(),
Version: role.GetVersion(),
}}
case scope.Unknown:
return errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unknown scope type for role: %s", role.PublicId))
}
resource = res // If we don't have dbMask or nullFields, we'll return this
if len(dbMask) > 0 || len(nullFields) > 0 {
resource, rowsUpdated, err = r.update(ctx, c, version, dbMask, nullFields, WithReaderWriter(read, w))
resource, rowsUpdated, err = r.update(ctx, res, version, dbMask, nullFields, WithReaderWriter(read, w))
if err != nil {
return errors.Wrap(ctx, err, op)
}
version = resource.(*Role).Version
}
// Do a fresh lookup since version may have gone up by 1 or 2 based
@@ -147,10 +207,11 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, version uint32,
)
if err != nil {
if errors.IsUniqueError(err) {
return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("role %s already exists in org %s", role.Name, role.ScopeId))
return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("role %s already exists in scope %s", role.Name, role.ScopeId))
}
return nil, nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", role.PublicId)))
}
return resource.(*Role), pr, rg, grantScopes, rowsUpdated, nil
}
@@ -164,16 +225,49 @@ func (r *Repository) LookupRole(ctx context.Context, withPublicId string, opt ..
return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
opts := getOpts(opt...)
role := allocRole()
role.PublicId = withPublicId
var pr []*PrincipalRole
var rg []*RoleGrant
var rgs []*RoleGrantScope
var role *Role
lookupFunc := func(read db.Reader, w db.Writer) error {
if err := read.LookupByPublicId(ctx, &role); err != nil {
scopeType, err := getRoleScopeType(ctx, read, withPublicId)
if err != nil {
return errors.Wrap(ctx, err, op)
}
var res Resource
switch scopeType {
case scope.Global:
gRole := allocGlobalRole()
gRole.PublicId = withPublicId
res = &gRole
case scope.Org:
oRole := allocOrgRole()
oRole.PublicId = withPublicId
res = &oRole
case scope.Project:
pRole := allocProjectRole()
pRole.PublicId = withPublicId
res = &pRole
case scope.Unknown:
return errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unknown scope type for role: %s", role.PublicId))
}
if err := read.LookupByPublicId(ctx, res); err != nil {
return errors.Wrap(ctx, err, op)
}
switch res.(type) {
case *globalRole:
role = res.(*globalRole).toRole()
case *orgRole:
role = res.(*orgRole).toRole()
case *projectRole:
role = res.(*projectRole).toRole()
default:
return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unknown role type %T", res)))
}
repo, err := NewRepository(ctx, read, w, r.kms)
if err != nil {
return errors.Wrap(ctx, err, op)
@@ -186,7 +280,7 @@ func (r *Repository) LookupRole(ctx context.Context, withPublicId string, opt ..
if err != nil {
return errors.Wrap(ctx, err, op)
}
rgs, err = repo.ListRoleGrantScopes(ctx, []string{withPublicId})
rgs, err = listRoleGrantScopes(ctx, read, []string{withPublicId})
if err != nil {
return errors.Wrap(ctx, err, op)
}
@@ -213,7 +307,7 @@ func (r *Repository) LookupRole(ctx context.Context, withPublicId string, opt ..
}
return nil, nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", withPublicId)))
}
return &role, pr, rg, rgs, nil
return role, pr, rg, rgs, nil
}
// DeleteRole will delete a role from the repository.
@@ -222,12 +316,29 @@ func (r *Repository) DeleteRole(ctx context.Context, withPublicId string, _ ...O
if withPublicId == "" {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
role := allocRole()
role.PublicId = withPublicId
if err := r.reader.LookupByPublicId(ctx, &role); err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId)))
scopeType, err := getRoleScopeType(ctx, r.reader, withPublicId)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("cannot find scope for role %s", withPublicId))
}
rowsDeleted, err := r.delete(ctx, &role)
var res Resource
switch scopeType {
case scope.Global:
gRole := allocGlobalRole()
gRole.PublicId = withPublicId
res = &gRole
case scope.Org:
oRole := allocOrgRole()
oRole.PublicId = withPublicId
res = &oRole
case scope.Project:
pRole := allocProjectRole()
pRole.PublicId = withPublicId
res = &pRole
default:
return db.NoRowsAffected, errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unknown scope type for role: %s", withPublicId))
}
rowsDeleted, err := r.delete(ctx, res)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId)))
}
@@ -250,11 +361,11 @@ func (r *Repository) listRoles(ctx context.Context, withScopeIds []string, opt .
case opts.withLimit < 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "limit must be non-negative")
}
var args []any
args = append(args, sql.Named("limit", limit))
whereClause := "scope_id in @scope_ids"
args = append(args, sql.Named("scope_ids", withScopeIds))
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause)
args = append(args,
@@ -262,8 +373,7 @@ func (r *Repository) listRoles(ctx context.Context, withScopeIds []string, opt .
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc")}
return r.queryRoles(ctx, whereClause, args, dbOpts...)
return r.queryRoles(ctx, whereClause, args)
}
// listRolesRefresh lists roles in the given scopes and supports the
@@ -278,9 +388,7 @@ func (r *Repository) listRolesRefresh(ctx context.Context, updatedAfter time.Tim
case len(withScopeIds) == 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
opts := getOpts(opt...)
limit := r.defaultLimit
switch {
case opts.withLimit > 0:
@@ -291,6 +399,7 @@ func (r *Repository) listRolesRefresh(ctx context.Context, updatedAfter time.Tim
}
var args []any
args = append(args, sql.Named("limit", limit))
whereClause := "update_time > @updated_after_time and scope_id in @scope_ids"
args = append(args,
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
@@ -303,38 +412,46 @@ func (r *Repository) listRolesRefresh(ctx context.Context, updatedAfter time.Tim
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")}
return r.queryRoles(ctx, whereClause, args, dbOpts...)
return r.queryRoles(ctx, whereClause, args)
}
func (r *Repository) queryRoles(ctx context.Context, whereClause string, args []any, opt ...db.Option) ([]*Role, time.Time, error) {
func (r *Repository) queryRoles(ctx context.Context, whereClause string, args []any) ([]*Role, time.Time, error) {
const op = "iam.(Repository).queryRoles"
var transactionTimestamp time.Time
query := fmt.Sprintf(listRolesQuery, whereClause)
var retRoles []*Role
var retRoleGrantScopes []*RoleGrantScope
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
var inRet []*Role
if err := rd.SearchWhere(ctx, &inRet, whereClause, args, opt...); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query roles"))
var transactionTimestamp time.Time
_, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
rows, err := rd.Query(ctx, query, args)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to execute list roles "))
}
for rows.Next() {
var role Role
if err := rd.ScanRows(ctx, rows, &role); err != nil {
return errors.Wrap(ctx, err, op)
}
retRoles = append(retRoles, &role)
}
if rows.Err() != nil {
return errors.Wrap(ctx, rows.Err(), op)
}
retRoles = inRet
var err error
if len(retRoles) > 0 {
roleIds := make([]string, 0, len(retRoles))
for _, retRole := range retRoles {
roleIds = append(roleIds, retRole.PublicId)
}
retRoleGrantScopes, err = r.ListRoleGrantScopes(ctx, roleIds, WithReaderWriter(rd, w))
retRoleGrantScopes, err = listRoleGrantScopes(ctx, r.reader, roleIds)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query role grant scopes"))
return errors.Wrap(ctx, err, op)
}
}
transactionTimestamp, err = rd.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, err
})
if err != nil {
return nil, time.Time{}, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query roles"))
}
roleGrantScopesMap := make(map[string][]*RoleGrantScope)
for _, rgs := range retRoleGrantScopes {
@@ -389,3 +506,84 @@ func (r *Repository) estimatedRoleCount(ctx context.Context) (int, error) {
}
return count, nil
}
// getRoleScopeType returns scope.Type of the roleId by reading it from the base type iam_role table
// use this to get scope ID to determine which of the role subtype tables to operate on
func getRoleScopeType(ctx context.Context, r db.Reader, roleId string) (scope.Type, error) {
const op = "iam.getRoleScopeType"
if roleId == "" {
return scope.Unknown, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if r == nil {
return scope.Unknown, errors.New(ctx, errors.InvalidParameter, op, "missing db.Reader")
}
rows, err := r.Query(ctx, scopeIdFromRoleIdQuery, []any{sql.Named("public_id", roleId)})
if err != nil {
return scope.Unknown, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to lookup role scope for :%s", roleId)))
}
var scopeIds []string
for rows.Next() {
if err := r.ScanRows(ctx, rows, &scopeIds); err != nil {
return scope.Unknown, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for :%s", roleId)))
}
}
if err := rows.Err(); err != nil {
return scope.Unknown, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for :%s", roleId)))
}
if len(scopeIds) == 0 {
return scope.Unknown, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("role %s not found", roleId))
}
if len(scopeIds) > 1 {
return scope.Unknown, errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", len(scopeIds)))
}
scopeId := scopeIds[0]
switch {
case strings.HasPrefix(scopeId, globals.GlobalPrefix):
return scope.Global, nil
case strings.HasPrefix(scopeId, globals.OrgPrefix):
return scope.Org, nil
case strings.HasPrefix(scopeId, globals.ProjectPrefix):
return scope.Project, nil
default:
return scope.Unknown, fmt.Errorf("unknown scope type for role %s", roleId)
}
}
// getRoleScope returns scope of the role
func getRoleScope(ctx context.Context, r db.Reader, roleId string) (*Scope, error) {
const op = "iam.getRoleScope"
if roleId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if r == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing db.Reader")
}
rows, err := r.Query(ctx, scopeIdFromRoleIdQuery, []any{sql.Named("public_id", roleId)})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to lookup role scope for :%s", roleId)))
}
var scopeIds []string
for rows.Next() {
if err := r.ScanRows(ctx, rows, &scopeIds); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for :%s", roleId)))
}
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for :%s", roleId)))
}
if len(scopeIds) == 0 {
return nil, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("role %s not found", roleId))
}
if len(scopeIds) > 1 {
return nil, errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", len(scopeIds)))
}
scp := AllocScope()
scp.PublicId = scopeIds[0]
err = r.LookupByPublicId(ctx, &scp)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup role scope"))
}
return &scp, nil
}
+286 -118
View File
@@ -5,8 +5,9 @@ package iam
import (
"context"
"database/sql"
"fmt"
"sort"
"slices"
"strings"
"github.com/hashicorp/boundary/globals"
@@ -15,9 +16,12 @@ import (
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/lib/pq"
)
// AddRoleGrant will add role grants associated with the role ID in the
// AddRoleGrants will add role grants associated with the role ID in the
// repository. No options are currently supported. Zero is not a valid value for
// the WithVersion option and will return an error.
func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, _ ...Option) ([]*RoleGrant, error) {
@@ -31,8 +35,6 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi
if roleVersion == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
newRoleGrants := make([]*RoleGrant, 0, len(grants))
for _, grant := range grants {
@@ -43,32 +45,47 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi
newRoleGrants = append(newRoleGrants, roleGrant)
}
scope, err := role.GetScope(ctx, r.reader)
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
var updatedRole Resource
switch scp.GetType() {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
g.Version = roleVersion + 1
updatedRole = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
o.Version = roleVersion + 1
updatedRole = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
p.Version = roleVersion + 1
updatedRole = &p
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId()))
}
roleTicket, err := w.GetTicket(ctx, updatedRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
// We need to update the role version as that's the aggregate
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = uint32(roleVersion) + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
@@ -84,8 +101,8 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
@@ -116,14 +133,11 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe
if roleVersion == 0 {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
scope, err := role.GetScope(ctx, r.reader)
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope to create metadata", roleId)))
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
@@ -135,15 +149,32 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
var updatedRole Resource
switch scp.GetType() {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
g.Version = roleVersion + 1
updatedRole = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
o.Version = roleVersion + 1
updatedRole = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
p.Version = roleVersion + 1
updatedRole = &p
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId()))
}
roleTicket, err := w.GetTicket(ctx, updatedRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = uint32(roleVersion) + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
@@ -200,8 +231,8 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
@@ -234,9 +265,6 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
role := allocRole()
role.PublicId = roleId
// TODO(mgaffney) 08/2020: Use SQL to calculate changes.
// NOTE: Set calculation can safely take place out of the transaction since
@@ -292,12 +320,11 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi
if len(addRoleGrants) == 0 && len(deleteRoleGrants) == 0 {
return currentRoleGrants, db.NoRowsAffected, nil
}
scope, err := role.GetScope(ctx, r.reader)
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
@@ -309,15 +336,33 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
var updatedRole Resource
switch scp.GetType() {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
g.Version = roleVersion + 1
updatedRole = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
o.Version = roleVersion + 1
updatedRole = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
p.Version = roleVersion + 1
updatedRole = &p
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId()))
}
roleTicket, err := w.GetTicket(ctx, updatedRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = roleVersion + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
@@ -351,8 +396,8 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
@@ -387,110 +432,233 @@ func (r *Repository) ListRoleGrants(ctx context.Context, roleId string, opt ...O
return roleGrants, nil
}
// ListRoleGrantScopes returns the grant scopes for the roleId and supports the WithLimit
// option.
func (r *Repository) ListRoleGrantScopes(ctx context.Context, roleIds []string, opt ...Option) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).ListRoleGrantScopes"
if len(roleIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids")
}
query := "?"
var args []any
for i, roleId := range roleIds {
if roleId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids")
}
if i > 0 {
query = query + ", ?"
}
args = append(args, roleId)
}
var roleGrantScopes []*RoleGrantScope
if err := r.list(ctx, &roleGrantScopes, fmt.Sprintf("role_id in (%s)", query), args, opt...); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup role grant scopes"))
}
return roleGrantScopes, nil
type grantsForUserResults struct {
// roleId is the public ID of the role.
roleId string
// roleScopeId is the scope ID of the role.
roleScopeId string
// roleParentScopeId is the parent scope ID of the role.
roleParentScopeId string
// grantScope is the grant scope of the role.
// The valid values are: "individual", "children" and "descendants".
grantScope string
// grantThisRoleScope is a boolean that indicates if the role has a grant
// for itself aka "this" or "individual" scope.
grantThisRoleScope bool
// individualGrantScopes represents the individual grant scopes for the role.
// This is a slice of strings that may be empty if the role does
// not have individual grants.
individualGrantScopes []string
// canonicalGrants represents the canonical grants for the role.
// This is a slice of strings that may be empty if the role does
// not have canonical grants associated with it.
canonicalGrants []string
}
type multiGrantTuple struct {
RoleId string
RoleScopeId string
RoleParentScopeId string
GrantScopeIds string
Grants string
}
func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Option) (perms.GrantTuples, error) {
// GrantsForUser returns perms.GrantTuples associated to a userId scoped down to the requested scope and resource type.
// Use WithRecursive option to indicate that the request is a recursive list request
// Supported options: WithRecursive
func (r *Repository) GrantsForUser(ctx context.Context, userId string, res []resource.Type, reqScopeId string, opt ...Option) (perms.GrantTuples, error) {
const op = "iam.(Repository).GrantsForUser"
if userId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id")
}
opts := getOpts(opt...)
const (
anonUser = `where public_id in (?)`
authUser = `where public_id in ('u_anon', 'u_auth', ?)`
)
var query string
switch userId {
case globals.AnonymousUserId:
query = fmt.Sprintf(grantsForUserQuery, anonUser)
if res == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing resource type")
}
if slices.Contains(res, resource.Unknown) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type cannot be unknown")
}
if slices.Contains(res, resource.All) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type cannot be all")
}
switch {
case strings.HasPrefix(reqScopeId, globals.GlobalPrefix):
case strings.HasPrefix(reqScopeId, globals.OrgPrefix):
case strings.HasPrefix(reqScopeId, globals.ProjectPrefix):
case reqScopeId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing request scope id")
default:
query = fmt.Sprintf(grantsForUserQuery, authUser)
return nil, errors.New(ctx, errors.InvalidParameter, op, "request scope must be global scope, an org scope, or a project scope")
}
var grants []multiGrantTuple
rows, err := r.reader.Query(ctx, query, []any{userId})
// Determine which query to use based on the resources, request scope, and recursive option
opts := getOpts(opt...)
query, err := r.resolveQuery(ctx, res, reqScopeId, opts.withRecursive)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to resolve query"))
}
// Execute the query to get the user's grants
var (
args []any
userIds []string
resources []string
)
switch userId {
case globals.AnonymousUserId:
userIds = []string{globals.AnonymousUserId}
default:
userIds = []string{globals.AnonymousUserId, globals.AnyAuthenticatedUserId, userId}
}
resources = []string{resource.Unknown.String(), resource.All.String()}
for _, res := range res {
resources = append(resources, res.String())
}
args = append(args,
sql.Named("user_ids", pq.Array(userIds)),
sql.Named("resources", pq.Array(resources)),
sql.Named("request_scope_id", reqScopeId),
)
var grants []grantsForUserResults
rows, err := r.reader.Query(ctx, query, args)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
defer rows.Close()
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &grants); err != nil {
var g grantsForUserResults
if err := rows.Scan(
&g.roleId,
&g.roleScopeId,
&g.roleParentScopeId,
&g.grantScope,
&g.grantThisRoleScope,
pq.Array(&g.individualGrantScopes),
pq.Array(&g.canonicalGrants),
); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
grants = append(grants, g)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
ret := make(perms.GrantTuples, 0, len(grants)*3)
ret := make(perms.GrantTuples, 0, len(grants))
for _, grant := range grants {
for _, grantScopeId := range strings.Split(grant.GrantScopeIds, "^") {
for _, canonicalGrant := range strings.Split(grant.Grants, "^") {
if grant.grantScope != globals.GrantScopeIndividual {
for _, canonicalGrant := range grant.canonicalGrants {
gt := perms.GrantTuple{
RoleId: grant.RoleId,
RoleScopeId: grant.RoleScopeId,
RoleParentScopeId: grant.RoleParentScopeId,
GrantScopeId: grantScopeId,
RoleId: grant.roleId,
RoleScopeId: grant.roleScopeId,
RoleParentScopeId: grant.roleParentScopeId,
GrantScopeId: grant.grantScope,
Grant: canonicalGrant,
}
if gt.GrantScopeId == globals.GrantScopeThis || gt.GrantScopeId == "" {
gt.GrantScopeId = grant.RoleScopeId
ret = append(ret, gt)
}
}
if grant.grantThisRoleScope {
switch {
case opts.withRecursive:
// Recursive requests can list the entire scope tree at any request scope
fallthrough
case reqScopeId == grant.roleScopeId:
// Non-recursive requests' role scope must match the request scope
for _, canonicalGrant := range grant.canonicalGrants {
gt := perms.GrantTuple{
RoleId: grant.roleId,
RoleScopeId: grant.roleScopeId,
RoleParentScopeId: grant.roleParentScopeId,
GrantScopeId: grant.roleScopeId,
Grant: canonicalGrant,
}
ret = append(ret, gt)
}
}
}
// loop over grants creating tuple with grant_scope = s.ScopeId
for _, individualGrantScope := range grant.individualGrantScopes {
for _, canonicalGrant := range grant.canonicalGrants {
gt := perms.GrantTuple{
RoleId: grant.roleId,
RoleScopeId: grant.roleScopeId,
RoleParentScopeId: grant.roleParentScopeId,
GrantScopeId: individualGrantScope,
Grant: canonicalGrant,
}
ret = append(ret, gt)
}
}
}
if opts.withTestCacheMultiGrantTuples != nil {
for i, grant := range grants {
grant.testStableSort()
grants[i] = grant
}
*opts.withTestCacheMultiGrantTuples = grants
}
return ret, nil
}
func (m *multiGrantTuple) testStableSort() {
grantScopeIds := strings.Split(m.GrantScopeIds, "^")
sort.Strings(grantScopeIds)
m.GrantScopeIds = strings.Join(grantScopeIds, "^")
gts := strings.Split(m.Grants, "^")
sort.Strings(gts)
m.Grants = strings.Join(gts, "^")
func (r *Repository) resolveQuery(
ctx context.Context,
res []resource.Type,
reqScopeId string,
isRecursive bool,
) (string, error) {
const op = "iam.(Repository).resolveQuery"
if res == nil {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing resource type")
}
if slices.Contains(res, resource.Unknown) {
return "", errors.New(ctx, errors.InvalidParameter, op, "resource type cannot be unknown")
}
if slices.Contains(res, resource.All) {
return "", errors.New(ctx, errors.InvalidParameter, op, "resource type cannot be all")
}
if reqScopeId == "" {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing request scope id")
}
// Use the largest set of allowed scopes for the given resources
var resourceAllowedIn []scope.Type
for _, re := range res {
a, err := scope.AllowedIn(ctx, re)
if err != nil {
return "", errors.Wrap(ctx, err, op)
}
if len(a) > len(resourceAllowedIn) {
resourceAllowedIn = a
}
}
// Recursive query
if isRecursive {
return grantsForUserRecursiveQuery, nil
}
// Non-recursive queries
switch {
case slices.Equal(resourceAllowedIn, []scope.Type{scope.Global}):
if reqScopeId != globals.GlobalPrefix {
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("request scope id must be global for %s resources", res))
}
return grantsForUserGlobalResourcesQuery, nil
case slices.Equal(resourceAllowedIn, []scope.Type{scope.Global, scope.Org}):
switch {
case strings.HasPrefix(reqScopeId, globals.GlobalPrefix):
return grantsForUserGlobalResourcesQuery, nil
case strings.HasPrefix(reqScopeId, globals.OrgPrefix):
return grantsForUserOrgResourcesQuery, nil
default:
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("request scope id must be global or org for %s resources", res))
}
case slices.Equal(resourceAllowedIn, []scope.Type{scope.Global, scope.Org, scope.Project}):
switch {
case strings.HasPrefix(reqScopeId, globals.GlobalPrefix):
return grantsForUserGlobalResourcesQuery, nil
case strings.HasPrefix(reqScopeId, globals.OrgPrefix):
return grantsForUserOrgResourcesQuery, nil
case strings.HasPrefix(reqScopeId, globals.ProjectPrefix):
return grantsForUserProjectResourcesQuery, nil
default:
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope id %s", reqScopeId))
}
case slices.Equal(resourceAllowedIn, []scope.Type{scope.Project}):
if !strings.HasPrefix(reqScopeId, globals.ProjectPrefix) {
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("request scope id must be project for %s resources", res))
}
return grantsForUserProjectResourcesQuery, nil
}
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid resource type: %v", res))
}
File diff suppressed because it is too large. Load diff
+861
View File
@@ -0,0 +1,861 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package iam
import (
"context"
"fmt"
"slices"
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/util"
)
// AddRoleGrantScopes will add role grant scopes associated with the role ID in
// the repository. No options are currently supported. Zero is not a valid value
// for the WithVersion option and will return an error.
func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, _ ...Option) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).AddRoleGrantScopes"
switch {
case roleId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case len(grantScopes) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grant scopes")
case roleVersion == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
if slices.Contains(grantScopes, globals.GrantScopeDescendants) && slices.Contains(grantScopes, globals.GrantScopeChildren) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "only one of descendants or children grant scope can be specified")
}
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
// Find existing grant scopes to find duplicate grants
originalGrantScopes, err := listRoleGrantScopes(ctx, r.reader, []string{roleId})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grant scopes"))
}
originalGrantScopeMap := map[string]struct{}{}
for _, rgs := range originalGrantScopes {
originalGrantScopeMap[rgs.ScopeIdOrSpecial] = struct{}{}
}
// deduplicate and create a map that contains only new grant scope we need to add
toAdd := make(map[string]struct{})
for _, scopeId := range grantScopes {
if _, ok := originalGrantScopeMap[scopeId]; ok {
delete(originalGrantScopeMap, scopeId)
continue
}
toAdd[scopeId] = struct{}{}
}
// no new scope to add so we're returning early
if len(toAdd) == 0 {
return []*RoleGrantScope{}, nil
}
// Allocate a subtype-specific role and manually bump version to ensure that version gets updated
// even when only individual grant scopes, which are stored in separate tables, are modified
updateRole, err := allocRoleScopeGranter(ctx, roleId, scp.GetType())
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to allocate role subtype to add role grant scopes"))
}
updateRole.setVersion(roleVersion + 1)
updateMask := []string{"Version"}
_, addThis := toAdd[globals.GrantScopeThis]
if addThis {
updateRole.setGrantThisRoleScope(true)
updateMask = append(updateMask, "GrantThisRoleScope")
}
// finalGrantScope is used to determine 'grant_scope' value for individualGlobalRoleGrantScopes.
// This only matters if the role is a global role and the grantScopes contains individual project scope, because
// there are 2 possible values: ['individual', 'children'].
// If no grant scope is being added, we must use the current value of iam_role_<type>.grant_scope column
finalGrantScope := globals.GrantScopeIndividual
_, addChildren := toAdd[globals.GrantScopeChildren]
_, addDescendants := toAdd[globals.GrantScopeDescendants]
switch {
case addDescendants:
// cannot add 'descendants' when children is already set, only one hierarchical grant scope can be set
if _, ok := originalGrantScopeMap[globals.GrantScopeChildren]; ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "grant scope children already exists, only one of descendants or children grant scope can be specified")
}
err = updateRole.setGrantScope(ctx, globals.GrantScopeDescendants)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
finalGrantScope = globals.GrantScopeDescendants
case addChildren:
// cannot add 'children' when descendant is already set, only one hierarchical grant scope can be set
if _, ok := originalGrantScopeMap[globals.GrantScopeDescendants]; ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "grant scope descendants already exists, only one of descendants or children grant scope can be specified")
}
err = updateRole.setGrantScope(ctx, globals.GrantScopeChildren)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
finalGrantScope = globals.GrantScopeChildren
default:
// if no hierarchical grant scope is being added, we need to check if 'children' grant is set
// to properly set 'grant_scope' on global role's individual project grant scopes value which has
// two possible values: ['children', 'individual']
// this is an edge case for global role which can have both children and individual project grant scopes
if _, ok := originalGrantScopeMap[globals.GrantScopeChildren]; ok {
finalGrantScope = globals.GrantScopeChildren
}
}
// generate a list of 'individual' scopes that need to be inserted to the database
// excluding the non-individual grant scopes [this, descendants, children]
individualScopesToAdd := make([]string, 0, len(toAdd))
for scopeId := range toAdd {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToAdd = append(individualScopesToAdd, scopeId)
}
}
// return early because there's no new scope to add
if !addDescendants && !addChildren && !addThis && len(individualScopesToAdd) == 0 {
return []*RoleGrantScope{}, nil
}
var retRoleGrantScopes []*RoleGrantScope
var globalRoleOrgGrantScopes []*globalRoleIndividualOrgGrantScope
var globalRoleProjectGrantScopes []*globalRoleIndividualProjectGrantScope
var orgRoleGrantScopes []*orgRoleIndividualGrantScope
switch scp.GetType() {
case scope.Global.String():
globalRoleOrgGrantScopes, globalRoleProjectGrantScopes, err = individualGlobalRoleGrantScope(ctx, roleId, finalGrantScope, individualScopesToAdd)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split global roles grant scopes"))
}
case scope.Org.String():
orgRoleGrantScopes, err = individualOrgGrantScope(ctx, roleId, individualScopesToAdd)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split org roles grant scopes"))
}
default:
// granting individual grant scope to roles is only allowed for roles in global and org scopes
if len(individualScopesToAdd) > 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "individual role grant scope can only be set for global roles or org roles")
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, updateRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, updateRole, updateMask, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role"))
}
if addThis {
if g, ok := updateRole.grantThisRoleScope(); ok {
retRoleGrantScopes = append(retRoleGrantScopes, g)
}
}
if addDescendants || addChildren {
if g, ok := updateRole.grantScope(); ok {
retRoleGrantScopes = append(retRoleGrantScopes, g)
}
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
if len(globalRoleOrgGrantScopes) > 0 {
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleOrgGrantScopes))
if err := w.CreateItems(ctx, globalRoleOrgGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual org grant scopes for global role"))
}
for _, gro := range globalRoleOrgGrantScopes {
retRoleGrantScopes = append(retRoleGrantScopes, gro.roleGrantScope())
}
}
if len(globalRoleProjectGrantScopes) > 0 {
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleProjectGrantScopes))
if err := w.CreateItems(ctx, globalRoleProjectGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scopes for global role"))
}
for _, grp := range globalRoleProjectGrantScopes {
retRoleGrantScopes = append(retRoleGrantScopes, grp.roleGrantScope())
}
}
if len(orgRoleGrantScopes) > 0 {
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(orgRoleGrantScopes))
if err := w.CreateItems(ctx, orgRoleGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scopes for global role"))
}
for _, or := range orgRoleGrantScopes {
retRoleGrantScopes = append(retRoleGrantScopes, or.roleGrantScope())
}
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return retRoleGrantScopes, nil
}
// DeleteRoleGrantScopes will delete role grant scopes associated with the role ID in
// the repository. No options are currently supported. Zero is not a valid value
// for the WithVersion option and will return an error.
// This function returns an 'int' representing number of rows deleted.
func (r *Repository) DeleteRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, _ ...Option) (int, error) {
const op = "iam.(Repository).DeleteRoleGrantScopes"
switch {
case roleId == "":
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case len(grantScopes) == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grant scopes")
case roleVersion == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
originalGrantScopes, err := listRoleGrantScopes(ctx, r.reader, []string{roleId})
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to list role grant scopes"))
}
originalGrantScopeMap := map[string]struct{}{}
for _, rgs := range originalGrantScopes {
originalGrantScopeMap[rgs.ScopeIdOrSpecial] = struct{}{}
}
toRemove := map[string]struct{}{}
// finding grants
for _, s := range grantScopes {
// grants doesn't exist in the original grant scopes so no need to delete
if _, ok := originalGrantScopeMap[s]; !ok {
continue
}
toRemove[s] = struct{}{}
}
// return early if there's nothing to remove
if len(toRemove) == 0 {
return db.NoRowsAffected, nil
}
// totalGrantScopeRemoved for special grant scopes ['this', 'descendants', 'children'] must be counted manually,
// as they are not stored as individual rows. They are stored as columns in the role entry.
// Individual grant scopes still rely on 'rowsDeleted' number returned from 'DeleteItems' calls, so we calculate
// a number that simulates the number of scopes removed.
var totalGrantScopeRemoved int
updateRole, err := allocRoleScopeGranter(ctx, roleId, scp.GetType())
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to allocate role resource"))
}
updateMask := []string{"Version"}
updateRole.setVersion(roleVersion + 1)
_, removeThis := toRemove[globals.GrantScopeThis]
_, removeChildren := toRemove[globals.GrantScopeChildren]
_, removeDescendants := toRemove[globals.GrantScopeDescendants]
// handle case where 'this' grant scope is removed
if removeThis {
updateRole.setGrantThisRoleScope(false)
updateMask = append(updateMask, "GrantThisRoleScope")
// manually bump rows deleted when for deleting 'this' grant scope since this is now
// a DB row update instead of deleting a row.
totalGrantScopeRemoved += 1
}
// handle case where hierarchical grant scope ['children', 'descendants'] is removed
// these grants are mutually exclusive so an OR operation is safe here
if (removeChildren || removeDescendants) && scp.Type != scope.Project.String() {
updateRole.removeGrantScope()
updateMask = append(updateMask, "GrantScope")
// manually bump rows deleted when for deleting hierarchical grant scope since this is now
// a DB row update instead of deleting a row.
totalGrantScopeRemoved += 1
}
// Generate a list of individual grant scopes that need to be removed from the database
// excluding non-individual grant scopes [this, descendants, children]
individualScopesToRemove := make([]string, 0, len(toRemove))
for scopeId := range toRemove {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToRemove = append(individualScopesToRemove, scopeId)
}
}
// split the list of individual scope to remove into type-specific slices
var globalRoleOrgToRemove []*globalRoleIndividualOrgGrantScope
var globalRoleProjToRemove []*globalRoleIndividualProjectGrantScope
var orgRoleProjToRemove []*orgRoleIndividualGrantScope
switch scp.GetType() {
case scope.Global.String():
// projGrantScope can be hardcoded since we're deleting entries, the foreign key check does not apply here
projGrantScope := globals.GrantScopeIndividual
globalRoleOrgToRemove, globalRoleProjToRemove, err = individualGlobalRoleGrantScope(ctx, roleId, projGrantScope, individualScopesToRemove)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split global roles grant scopes"))
}
case scope.Org.String():
orgRoleProjToRemove, err = individualOrgGrantScope(ctx, roleId, individualScopesToRemove)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split org roles grant scopes"))
}
default:
// granting individual grant scope to roles is only allowed for roles in global and org scopes
// but deleting individual grant scopes when the grant scope doesn't exist on a role is allowed
// so we don't return an error here and treat this as a no-op
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, updateRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, updateRole, updateMask, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
if len(globalRoleOrgToRemove) > 0 {
globalRoleOrgGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleOrgToRemove))
rowsDeleted, err := w.DeleteItems(ctx, globalRoleOrgToRemove, db.NewOplogMsgs(&globalRoleOrgGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to remove global role individual org grants"))
}
totalGrantScopeRemoved += rowsDeleted
msgs = append(msgs, globalRoleOrgGrantScopesOplogMsgs...)
}
if len(globalRoleProjToRemove) > 0 {
globalRoleProjGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleOrgToRemove))
rowsDeleted, err := w.DeleteItems(ctx, globalRoleProjToRemove, db.NewOplogMsgs(&globalRoleProjGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to remove global role individual project grants"))
}
totalGrantScopeRemoved += rowsDeleted
msgs = append(msgs, globalRoleProjGrantScopesOplogMsgs...)
}
if len(orgRoleProjToRemove) > 0 {
orgRoleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(orgRoleProjToRemove))
rowsDeleted, err := w.DeleteItems(ctx, orgRoleProjToRemove, db.NewOplogMsgs(&orgRoleGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to remove org role individual project grants"))
}
totalGrantScopeRemoved += rowsDeleted
msgs = append(msgs, orgRoleGrantScopesOplogMsgs...)
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return totalGrantScopeRemoved, nil
}
// SetRoleGrantScopes sets grant scopes on a role (roleId). The role's current
// db version
// must match the roleVersion or an error will be returned. Zero is not a valid
// value for the WithVersion option and will return an error.
func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, opt ...Option) ([]*RoleGrantScope, int, error) {
const op = "iam.(Repository).SetRoleGrantScopes"
switch {
case roleId == "":
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case roleVersion == 0:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
case grantScopes == nil:
// Explicitly set to zero clears, but treat nil as a mistake
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
reader := r.reader
writer := r.writer
needFreshReaderWriter := true
opts := getOpts(opt...)
if !util.IsNil(opts.withReader) && !util.IsNil(opts.withWriter) {
reader = opts.withReader
writer = opts.withWriter
needFreshReaderWriter = false
}
// fetch current role scopes
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
// NOTE: Set calculation can safely take place out of the transaction since
// we are using roleVersion to ensure that we end up operating on the same
// set of data from this query to the final set in the transaction function
// Find existing grant scopes
originalGrantScopes, err := listRoleGrantScopes(ctx, reader, []string{roleId})
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grant scopes"))
}
originalGrantScopeMap := map[string]struct{}{}
for _, rgs := range originalGrantScopes {
originalGrantScopeMap[rgs.ScopeIdOrSpecial] = struct{}{}
}
// finalGrantScopeForIndividualGrantScope is the final value of what 'grant_scope' column of the role will be.
// this is only important for global roles with individual scopes IDs granted to the role.
// A foreign key between iam_role_global.grant_scope and iam_role_global_individual_project_grant_scope.grant_scope
// can either be ['children', 'individual'] which will prevents inserting if the values don't match.
// Assuming that the value is 'individual' and only set it to 'children' if we're adding a 'children' grant
// the current value in the database does not matter and will be overridden by this method
finalGrantScopeForIndividualGrantScope := globals.GrantScopeIndividual
toAdd := make(map[string]struct{})
for _, scopeId := range grantScopes {
if scopeId == globals.GrantScopeChildren {
// set final grant scope to 'children'. finalGrantScopeForIndividualGrantScope will be used later
// when constructing entries for individual project grant scope
// We have to do this before removing the already exist grants in case the role already contains 'children'
// and the caller attempts to set value to 'children' again
finalGrantScopeForIndividualGrantScope = scopeId
}
if _, ok := originalGrantScopeMap[scopeId]; ok {
delete(originalGrantScopeMap, scopeId)
continue
}
toAdd[scopeId] = struct{}{}
}
toRemove := make(map[string]struct{})
for scopeId := range originalGrantScopeMap {
toRemove[scopeId] = struct{}{}
}
// return early since there's no grant scope to add or remove
if len(toAdd) == 0 && len(toRemove) == 0 {
return []*RoleGrantScope{}, db.NoRowsAffected, nil
}
// totalRowsDeleted has to be calculated manually, especially for non-individual grant scopes [this, children, descendants]
// because those are deleted by updating 'GrantThisRoleScope' and 'GrantScope' column on the role table
// totalRowsDeleted are kept in place to maintain the existing contract
totalRowsDeleted := 0
updateRole, err := allocRoleScopeGranter(ctx, roleId, scp.GetType())
if err != nil {
return nil, 0, errors.Wrap(ctx, err, op, errors.WithMsg("unable to allocate role resource"))
}
// bump version manually to force version to change when the role entry doesn't
// version still needs to be bumped when a grant scope is added or removed
updateMask := []string{"Version"}
updateRole.setVersion(roleVersion + 1)
// handle 'this' grant scope - which is now stored in 'GrantThisRoleScope' column
_, removeThis := toRemove[globals.GrantScopeThis]
_, addThis := toAdd[globals.GrantScopeThis]
switch {
case addThis:
updateRole.setGrantThisRoleScope(true)
updateMask = append(updateMask, "GrantThisRoleScope")
case removeThis:
updateRole.setGrantThisRoleScope(false)
updateMask = append(updateMask, "GrantThisRoleScope")
// manually count row deleted since removing 'this' is done by an update to 'grant_this_role_scope' column
// on the role record
totalRowsDeleted++
}
// return early if the there's a conflict in grant_scopes we're trying to add
_, addDescendants := toAdd[globals.GrantScopeDescendants]
_, addChildren := toAdd[globals.GrantScopeChildren]
if addDescendants && addChildren {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "only one of ['children', 'descendants'] can be specified")
}
_, removeDescendants := toRemove[globals.GrantScopeDescendants]
_, removeChildren := toRemove[globals.GrantScopeChildren]
// children and descendants are mutually exclusive so we only need to count row once
if removeDescendants || removeChildren {
// manually count row deleted since removing 'descendants' or 'children' is done by an update to 'grant_scope' column
// on the role record
totalRowsDeleted++
}
// determine the final hierarchical grant scopes stored in `grant_scope` column [`descendants`, `children`]
// depending on if we're adding or removing grants
// if descendants or children is being added, set finalGrantScope to the grant-to-be-added
// to resolve the
switch {
case addDescendants:
err := updateRole.setGrantScope(ctx, globals.GrantScopeDescendants)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
case addChildren:
err := updateRole.setGrantScope(ctx, globals.GrantScopeChildren)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
case removeDescendants || removeChildren:
updateRole.removeGrantScope()
updateMask = append(updateMask, "GrantScope")
}
// generate a list of 'individual' scopes that need to be inserted to the database
// excluding the non-individual grant scopes ['this', 'descendants', 'children']
individualScopesToAdd := make([]string, 0, len(toAdd))
for scopeId := range toAdd {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToAdd = append(individualScopesToAdd, scopeId)
}
}
// generate a list of 'individual' scopes that need to be removed from the database
// excluding the non-individual grant scopes ['this', 'descendants', 'children']
individualScopesToRemove := make([]string, 0, len(toRemove))
for scopeId := range toRemove {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToRemove = append(individualScopesToRemove, scopeId)
}
}
// convert list of individual grant scopes to add and scopes to removed into their respective structs
// these lists will be passed to CreateItems and DeleteItems to create or remove
// individual grant scope entries
var globalRoleIndividualOrgToAdd []*globalRoleIndividualOrgGrantScope
var globalRoleIndividualProjToAdd []*globalRoleIndividualProjectGrantScope
var globalRoleIndividualOrgToRemove []*globalRoleIndividualOrgGrantScope
var globalRoleIndividualProjToRemove []*globalRoleIndividualProjectGrantScope
var orgRoleIndividualScopeToAdd []*orgRoleIndividualGrantScope
var orgRoleIndividualScopeToRemove []*orgRoleIndividualGrantScope
switch scp.Type {
case scope.Global.String():
globalRoleIndividualOrgToAdd, globalRoleIndividualProjToAdd, err = individualGlobalRoleGrantScope(ctx, roleId, finalGrantScopeForIndividualGrantScope, individualScopesToAdd)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert global role individual org scopes to scope specific scope object for grant scope addition"))
}
globalRoleIndividualOrgToRemove, globalRoleIndividualProjToRemove, err = individualGlobalRoleGrantScope(ctx, roleId, finalGrantScopeForIndividualGrantScope, individualScopesToRemove)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert global role individual proj scopes to scope specific scope object for grant scope removal"))
}
case scope.Org.String():
orgRoleIndividualScopeToAdd, err = individualOrgGrantScope(ctx, roleId, individualScopesToAdd)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert org role individual proj scopes to scope specific scope object for grant scope addition"))
}
orgRoleIndividualScopeToRemove, err = individualOrgGrantScope(ctx, roleId, individualScopesToRemove)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert org role individual proj scopes to scope specific scope object for grant scope removal"))
}
default:
if len(individualScopesToRemove) > 0 || len(individualScopesToAdd) > 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op,
fmt.Sprintf("roles in scope type %s does not allow individual role grant scope", scp.Type))
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wra pper"))
}
var retGrantScopes []*RoleGrantScope
txFunc := func(rdr db.Reader, wtr db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := wtr.GetTicket(ctx, updateRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
if len(globalRoleIndividualOrgToRemove) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualOrgToRemove))
rowsDeleted, err := wtr.DeleteItems(ctx, globalRoleIndividualOrgToRemove, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete role global role individual org grant scope"))
}
if rowsDeleted != len(globalRoleIndividualOrgToRemove) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(globalRoleIndividualOrgToRemove)))
}
totalRowsDeleted += rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(globalRoleIndividualProjToRemove) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualProjToRemove))
rowsDeleted, err := wtr.DeleteItems(ctx, globalRoleIndividualProjToRemove, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete global role individual project grant scope"))
}
if rowsDeleted != len(globalRoleIndividualProjToRemove) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(globalRoleIndividualProjToRemove)))
}
totalRowsDeleted += rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(orgRoleIndividualScopeToRemove) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(orgRoleIndividualScopeToRemove))
rowsDeleted, err := wtr.DeleteItems(ctx, orgRoleIndividualScopeToRemove, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete org role individual project grant scope"))
}
if rowsDeleted != len(orgRoleIndividualScopeToRemove) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(orgRoleIndividualScopeToRemove)))
}
totalRowsDeleted += rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
var roleOplogMsg oplog.Message
rowsUpdated, err := wtr.Update(ctx, updateRole, updateMask, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
if len(globalRoleIndividualOrgToAdd) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualOrgToAdd))
if err := wtr.CreateItems(ctx, globalRoleIndividualOrgToAdd, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual org grant scope for global role during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(globalRoleIndividualProjToAdd) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualProjToAdd))
if err := wtr.CreateItems(ctx, globalRoleIndividualProjToAdd, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scope for global role during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(orgRoleIndividualScopeToAdd) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(orgRoleIndividualScopeToAdd))
if err := wtr.CreateItems(ctx, orgRoleIndividualScopeToAdd, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scope for org role during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := wtr.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
allGrantScopes, err := listRoleGrantScopes(ctx, rdr, []string{roleId})
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve current role grant scopes after set"))
}
retGrantScopes = allGrantScopes
return nil
}
if !needFreshReaderWriter {
err = txFunc(reader, writer)
} else {
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
txFunc,
)
}
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return retGrantScopes, totalRowsDeleted, nil
}
// listRoleGrantScopes returns the grant scopes for the roleId
func listRoleGrantScopes(ctx context.Context, reader db.Reader, roleIds []string) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).listRoleGrantScopes"
if len(roleIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids")
}
rows, err := reader.Query(ctx, roleGrantsScopeQuery, []any{roleIds})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query role grant scopes"))
}
if rows.Err() != nil {
return nil, errors.Wrap(ctx, rows.Err(), op, errors.WithMsg("role grant scope rows error"))
}
var result []*RoleGrantScope
for rows.Next() {
if err := reader.ScanRows(ctx, rows, &result); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for: %s", roleIds)))
}
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for: %s", roleIds)))
}
return result, nil
}
// individualGlobalRoleGrantScope parses a list individual grant scope IDs to their corresponding struct representation
// projGrantScope (value of iam_role_global.grant_scope) is required because for individually granted project scope
// has a foreign key enforcement that the iam_role_global_individual_project_grant_scope.grant_scope matches iam_role_global.grant_scope)
// which has two possible values: ['individual', 'children']
func individualGlobalRoleGrantScope(ctx context.Context, roleId string, projGrantScope string, grantScopeIds []string) ([]*globalRoleIndividualOrgGrantScope, []*globalRoleIndividualProjectGrantScope, error) {
const op = "iam.(Repository).individualGlobalRoleGrantScope"
org := make([]*globalRoleIndividualOrgGrantScope, 0, len(grantScopeIds))
proj := make([]*globalRoleIndividualProjectGrantScope, 0, len(grantScopeIds))
for _, rgs := range grantScopeIds {
switch {
case strings.HasPrefix(rgs, globals.OrgPrefix):
org = append(org, &globalRoleIndividualOrgGrantScope{
GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{
RoleId: roleId,
ScopeId: rgs,
GrantScope: globals.GrantScopeIndividual,
},
})
case strings.HasPrefix(rgs, globals.ProjectPrefix):
proj = append(proj, &globalRoleIndividualProjectGrantScope{
GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{
RoleId: roleId,
ScopeId: rgs,
GrantScope: projGrantScope,
},
})
default:
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "grant scope cannot be added to project roles")
}
}
return org, proj, nil
}
// individualOrgGrantScope converts a list of scope IDs into a slice of *orgRoleIndividualGrantScope
func individualOrgGrantScope(ctx context.Context, roleId string, grantScopeIds []string) ([]*orgRoleIndividualGrantScope, error) {
const op = "iam.(Repository).individualOrgGrantScope"
grantScopes := make([]*orgRoleIndividualGrantScope, 0, len(grantScopeIds))
for _, rgs := range grantScopeIds {
if !strings.HasPrefix(rgs, globals.ProjectPrefix) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "individually granted scopes must be project for org role")
}
grantScopes = append(grantScopes, &orgRoleIndividualGrantScope{
OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{
RoleId: roleId,
ScopeId: rgs,
// only individual' is allowed here since so we can hard-code this value
GrantScope: globals.GrantScopeIndividual,
},
})
}
return grantScopes, nil
}
// allocRoleScopeGranter allocates an in-memory instance scope-type specific Role with
func allocRoleScopeGranter(ctx context.Context, roleId string, scopeType string) (roleGrantScopeUpdater, error) {
const op = "iam.(Repository).allocRoleScopeGranter"
var res roleGrantScopeUpdater
switch scopeType {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
res = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
res = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
res = &p
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid role scope")
}
return res, nil
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+94 -46
View File
@@ -16,6 +16,7 @@ import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/resource"
@@ -89,7 +90,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
var adminRolePublicId string
var adminRoleMetadata oplog.Metadata
var adminRole *Role
var adminRole Resource
var adminRoleRaw any
switch {
case userId == "",
@@ -107,17 +108,45 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
_ = adminRole
default:
adminRole, err = NewRole(ctx, scopePublicId)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error instantiating new admin role"))
}
adminRolePublicId, err = newRoleId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating public id for new admin role"))
}
adminRole.PublicId = adminRolePublicId
adminRole.Name = "Administration"
adminRole.Description = fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId)
switch s.Type {
case scope.Global.String():
adminRole = &globalRole{
GlobalRole: &store.GlobalRole{
PublicId: adminRolePublicId,
ScopeId: scopePublicId,
Name: "Administration",
Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId),
GrantThisRoleScope: true,
GrantScope: globals.GrantScopeIndividual,
},
}
case scope.Org.String():
adminRole = &orgRole{
OrgRole: &store.OrgRole{
PublicId: adminRolePublicId,
ScopeId: scopePublicId,
Name: "Administration",
Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId),
GrantThisRoleScope: true,
GrantScope: globals.GrantScopeIndividual,
},
}
case scope.Project.String():
adminRole = &projectRole{
ProjectRole: &store.ProjectRole{
PublicId: adminRolePublicId,
ScopeId: scopePublicId,
GrantThisRoleScope: true,
Name: "Administration",
Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId),
},
}
}
adminRoleRaw = adminRole
adminRoleMetadata = oplog.Metadata{
"resource-public-id": []string{adminRolePublicId},
@@ -130,25 +159,46 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
var defaultRolePublicId string
var defaultRoleMetadata oplog.Metadata
var defaultRole *Role
var defaultRole Resource
var defaultRoleRaw any
if !opts.withSkipDefaultRoleCreation {
defaultRole, err = NewRole(ctx, scopePublicId)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error instantiating new default role"))
}
defaultRolePublicId, err = newRoleId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating public id for new default role"))
}
defaultRole.PublicId = defaultRolePublicId
switch s.Type {
case scope.Global.String():
defaultRole = &globalRole{
GlobalRole: &store.GlobalRole{
PublicId: defaultRolePublicId,
ScopeId: scopePublicId,
Name: "Login and Default Grants",
Description: fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId),
GrantThisRoleScope: true,
GrantScope: globals.GrantScopeIndividual,
},
}
case scope.Org.String():
defaultRole = &orgRole{
OrgRole: &store.OrgRole{
PublicId: defaultRolePublicId,
ScopeId: scopePublicId,
Name: "Login and Default Grants",
Description: fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId),
GrantThisRoleScope: true,
GrantScope: globals.GrantScopeIndividual,
},
}
case scope.Project.String():
defaultRole.Name = "Default Grants"
defaultRole.Description = fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", scopePublicId)
default:
defaultRole.Name = "Login and Default Grants"
defaultRole.Description = fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId)
defaultRole = &projectRole{
ProjectRole: &store.ProjectRole{
PublicId: defaultRolePublicId,
ScopeId: scopePublicId,
Name: "Default Grants",
GrantThisRoleScope: true,
Description: fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", scopePublicId),
},
}
}
defaultRoleRaw = defaultRole
defaultRoleMetadata = oplog.Metadata{
@@ -207,8 +257,18 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
return errors.Wrap(ctx, err, op, errors.WithMsg("error creating role"))
}
adminRole = adminRoleRaw.(*Role)
var adminRoleVersion uint32
switch s.Type {
case scope.Global.String():
adminRoleVersion = adminRoleRaw.(*globalRole).GetVersion()
adminRole = adminRoleRaw.(*globalRole)
case scope.Org.String():
adminRoleVersion = adminRoleRaw.(*orgRole).GetVersion()
adminRole = adminRoleRaw.(*orgRole)
case scope.Project.String():
adminRoleVersion = adminRoleRaw.(*projectRole).GetVersion()
adminRole = adminRoleRaw.(*projectRole)
}
msgs := make([]*oplog.Message, 0, 4)
roleTicket, err := w.GetTicket(ctx, adminRole)
if err != nil {
@@ -217,7 +277,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
// We need to update the role version as that's the aggregate
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, adminRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&adminRole.Version))
rowsUpdated, err := w.Update(ctx, adminRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&adminRoleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version for adding grant"))
}
@@ -237,16 +297,6 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
}
msgs = append(msgs, roleGrantOplogMsgs...)
roleGrantScope, err := NewRoleGrantScope(ctx, adminRolePublicId, globals.GrantScopeThis)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope"))
}
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, 1)
if err := w.CreateItems(ctx, []*RoleGrantScope{roleGrantScope}, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grant scope"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
rolePrincipal, err := NewUserRole(ctx, adminRolePublicId, userId)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role user"))
@@ -261,7 +311,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{s.PublicId},
"scope-type": []string{s.Type},
"resource-public-id": []string{adminRole.PublicId},
"resource-public-id": []string{adminRole.GetPublicId()},
}
if err := w.WriteOplogEntryWith(ctx, childOplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
@@ -280,7 +330,15 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
return errors.Wrap(ctx, err, op, errors.WithMsg("error creating role"))
}
defaultRole = defaultRoleRaw.(*Role)
var defaultRoleVersion uint32
switch s.Type {
case scope.Global.String():
defaultRoleVersion = defaultRoleRaw.(*globalRole).GetVersion()
case scope.Org.String():
defaultRoleVersion = defaultRoleRaw.(*orgRole).GetVersion()
case scope.Project.String():
defaultRoleVersion = defaultRoleRaw.(*projectRole).GetVersion()
}
msgs := make([]*oplog.Message, 0, 7)
roleTicket, err := w.GetTicket(ctx, defaultRole)
@@ -290,7 +348,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
// We need to update the role version as that's the aggregate
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, defaultRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&defaultRole.Version))
rowsUpdated, err := w.Update(ctx, defaultRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&defaultRoleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version for adding grant"))
}
@@ -370,21 +428,11 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o
msgs = append(msgs, roleUserOplogMsgs...)
}
roleGrantScope, err := NewRoleGrantScope(ctx, defaultRolePublicId, globals.GrantScopeThis)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope"))
}
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, 1)
if err := w.CreateItems(ctx, []*RoleGrantScope{roleGrantScope}, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grant scope"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{s.PublicId},
"scope-type": []string{s.Type},
"resource-public-id": []string{defaultRole.PublicId},
"resource-public-id": []string{defaultRole.GetPublicId()},
}
if err := w.WriteOplogEntryWith(ctx, childOplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
+6 -2
View File
@@ -49,7 +49,7 @@ type Cloneable interface {
type ResourceWithScope interface {
GetPublicId() string
GetScopeId() string
validScopeTypes() []scope.Type
getResourceType() resource.Type
}
// LookupScope looks up the resource's scope
@@ -98,7 +98,11 @@ func validateScopeForWrite(ctx context.Context, r db.Reader, resource ResourceWi
return errors.Wrap(ctx, err, op)
}
validScopeType := false
for _, t := range resource.validScopeTypes() {
validScopes, err := scope.AllowedIn(ctx, resource.getResourceType())
if err != nil {
return errors.Wrap(ctx, err, op)
}
for _, t := range validScopes {
if ps.Type == t.String() {
validScopeType = true
}
+522 -47
View File
@@ -6,32 +6,144 @@ package iam
import (
"context"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"google.golang.org/protobuf/proto"
)
const (
defaultRoleTableName = "iam_role"
defaultGlobalRoleTableName = "iam_role_global"
defaultOrgRoleTableName = "iam_role_org"
defaultProjectRoleTableName = "iam_role_project"
)
// roleGrantScopeUpdater represents an internal scope type specific role structs that
// support grant scope columns. Currently this only applies to globalRole and orgRole
// this is used in SetRoleGrantScope, AddRoleGrantScope, DeleteRoleGrantScope
type roleGrantScopeUpdater interface {
// setVersion sets value of `Version` of this role. This is used in
// `repository_grant_scope` operations where version column of the associated role
// has to increase when grant scopes list is changed (add/remove) which is done in a different table.
// This version bump cannot be done by automatically with trigger and is being handled in the application code
setVersion(version uint32)
// setThisGrantScope sets value of `GrantThisRoleScope` of this role which control
// whether this role has 'this' scope granted to it
setGrantThisRoleScope(grantThis bool)
// setGrantScope sets value of `GrantScope` column of this role. The allowed values depends on the scope
// that the role is in
// - global-role: ['descendants', 'children']
// - org-role: ['children']
// - project-role: [] (None)
// This value controls whether hierarchical grant scope is granted to this role
// This method may return error when role does not support hierarchical grant scope (project role)
setGrantScope(ctx context.Context, specialGrant string) error
// removeHierarchicalGrantScope removes all hierarchical grant scopes from a role ['children', 'descendants']
// and sets 'grant_scope' column to 'individual' if required
// - global-role: may remove ['descendants', 'children']
// - org-role: may remove ['children']
// - project-role: no-op as hierarchical grant scope isn't supported for project roles
removeGrantScope()
// GrantThisRoleScope return value of `GrantScopeThis` column as *RoleGrantScope.
// Prior to the grants refactor, `this` grant scope is granted to a role by
// inserting a row to 'role_grant_scope' table, but we've moved on to storing
// 'this' grant as a dedicated column in the type-specific role tables
grantThisRoleScope() (*RoleGrantScope, bool)
// grantScope returns hierarchical grant scopes ['descendants', 'children'] if available.
// returns nil, false if grant_scope is 'individual'
grantScope() (*RoleGrantScope, bool)
}
// Roles are granted permissions and assignable to Users and Groups.
type Role struct {
*store.Role
PublicId string
ScopeId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
GrantScopes []*RoleGrantScope `gorm:"-"`
tableName string `gorm:"-"`
}
func (role *Role) GetPublicId() string {
if role == nil {
return ""
}
return role.PublicId
}
func (role *Role) GetScopeId() string {
if role == nil {
return ""
}
return role.ScopeId
}
func (role *Role) GetName() string {
if role == nil {
return ""
}
return role.Name
}
func (role *Role) GetDescription() string {
if role == nil {
return ""
}
return role.Description
}
func (role *Role) GetCreateTime() *timestamp.Timestamp {
if role == nil {
return nil
}
return role.CreateTime
}
func (role *Role) GetUpdateTime() *timestamp.Timestamp {
if role == nil {
return nil
}
return role.UpdateTime
}
func (role *Role) GetVersion() uint32 {
if role == nil {
return 0
}
return role.Version
}
// ensure that Role implements the interfaces of: Resource, Cloneable, and db.VetForWriter.
var (
_ Resource = (*Role)(nil)
_ Cloneable = (*Role)(nil)
_ db.VetForWriter = (*Role)(nil)
_ roleGrantScopeUpdater = (*globalRole)(nil)
_ Resource = (*globalRole)(nil)
_ Cloneable = (*globalRole)(nil)
_ db.VetForWriter = (*globalRole)(nil)
_ oplog.ReplayableMessage = (*globalRole)(nil)
_ roleGrantScopeUpdater = (*orgRole)(nil)
_ Resource = (*orgRole)(nil)
_ Cloneable = (*orgRole)(nil)
_ db.VetForWriter = (*orgRole)(nil)
_ oplog.ReplayableMessage = (*orgRole)(nil)
_ roleGrantScopeUpdater = (*projectRole)(nil)
_ Resource = (*projectRole)(nil)
_ Cloneable = (*projectRole)(nil)
_ db.VetForWriter = (*projectRole)(nil)
_ oplog.ReplayableMessage = (*projectRole)(nil)
)
// NewRole creates a new in memory role with a scope (project/org)
@@ -43,33 +155,13 @@ func NewRole(ctx context.Context, scopeId string, opt ...Option) (*Role, error)
}
opts := getOpts(opt...)
r := &Role{
Role: &store.Role{
ScopeId: scopeId,
Name: opts.withName,
Description: opts.withDescription,
},
ScopeId: scopeId,
Name: opts.withName,
Description: opts.withDescription,
}
return r, nil
}
func allocRole() Role {
return Role{
Role: &store.Role{},
}
}
// Clone creates a clone of the Role.
func (role *Role) Clone() any {
cp := proto.Clone(role.Role)
ret := &Role{
Role: cp.(*store.Role),
}
for _, grantScope := range role.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
// VetForWrite implements db.VetForWrite() interface.
func (role *Role) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(Role).VetForWrite"
@@ -82,8 +174,8 @@ func (role *Role) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType
return nil
}
func (role *Role) validScopeTypes() []scope.Type {
return []scope.Type{scope.Global, scope.Org, scope.Project}
func (role *Role) getResourceType() resource.Type {
return resource.Role
}
// GetScope returns the scope for the Role.
@@ -106,21 +198,6 @@ func (*Role) Actions() map[string]action.Type {
return ret
}
// TableName returns the tablename to override the default gorm table name.
func (role *Role) TableName() string {
if role.tableName != "" {
return role.tableName
}
return defaultRoleTableName
}
// SetTableName sets the tablename and satisfies the ReplayableMessage
// interface. If the caller attempts to set the name to "" the name will be
// reset to the default name.
func (role *Role) SetTableName(n string) {
role.tableName = n
}
type deletedRole struct {
PublicId string `gorm:"primary_key"`
DeleteTime *timestamp.Timestamp
@@ -130,3 +207,401 @@ type deletedRole struct {
func (s *deletedRole) TableName() string {
return "iam_role_deleted"
}
// globalRole is a type embedding store.GlobalRole used to interact with iam_role_global table which contains
// all iam_role entries that are created in global-level scopes through gorm.
type globalRole struct {
*store.GlobalRole
GrantScopes []*RoleGrantScope `gorm:"-"`
tableName string `gorm:"-"`
}
func (g *globalRole) removeGrantScope() {
if g != nil {
g.GrantScope = globals.GrantScopeIndividual
}
}
func (g *globalRole) setVersion(version uint32) {
if g == nil {
return
}
g.Version = version
}
func (g *globalRole) setGrantThisRoleScope(grantThis bool) {
if g == nil {
return
}
g.GrantThisRoleScope = grantThis
}
func (g *globalRole) setGrantScope(ctx context.Context, specialGrant string) error {
if g != nil {
g.GrantScope = specialGrant
}
return nil
}
func (g *globalRole) grantThisRoleScope() (*RoleGrantScope, bool) {
if g == nil {
return &RoleGrantScope{}, false
}
if !g.GrantThisRoleScope {
return &RoleGrantScope{}, false
}
return &RoleGrantScope{
CreateTime: g.GrantThisRoleScopeUpdateTime,
RoleId: g.PublicId,
ScopeIdOrSpecial: globals.GrantScopeThis,
}, true
}
func (g *globalRole) grantScope() (*RoleGrantScope, bool) {
if g == nil {
return nil, false
}
if g.GrantScope == globals.GrantScopeIndividual {
return nil, false
}
return &RoleGrantScope{
CreateTime: g.GrantScopeUpdateTime,
RoleId: g.PublicId,
ScopeIdOrSpecial: g.GrantScope,
}, true
}
func (g *globalRole) TableName() string {
if g.tableName != "" {
return g.tableName
}
return defaultGlobalRoleTableName
}
func (g *globalRole) SetTableName(n string) {
g.tableName = n
}
func (g *globalRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(globalRole).VetForWrite"
if g.PublicId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
if err := validateScopeForWrite(ctx, r, g, opType, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
func allocGlobalRole() globalRole {
return globalRole{
GlobalRole: &store.GlobalRole{},
}
}
func (g *globalRole) Clone() any {
cp := proto.Clone(g.GlobalRole)
ret := &globalRole{
GlobalRole: cp.(*store.GlobalRole),
}
for _, grantScope := range g.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
func (g *globalRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
return LookupScope(ctx, r, g)
}
func (g *globalRole) GetResourceType() resource.Type { return resource.Role }
func (g *globalRole) getResourceType() resource.Type { return resource.Role }
func (g *globalRole) Actions() map[string]action.Type {
ret := CrudlActions()
ret[action.AddGrants.String()] = action.AddGrants
ret[action.RemoveGrants.String()] = action.RemoveGrants
ret[action.SetGrants.String()] = action.SetGrants
ret[action.AddPrincipals.String()] = action.AddPrincipals
ret[action.RemovePrincipals.String()] = action.RemovePrincipals
ret[action.SetPrincipals.String()] = action.SetPrincipals
return ret
}
func (g *globalRole) toRole() *Role {
if g == nil {
return nil
}
ret := &Role{
PublicId: g.GetPublicId(),
ScopeId: g.GetScopeId(),
Name: g.GetName(),
Description: g.GetDescription(),
CreateTime: g.GetCreateTime(),
UpdateTime: g.GetUpdateTime(),
Version: g.GetVersion(),
}
for _, grantScope := range g.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
// orgRole is a type embedding store.OrgRole used to interact with iam_role_org table which contains
// all iam_role entries that are created in org-level scopes through gorm.
type orgRole struct {
*store.OrgRole
GrantScopes []*RoleGrantScope `gorm:"-"`
tableName string `gorm:"-"`
}
func (o *orgRole) removeGrantScope() {
if o != nil {
o.GrantScope = globals.GrantScopeIndividual
}
}
func (o *orgRole) setVersion(version uint32) {
if o == nil {
return
}
o.Version = version
}
func (o *orgRole) setGrantThisRoleScope(grantThis bool) {
if o == nil {
return
}
o.GrantThisRoleScope = grantThis
}
func (o *orgRole) setGrantScope(ctx context.Context, specialGrant string) error {
if o != nil {
o.GrantScope = specialGrant
}
return nil
}
func (o *orgRole) grantThisRoleScope() (*RoleGrantScope, bool) {
if o == nil {
return &RoleGrantScope{}, false
}
if !o.GrantThisRoleScope {
return &RoleGrantScope{}, false
}
return &RoleGrantScope{
CreateTime: o.GrantThisRoleScopeUpdateTime,
RoleId: o.PublicId,
ScopeIdOrSpecial: globals.GrantScopeThis,
}, true
}
func (o *orgRole) grantScope() (*RoleGrantScope, bool) {
if o == nil {
return nil, false
}
if o.GrantScope == globals.GrantScopeIndividual {
return nil, false
}
return &RoleGrantScope{
CreateTime: o.GrantScopeUpdateTime,
RoleId: o.PublicId,
ScopeIdOrSpecial: o.GrantScope,
}, true
}
func (o *orgRole) TableName() string {
if o.tableName != "" {
return o.tableName
}
return defaultOrgRoleTableName
}
func (o *orgRole) SetTableName(n string) {
o.tableName = n
}
func (o *orgRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(orgRole).VetForWrite"
if o.PublicId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
if err := validateScopeForWrite(ctx, r, o, opType, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
func allocOrgRole() orgRole {
return orgRole{
OrgRole: &store.OrgRole{},
}
}
func (o *orgRole) Clone() any {
cp := proto.Clone(o.OrgRole)
ret := &orgRole{
OrgRole: cp.(*store.OrgRole),
}
for _, grantScope := range o.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
func (o *orgRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
return LookupScope(ctx, r, o)
}
func (o *orgRole) GetResourceType() resource.Type { return resource.Role }
func (o *orgRole) getResourceType() resource.Type { return resource.Role }
func (o *orgRole) Actions() map[string]action.Type {
ret := CrudlActions()
ret[action.AddGrants.String()] = action.AddGrants
ret[action.RemoveGrants.String()] = action.RemoveGrants
ret[action.SetGrants.String()] = action.SetGrants
ret[action.AddPrincipals.String()] = action.AddPrincipals
ret[action.RemovePrincipals.String()] = action.RemovePrincipals
ret[action.SetPrincipals.String()] = action.SetPrincipals
return ret
}
func (o *orgRole) toRole() *Role {
if o == nil {
return nil
}
ret := &Role{
PublicId: o.GetPublicId(),
ScopeId: o.GetScopeId(),
Name: o.GetName(),
Description: o.GetDescription(),
CreateTime: o.GetCreateTime(),
UpdateTime: o.GetUpdateTime(),
Version: o.GetVersion(),
}
for _, grantScope := range o.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
// projectRole is a type embedding store.ProjectRole used to interact with iam_role_project table which contains
// all iam_role entries that are created in project-level scopes through gorm.
type projectRole struct {
*store.ProjectRole
GrantScopes []*RoleGrantScope `gorm:"-"`
tableName string `gorm:"-"`
}
func (p *projectRole) removeGrantScope() {
// no-op since hierarchical isn't supported by project roles
return
}
func (p *projectRole) setGrantScope(ctx context.Context, specialGrant string) error {
const op = "iam.(projectRole).setGrantScope"
return errors.New(ctx, errors.InvalidParameter, op, "hierarchical grant scope is not allowed for project role")
}
func (p *projectRole) grantScope() (*RoleGrantScope, bool) {
return &RoleGrantScope{}, false
}
func (p *projectRole) setVersion(version uint32) {
if p == nil {
return
}
p.Version = version
}
func (p *projectRole) setGrantThisRoleScope(grantThis bool) {
if p == nil {
return
}
p.GrantThisRoleScope = grantThis
}
func (p *projectRole) grantThisRoleScope() (*RoleGrantScope, bool) {
if p == nil {
return &RoleGrantScope{}, false
}
if !p.GrantThisRoleScope {
return &RoleGrantScope{}, false
}
return &RoleGrantScope{
CreateTime: p.GrantThisRoleScopeUpdateTime,
RoleId: p.PublicId,
ScopeIdOrSpecial: globals.GrantScopeThis,
}, true
}
func (p *projectRole) TableName() string {
if p.tableName != "" {
return p.tableName
}
return defaultProjectRoleTableName
}
func (p *projectRole) SetTableName(n string) {
p.tableName = n
}
func (p *projectRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(projectRole).VetForWrite"
if p.PublicId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
if err := validateScopeForWrite(ctx, r, p, opType, opt...); err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
}
func allocProjectRole() projectRole {
return projectRole{
ProjectRole: &store.ProjectRole{},
}
}
func (p *projectRole) Clone() any {
cp := proto.Clone(p.ProjectRole)
ret := &projectRole{
ProjectRole: cp.(*store.ProjectRole),
}
for _, grantScope := range p.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
func (p *projectRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) {
return LookupScope(ctx, r, p)
}
func (p *projectRole) GetResourceType() resource.Type { return resource.Role }
func (p *projectRole) getResourceType() resource.Type { return resource.Role }
func (p *projectRole) Actions() map[string]action.Type {
ret := CrudlActions()
ret[action.AddGrants.String()] = action.AddGrants
ret[action.RemoveGrants.String()] = action.RemoveGrants
ret[action.SetGrants.String()] = action.SetGrants
ret[action.AddPrincipals.String()] = action.AddPrincipals
ret[action.RemovePrincipals.String()] = action.RemovePrincipals
ret[action.SetPrincipals.String()] = action.SetPrincipals
return ret
}
func (p *projectRole) toRole() *Role {
if p == nil {
return nil
}
ret := &Role{
PublicId: p.GetPublicId(),
ScopeId: p.GetScopeId(),
Name: p.GetName(),
Description: p.GetDescription(),
CreateTime: p.GetCreateTime(),
UpdateTime: p.GetUpdateTime(),
Version: p.GetVersion(),
}
for _, grantScope := range p.GrantScopes {
ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope))
}
return ret
}
+208 -29
View File
@@ -6,30 +6,80 @@ package iam
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"google.golang.org/protobuf/proto"
)
const defaultRoleGrantScopeTable = "iam_role_grant_scope"
// RoleGrantScope defines the grant scopes that are assigned to a role
type RoleGrantScope struct {
*store.RoleGrantScope
tableName string `gorm:"-"`
}
const (
defaultRoleGrantScopeTable = "iam_role_grant_scope"
defaultGlobalRoleIndividualOrgGrantScopeTable = "iam_role_global_individual_org_grant_scope"
defaultGlobalRoleIndividualProjectGrantScopeTable = "iam_role_global_individual_project_grant_scope"
defaultOrgRoleIndividualGrantScopeTable = "iam_role_org_individual_grant_scope"
)
// ensure that RoleGrantScope implements the interfaces of: Cloneable and db.VetForWriter
var (
_ Cloneable = (*RoleGrantScope)(nil)
_ db.VetForWriter = (*RoleGrantScope)(nil)
_ Cloneable = (*globalRoleIndividualOrgGrantScope)(nil)
_ db.VetForWriter = (*globalRoleIndividualOrgGrantScope)(nil)
_ oplog.ReplayableMessage = (*globalRoleIndividualOrgGrantScope)(nil)
_ roleGrantScoper = (*globalRoleIndividualOrgGrantScope)(nil)
_ Cloneable = (*globalRoleIndividualProjectGrantScope)(nil)
_ db.VetForWriter = (*globalRoleIndividualProjectGrantScope)(nil)
_ oplog.ReplayableMessage = (*globalRoleIndividualProjectGrantScope)(nil)
_ roleGrantScoper = (*globalRoleIndividualProjectGrantScope)(nil)
_ Cloneable = (*orgRoleIndividualGrantScope)(nil)
_ db.VetForWriter = (*orgRoleIndividualGrantScope)(nil)
_ oplog.ReplayableMessage = (*orgRoleIndividualGrantScope)(nil)
_ roleGrantScoper = (*orgRoleIndividualGrantScope)(nil)
)
// roleGrantScoper is an interface for converting internal grantScopeTypes to exported RoleGrantScope
type roleGrantScoper interface {
roleGrantScope() *RoleGrantScope
}
// RoleGrantScope defines the grant scopes that are assigned to a role
type RoleGrantScope struct {
CreateTime *timestamp.Timestamp
RoleId string
ScopeIdOrSpecial string
}
func (r *RoleGrantScope) GetCreateTime() *timestamp.Timestamp {
if r == nil {
return nil
}
return r.CreateTime
}
func (r *RoleGrantScope) GetRoleId() string {
if r == nil {
return ""
}
return r.RoleId
}
func (r *RoleGrantScope) GetScopeIdOrSpecial() string {
if r == nil {
return ""
}
return r.ScopeIdOrSpecial
}
// NewRoleGrantScope creates a new in memory role grant scope. No options are
// supported.
func NewRoleGrantScope(ctx context.Context, roleId string, grantScope string, _ ...Option) (*RoleGrantScope, error) {
@@ -48,28 +98,19 @@ func NewRoleGrantScope(ctx context.Context, roleId string, grantScope string, _
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown grant scope id %q", grantScope))
}
rgs := &RoleGrantScope{
RoleGrantScope: &store.RoleGrantScope{
RoleId: roleId,
ScopeIdOrSpecial: grantScope,
},
RoleId: roleId,
ScopeIdOrSpecial: grantScope,
}
return rgs, nil
}
func allocRoleGrantScope() RoleGrantScope {
return RoleGrantScope{
RoleGrantScope: &store.RoleGrantScope{},
}
}
// Clone creates a clone of the RoleGrantScope
func (g *RoleGrantScope) Clone() any {
cp := proto.Clone(g.RoleGrantScope)
return &RoleGrantScope{
RoleGrantScope: cp.(*store.RoleGrantScope),
CreateTime: g.CreateTime,
RoleId: g.RoleId,
ScopeIdOrSpecial: g.ScopeIdOrSpecial,
}
}
@@ -96,17 +137,155 @@ func (g *RoleGrantScope) VetForWrite(ctx context.Context, _ db.Reader, _ db.OpTy
return nil
}
// TableName returns the tablename to override the default gorm table name
func (g *RoleGrantScope) TableName() string {
// globalRoleIndividualOrgGrantScope defines the grant org scopes
// that are assigned to a global role
type globalRoleIndividualOrgGrantScope struct {
*store.GlobalRoleIndividualOrgGrantScope
tableName string `gorm:"-"`
}
func (g *globalRoleIndividualOrgGrantScope) roleGrantScope() *RoleGrantScope {
if g == nil {
return nil
}
return &RoleGrantScope{
CreateTime: g.CreateTime,
RoleId: g.RoleId,
ScopeIdOrSpecial: g.GetScopeId(),
}
}
func (g *globalRoleIndividualOrgGrantScope) TableName() string {
if g.tableName != "" {
return g.tableName
}
return defaultRoleGrantScopeTable
return defaultGlobalRoleIndividualOrgGrantScopeTable
}
// SetTableName sets the tablename and satisfies the ReplayableMessage
// interface. If the caller attempts to set the name to "" the name will be
// reset to the default name.
func (g *RoleGrantScope) SetTableName(n string) {
g.tableName = n
func (g *globalRoleIndividualOrgGrantScope) SetTableName(name string) {
g.tableName = name
}
func (g *globalRoleIndividualOrgGrantScope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(GlobalRoleIndividualOrgGrantScope).VetForWrite"
if g.RoleId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if g.ScopeId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if globals.ResourceInfoFromPrefix(g.ScopeId).Type != resource.Scope &&
strings.HasPrefix(g.String(), globals.OrgPrefix) {
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope ID %s", g.ScopeId))
}
return nil
}
func (g *globalRoleIndividualOrgGrantScope) Clone() any {
cp := proto.Clone(g.GlobalRoleIndividualOrgGrantScope)
return &globalRoleIndividualOrgGrantScope{
GlobalRoleIndividualOrgGrantScope: cp.(*store.GlobalRoleIndividualOrgGrantScope),
}
}
// globalRoleIndividualProjectGrantScope defines the grant project scopes
// that are assigned to a global role
type globalRoleIndividualProjectGrantScope struct {
*store.GlobalRoleIndividualProjectGrantScope
tableName string `gorm:"-"`
}
func (g *globalRoleIndividualProjectGrantScope) roleGrantScope() *RoleGrantScope {
if g == nil {
return nil
}
return &RoleGrantScope{
CreateTime: g.GetCreateTime(),
RoleId: g.GetRoleId(),
ScopeIdOrSpecial: g.GetScopeId(),
}
}
func (g *globalRoleIndividualProjectGrantScope) TableName() string {
if g.tableName != "" {
return g.tableName
}
return defaultGlobalRoleIndividualProjectGrantScopeTable
}
func (g *globalRoleIndividualProjectGrantScope) SetTableName(name string) {
g.tableName = name
}
func (g *globalRoleIndividualProjectGrantScope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(GlobalRoleIndividualProjectGrantScope).VetForWrite"
if g.RoleId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if g.ScopeId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if globals.ResourceInfoFromPrefix(g.ScopeId).Type != resource.Scope &&
strings.HasPrefix(g.String(), globals.ProjectPrefix) {
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope ID %s", g.ScopeId))
}
return nil
}
func (g *globalRoleIndividualProjectGrantScope) Clone() any {
cp := proto.Clone(g.GlobalRoleIndividualProjectGrantScope)
return &globalRoleIndividualProjectGrantScope{
GlobalRoleIndividualProjectGrantScope: cp.(*store.GlobalRoleIndividualProjectGrantScope),
}
}
// OrgRoleIndividualGrantScope defines the grant project scopes
// that are assigned to an org role
type orgRoleIndividualGrantScope struct {
*store.OrgRoleIndividualGrantScope
tableName string `gorm:"-"`
}
func (g *orgRoleIndividualGrantScope) roleGrantScope() *RoleGrantScope {
if g == nil {
return nil
}
return &RoleGrantScope{
CreateTime: g.GetCreateTime(),
RoleId: g.GetRoleId(),
ScopeIdOrSpecial: g.GetScopeId(),
}
}
func (g *orgRoleIndividualGrantScope) TableName() string {
if g.tableName != "" {
return g.tableName
}
return defaultOrgRoleIndividualGrantScopeTable
}
func (g *orgRoleIndividualGrantScope) SetTableName(name string) {
g.tableName = name
}
func (g *orgRoleIndividualGrantScope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error {
const op = "iam.(OrgRoleIndividualGrantScope).VetForWrite"
if g.RoleId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if g.ScopeId == "" {
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
if globals.ResourceInfoFromPrefix(g.ScopeId).Type != resource.Scope &&
strings.HasPrefix(g.String(), globals.ProjectPrefix) {
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope ID %s", g.ScopeId))
}
return nil
}
func (g *orgRoleIndividualGrantScope) Clone() any {
cp := proto.Clone(g.OrgRoleIndividualGrantScope)
return &orgRoleIndividualGrantScope{
OrgRoleIndividualGrantScope: cp.(*store.OrgRoleIndividualGrantScope),
}
}
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more