-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(coderd/rbac): compare scopes by permission coverage #28167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
a672066
feat(coderd/rbac): compare scopes by permission coverage
BobbyHo 4315706
Merge branch 'main' into plat479-1-rbac-scope-coverage
BobbyHo 787c461
docs(coderd/rbac): shorten the ScopesCover doc comment
BobbyHo 2d39e04
Merge branch 'main' into plat479-1-rbac-scope-coverage
BobbyHo 2f6c44e
fix(coderd/rbac): guard allowed-side org and user permissions
BobbyHo bd40270
refactor(coderd/rbac): drop the unreachable negative skip in coverage
BobbyHo 3139c54
test(coderd/rbac): pin the scope coverage table's weak assertions
BobbyHo 9276bb8
refactor(coderd/rbac): make the coverage guards reachable from tests
BobbyHo 865eb9a
refactor(coderd/rbac): share the alias table and name the canonical c…
BobbyHo 26a6bed
docs(coderd/rbac): document the expansion invariant where it can be b…
BobbyHo 7cca7b3
Merge branch 'main' into plat479-1-rbac-scope-coverage
BobbyHo 1678a77
test(coderd/rbac): pin the coverage guards at one strength
BobbyHo aba3c6c
fix(coderd/rbac): name the scope once in expansion errors
BobbyHo 9a08105
docs(coderd/rbac): correct the external scope list contract
BobbyHo 2fcce8b
docs(coderd/rbac): trim the restated coverage invariant
BobbyHo a775a48
docs(coderd/rbac): correct the negative permission cross-reference
BobbyHo e0c0d4a
docs(coderd/rbac): name every category IsExternalScope admits
BobbyHo 189740d
test(coderd/rbac): pin the alias list invariants on the alias table
BobbyHo 7d08e49
Merge branch 'main' into plat479-1-rbac-scope-coverage
BobbyHo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| package rbac | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/coder/coder/v2/coderd/rbac/policy" | ||
| ) | ||
|
|
||
| var ( | ||
| workspaceRead = Permission{ResourceType: "workspace", Action: policy.ActionRead} | ||
| workspaceWildcard = Permission{ResourceType: "workspace", Action: policy.WildcardSymbol} | ||
| workspaceDeleteNegate = Permission{ResourceType: "workspace", Action: policy.ActionDelete, Negate: true} | ||
| ) | ||
|
|
||
| // coverableScope is the shape every ExpandScope result has: site permissions | ||
| // only, wildcard allow list, no negatives. | ||
| func coverableScope(perms ...Permission) Scope { | ||
| return Scope{ | ||
| Role: Role{Site: perms}, | ||
| AllowIDList: []AllowListElement{AllowListAll()}, | ||
| } | ||
| } | ||
|
|
||
| // TestScopeAliases asserts what the shared table exists to guarantee, which no | ||
| // test outside this package can: every alias is public, resolves to a public | ||
| // name, and resolves to one the RBAC layer can expand. A name IsExternalScope | ||
| // calls public but ExpandScope rejects is requestable in name only, and every | ||
| // request naming it fails. Iterating the table rather than naming the two | ||
| // aliases means a third is covered the day it is added. | ||
| func TestScopeAliases(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| require.NotEmpty(t, scopeAliases) | ||
|
|
||
| for alias, canonical := range scopeAliases { | ||
| require.Truef(t, IsExternalScope(alias), "alias %q must be public", alias) | ||
| require.Equalf(t, canonical, CanonicalScopeName(alias), "alias %q", alias) | ||
|
|
||
| // An alias is a second spelling of a scope, not a scope of its own. | ||
| require.Truef(t, IsExternalScope(canonical), "canonical %q must be public", canonical) | ||
| _, err := ExpandScope(canonical) | ||
| require.NoErrorf(t, err, "canonical %q must expand", canonical) | ||
|
|
||
| // The alias itself does not expand, which is what makes | ||
| // canonicalization mandatory before storage or coverage rather than a | ||
| // tidying step callers may skip. | ||
| _, err = ExpandScope(alias) | ||
| require.Errorf(t, err, "alias %q must not expand directly", alias) | ||
|
|
||
| // The list a client reads offers the canonical spelling and only that | ||
| // one, so a caller can request a name from it and store what it | ||
| // requested. Listing the alias too would offer two names for one scope, | ||
| // one of which fails to expand once stored. | ||
| require.NotContainsf(t, ExternalScopeNames(), string(alias), "list must omit alias %q", alias) | ||
| require.Containsf(t, ExternalScopeNames(), string(canonical), "list must offer %q", canonical) | ||
| } | ||
| } | ||
|
BobbyHo marked this conversation as resolved.
|
||
|
|
||
| // TestScopesCoverGuards drives Scope values that no catalog entry produces. | ||
| // The guards exist for authority ScopeName inputs cannot express today, so | ||
| // ScopesCover cannot reach them and they would otherwise ship unverified. | ||
| // | ||
| // Each shape runs on both sides of the comparison, with the opposite side | ||
| // coverable, so a guard consulted on only one side fails here. | ||
| func TestScopesCoverGuards(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| scope Scope | ||
| wantErr string | ||
| }{ | ||
| { | ||
| name: "SitePermissionsOnly", | ||
| scope: coverableScope(workspaceRead), | ||
| }, | ||
| { | ||
| name: "UserPermission", | ||
| scope: Scope{ | ||
| Role: Role{ | ||
| Site: []Permission{workspaceRead}, | ||
| User: []Permission{workspaceRead}, | ||
| }, | ||
| AllowIDList: []AllowListElement{AllowListAll()}, | ||
| }, | ||
| wantErr: "grants org or user permissions", | ||
| }, | ||
| { | ||
| // The case the guards were added for: a scope granting every | ||
| // workspace action except delete. The permission coverage reads is | ||
| // harmless, and the one it does not read carves delete back out, so | ||
| // comparing on Site alone would answer a request for | ||
| // workspace:delete from a wildcard the scope has already qualified. | ||
| name: "NegativeUserPermission", | ||
| scope: Scope{ | ||
| Role: Role{ | ||
| Site: []Permission{workspaceWildcard}, | ||
| User: []Permission{workspaceDeleteNegate}, | ||
| }, | ||
| AllowIDList: []AllowListElement{AllowListAll()}, | ||
| }, | ||
| wantErr: "grants org or user permissions", | ||
| }, | ||
| { | ||
| name: "OrgPermission", | ||
| scope: Scope{ | ||
| Role: Role{ | ||
| Site: []Permission{workspaceRead}, | ||
| ByOrgID: map[string]OrgPermissions{"00000000-0000-0000-0000-000000000001": {}}, | ||
| }, | ||
| AllowIDList: []AllowListElement{AllowListAll()}, | ||
| }, | ||
| wantErr: "grants org or user permissions", | ||
| }, | ||
| { | ||
| name: "NegativeSitePermission", | ||
| scope: coverableScope(workspaceWildcard, workspaceDeleteNegate), | ||
| wantErr: "carries a negative permission", | ||
| }, | ||
| { | ||
| name: "NarrowedAllowList", | ||
| scope: Scope{ | ||
| Role: Role{Site: []Permission{workspaceRead}}, | ||
| AllowIDList: []AllowListElement{{Type: "workspace", ID: "00000000-0000-0000-0000-000000000002"}}, | ||
| }, | ||
| wantErr: "carries a resource allow list", | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // The opposite side always covers, so a guard error stays | ||
| // distinguishable from an ordinary uncovered result. | ||
| cleanGrant := namedScope{name: "clean_scope", scope: coverableScope(workspaceWildcard)} | ||
| cleanRequest := namedScope{name: "clean_scope", scope: coverableScope(workspaceRead)} | ||
| under := namedScope{name: "test_scope", scope: test.scope} | ||
|
|
||
| sides := []struct { | ||
| side string | ||
| allowed []namedScope | ||
| requested namedScope | ||
| }{ | ||
| {side: coverageSideRequested, allowed: []namedScope{cleanGrant}, requested: under}, | ||
| {side: coverageSideAllowed, allowed: []namedScope{under}, requested: cleanRequest}, | ||
| } | ||
|
|
||
| for _, args := range sides { | ||
| side := args.side | ||
| got, err := scopesCoverExpanded(args.allowed, args.requested) | ||
| if test.wantErr == "" { | ||
| require.NoErrorf(t, err, "side %q", side) | ||
| require.Truef(t, got, "side %q", side) | ||
| continue | ||
| } | ||
| require.ErrorContainsf(t, err, test.wantErr, "side %q", side) | ||
| // The side names itself, so an operator reading the error can | ||
| // tell which half of the comparison was undecidable. | ||
| require.ErrorContainsf(t, err, side+` scope "test_scope"`, "side %q", side) | ||
| require.Falsef(t, got, "an undecided comparison must not report coverage, side %q", side) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.