Compare commits

...

61 Commits

Author SHA1 Message Date
Steven Masley 2c084f93b3 test: unit test for org member vs org vs user permissions 2025-10-21 14:45:27 -05:00
ケイラ b6d3986c7c fix app sharing permission check 2025-10-20 15:58:52 +00:00
ケイラ 1490f9931e hmm 2025-10-20 15:27:46 +00:00
ケイラ 016f94b1fc so that did nothing I guess 2025-10-20 15:17:50 +00:00
ケイラ 0eaeba4d2c :| 2025-10-20 14:46:31 +00:00
ケイラ 6110478654 test this out for my sanity 2025-10-20 14:37:55 +00:00
ケイラ 6cc2f51331 add OrganizationID to readOrgWorkspaces test 2025-10-18 02:53:32 +00:00
ケイラ 29dd0dbd6d Update README.md 2025-10-18 02:02:57 +00:00
ケイラ dcb80b0e35 idk man 2025-10-18 02:01:22 +00:00
ケイラ b6d9c1da50 YES 2025-10-18 01:52:40 +00:00
ケイラ 9f668794dc fix subjectSubAgentAPI 2025-10-17 21:47:47 +00:00
ケイラ 783ebc8bfa fix tables 2025-10-17 21:40:34 +00:00
ケイラ 5e24dfa215 fix some merging 2025-10-17 21:27:06 +00:00
ケイラ bd42a53b7c Delete policy.md 2025-10-17 21:20:27 +00:00
ケイラ e46ccc1512 Update typesGenerated.ts 2025-10-17 20:38:57 +00:00
ケイラ 1b70613736 Merge branch 'lilac/policy-refactor' into lilac/organization-member-level 2025-10-17 20:16:07 +00:00
ケイラ 66300fb085 Merge branch 'main' into lilac/policy-refactor 2025-10-17 20:15:23 +00:00
ケイラ 258d7f57c6 Merge branch 'lilac/policy-refactor' into lilac/organization-member-level 2025-10-17 20:14:13 +00:00
ケイラ 9105bcadff Merge branch 'main' into lilac/organization-member-level 2025-10-17 20:13:00 +00:00
ケイラ edfa66bf37 oh boy 2025-10-17 19:52:01 +00:00
ケイラ 2d1e8a6751 undo some reordering that doesn't make sense yet to clean up the diff 2025-10-17 19:27:24 +00:00
ケイラ f66c12bc37 smol 2025-10-17 19:21:06 +00:00
ケイラ 60eecef3cd geez louis 2025-10-17 18:56:52 +00:00
ケイラ 7059b50fbb put back org_ok definition for now too 2025-10-17 18:52:31 +00:00
ケイラ 36467b879c typo 2025-10-17 18:50:18 +00:00
ケイラ f4a37df64b put back org_ok in user paths until members get put back 2025-10-17 18:46:38 +00:00
ケイラ 6ff1c94429 remove org members for now 2025-10-17 18:31:29 +00:00
ケイラ 9ad6df779f some more polish 2025-10-17 18:28:21 +00:00
ケイラ 7af9fdbe8a refactor: clean up policy.rego 2025-10-17 17:45:08 +00:00
ケイラ 246d6bef06 Merge branch 'lilac/kayla-is-trying-really-hard-to-figure-out-permissions-things' into lilac/organization-member-level 2025-10-17 17:22:42 +00:00
ケイラ 96bee81f50 oh boy 2025-10-17 17:14:41 +00:00
ケイラ 169edcfe32 silly tweaks 2025-10-16 21:18:43 +00:00
ケイラ c8ee467700 oops 2025-10-16 21:07:12 +00:00
ケイラ 5fe9ad0599 when we were yooooung 2025-10-16 01:16:56 +00:00
ケイラ e2ddd24270 Update policy.rego 2025-10-16 00:49:12 +00:00
ケイラ b4bbab92ca tweak 2025-10-16 00:42:58 +00:00
ケイラ 6b45ad4485 some fixes 2025-10-16 00:34:38 +00:00
ケイラ af62d1a470 formatting 2025-10-16 00:05:43 +00:00
ケイラ b7bac866b4 try to document the stuff 2025-10-16 00:01:12 +00:00
ケイラ 24025452e0 do the stuff 2025-10-15 23:02:59 +00:00
ケイラ 4e3956e2bf Merge branch 'main' into lilac/organization-member-level 2025-10-15 20:51:12 +00:00
ケイラ c80ffec9b2 small patches 2025-10-09 21:07:10 +00:00
ケイラ 494b93fbf9 oop 2025-10-09 20:43:33 +00:00
ケイラ 9978583fa1 Merge branch 'main' into lilac/organization-member-level 2025-10-09 20:37:03 +00:00
ケイラ fd718450ff Merge branch 'lilac/by-org-id' into lilac/organization-member-level 2025-10-07 22:01:00 +00:00
ケイラ a648977160 Merge branch 'main' into lilac/organization-member-level 2025-10-07 21:51:40 +00:00
ケイラ 6585fe02d6 group permissions by org id 2025-10-07 21:27:06 +00:00
Steven Masley b8446debfa chore: update rego to combined org + member permissions 2025-10-06 15:57:28 -05:00
ケイラ 52f1d1cd5e ByOrgID 2025-10-06 20:48:20 +00:00
Steven Masley 6c646214cf Add more perms to org members 2025-10-02 11:17:22 -05:00
Steven Masley dcf52f838c fix policy with any_org partial 2025-10-02 11:04:45 -05:00
Steven Masley 70651c68e1 update tests and policy 2025-10-02 09:46:28 -05:00
Steven Masley ff6552e008 update roles 2025-10-02 09:30:57 -05:00
Steven Masley 4344ed2367 update rego policy to match new table 2025-10-02 09:24:34 -05:00
Steven Masley 945b0cb879 update readme table for rbac document 2025-10-01 15:03:03 -05:00
ケイラ 8135c68fc4 (probably terrible) vibes 2025-09-26 21:47:10 +00:00
ケイラ c24d0dc44f ok well one of you did something 2025-09-26 17:06:33 +00:00
ケイラ 74a6c99186 fixish 2025-09-26 16:35:54 +00:00
ケイラ b748e6aa88 you 2025-09-25 21:38:56 +00:00
ケイラ 3f0a5bf86c :^) 2025-09-24 23:14:19 +00:00
ケイラ 77432a2335 start adding an organization member permission level 2025-09-24 23:06:54 +00:00
24 changed files with 1025 additions and 556 deletions
+21
View File
@@ -12344,6 +12344,13 @@ const docTemplate = `{
"type": "string",
"format": "uuid"
},
"organization_member_permissions": {
"description": "OrganizationMemberPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Permission"
}
},
"organization_permissions": {
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
@@ -13567,6 +13574,13 @@ const docTemplate = `{
"name": {
"type": "string"
},
"organization_member_permissions": {
"description": "OrganizationMemberPermissions are specific to the organization the role belongs to.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Permission"
}
},
"organization_permissions": {
"description": "OrganizationPermissions are specific to the organization the role belongs to.",
"type": "array",
@@ -17312,6 +17326,13 @@ const docTemplate = `{
"type": "string",
"format": "uuid"
},
"organization_member_permissions": {
"description": "OrganizationMemberPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Permission"
}
},
"organization_permissions": {
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
+21
View File
@@ -11042,6 +11042,13 @@
"type": "string",
"format": "uuid"
},
"organization_member_permissions": {
"description": "OrganizationMemberPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Permission"
}
},
"organization_permissions": {
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
@@ -12197,6 +12204,13 @@
"name": {
"type": "string"
},
"organization_member_permissions": {
"description": "OrganizationMemberPermissions are specific to the organization the role belongs to.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Permission"
}
},
"organization_permissions": {
"description": "OrganizationPermissions are specific to the organization the role belongs to.",
"type": "array",
@@ -15824,6 +15838,13 @@
"type": "string",
"format": "uuid"
},
"organization_member_permissions": {
"description": "OrganizationMemberPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.Permission"
}
},
"organization_permissions": {
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
"type": "array",
+11 -10
View File
@@ -50,6 +50,13 @@ func TestCheckPermissions(t *testing.T) {
},
Action: "read",
},
readOrgWorkspaces: {
Object: codersdk.AuthorizationObject{
ResourceType: codersdk.ResourceWorkspace,
OrganizationID: adminUser.OrganizationID.String(),
},
Action: "read",
},
readMyself: {
Object: codersdk.AuthorizationObject{
ResourceType: codersdk.ResourceUser,
@@ -58,16 +65,10 @@ func TestCheckPermissions(t *testing.T) {
Action: "read",
},
readOwnWorkspaces: {
Object: codersdk.AuthorizationObject{
ResourceType: codersdk.ResourceWorkspace,
OwnerID: "me",
},
Action: "read",
},
readOrgWorkspaces: {
Object: codersdk.AuthorizationObject{
ResourceType: codersdk.ResourceWorkspace,
OrganizationID: adminUser.OrganizationID.String(),
OwnerID: "me",
},
Action: "read",
},
@@ -92,9 +93,9 @@ func TestCheckPermissions(t *testing.T) {
UserID: adminUser.UserID,
Check: map[string]bool{
readAllUsers: true,
readOrgWorkspaces: true,
readMyself: true,
readOwnWorkspaces: true,
readOrgWorkspaces: true,
updateSpecificTemplate: true,
},
},
@@ -104,9 +105,9 @@ func TestCheckPermissions(t *testing.T) {
UserID: orgAdminUser.ID,
Check: map[string]bool{
readAllUsers: true,
readOrgWorkspaces: true,
readMyself: true,
readOwnWorkspaces: true,
readOrgWorkspaces: true,
updateSpecificTemplate: true,
},
},
@@ -116,9 +117,9 @@ func TestCheckPermissions(t *testing.T) {
UserID: memberUser.ID,
Check: map[string]bool{
readAllUsers: false,
readOrgWorkspaces: false,
readMyself: true,
readOwnWorkspaces: true,
readOrgWorkspaces: false,
updateSpecificTemplate: false,
},
},
+8 -7
View File
@@ -695,12 +695,13 @@ func RBACRole(role rbac.Role) codersdk.Role {
orgPerms := role.ByOrgID[slim.OrganizationID]
return codersdk.Role{
Name: slim.Name,
OrganizationID: slim.OrganizationID,
DisplayName: slim.DisplayName,
SitePermissions: List(role.Site, RBACPermission),
OrganizationPermissions: List(orgPerms.Org, RBACPermission),
UserPermissions: List(role.User, RBACPermission),
Name: slim.Name,
OrganizationID: slim.OrganizationID,
DisplayName: slim.DisplayName,
SitePermissions: List(role.Site, RBACPermission),
UserPermissions: List(role.User, RBACPermission),
OrganizationPermissions: List(orgPerms.Org, RBACPermission),
OrganizationMemberPermissions: List(orgPerms.Member, RBACPermission),
}
}
@@ -715,8 +716,8 @@ func Role(role database.CustomRole) codersdk.Role {
OrganizationID: orgID,
DisplayName: role.DisplayName,
SitePermissions: List(role.SitePermissions, Permission),
OrganizationPermissions: List(role.OrgPermissions, Permission),
UserPermissions: List(role.UserPermissions, Permission),
OrganizationPermissions: List(role.OrgPermissions, Permission),
}
}
+26 -13
View File
@@ -395,11 +395,13 @@ var (
Identifier: rbac.RoleIdentifier{Name: "subagentapi"},
DisplayName: "Sub Agent API",
Site: []rbac.Permission{},
User: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{
orgID.String(): {},
orgID.String(): {
Member: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreateAgent, policy.ActionDeleteAgent},
}),
},
},
},
}),
@@ -1257,8 +1259,8 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole)
}
if len(rbacRole.ByOrgID) > 0 && len(rbacRole.Site) > 0 {
// This is a choice to keep roles simple. If we allow mixing site and org scoped perms, then knowing who can
// do what gets more complicated.
// This is a choice to keep roles simple. If we allow mixing site and org
// scoped perms, then knowing who can do what gets more complicated.
return xerrors.Errorf("invalid custom role, cannot assign both org and site permissions at the same time")
}
@@ -1279,7 +1281,18 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole)
for _, orgPerm := range perms.Org {
err := q.customRoleEscalationCheck(ctx, act, orgPerm, rbac.Object{OrgID: orgID, Type: orgPerm.ResourceType})
if err != nil {
return xerrors.Errorf("org=%q: %w", orgID, err)
return xerrors.Errorf("org=%q: org: %w", orgID, err)
}
}
for _, memberPerm := range perms.Member {
// The person giving the permission should still be required to have
// the permissions throughout the org in order to give individuals the
// same permission among their own resources, since the role can be given
// to anyone. The `Owner` is intentionally omitted from the `Object` to
// enforce this.
err := q.customRoleEscalationCheck(ctx, act, memberPerm, rbac.Object{OrgID: orgID, Type: memberPerm.ResourceType})
if err != nil {
return xerrors.Errorf("org=%q: member: %w", orgID, err)
}
}
}
@@ -1297,8 +1310,8 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole)
func (q *querier) authorizeProvisionerJob(ctx context.Context, job database.ProvisionerJob) error {
switch job.Type {
case database.ProvisionerJobTypeWorkspaceBuild:
// Authorized call to get workspace build. If we can read the build, we
// can read the job.
// Authorized call to get workspace build. If we can read the build, we can
// read the job.
_, err := q.GetWorkspaceBuildByJobID(ctx, job.ID)
if err != nil {
return xerrors.Errorf("fetch related workspace build: %w", err)
@@ -1341,8 +1354,8 @@ func (q *querier) ActivityBumpWorkspace(ctx context.Context, arg database.Activi
}
func (q *querier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) {
// Although this technically only reads users, only system-related functions should be
// allowed to call this.
// Although this technically only reads users, only system-related functions
// should be allowed to call this.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
@@ -1361,8 +1374,8 @@ func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg databas
}
func (q *querier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg database.BatchUpdateWorkspaceLastUsedAtParams) error {
// Could be any workspace and checking auth to each workspace is overkill for the purpose
// of this function.
// Could be any workspace and checking auth to each workspace is overkill for
// the purpose of this function.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil {
return err
}
+2
View File
@@ -208,6 +208,7 @@ func (s APIKeyScopes) expandRBACScope() (rbac.Scope, error) {
for orgID, perms := range expanded.ByOrgID {
orgPerms := merged.ByOrgID[orgID]
orgPerms.Org = append(orgPerms.Org, perms.Org...)
orgPerms.Member = append(orgPerms.Member, perms.Member...)
merged.ByOrgID[orgID] = orgPerms
}
merged.User = append(merged.User, expanded.User...)
@@ -220,6 +221,7 @@ func (s APIKeyScopes) expandRBACScope() (rbac.Scope, error) {
merged.User = rbac.DeduplicatePermissions(merged.User)
for orgID, perms := range merged.ByOrgID {
perms.Org = rbac.DeduplicatePermissions(perms.Org)
perms.Member = rbac.DeduplicatePermissions(perms.Member)
merged.ByOrgID[orgID] = perms
}
+104
View File
@@ -0,0 +1,104 @@
# Rego authorization policy
## Code style
It's a good idea to consult the [Rego style guide](https://docs.styra.com/opa/rego-style-guide). The "Variables and Data Types" section in particular has some helpful and non-obvious advice in it.
## Debugging
Open Policy Agent provides a CLI and a playground that can be used for evaluating, formatting, testing, and linting policies.
### CLI
Below are some helpful commands you can use for debugging.
For full evaluation, run:
```sh
opa eval --format=pretty 'data.authz.allow' -d policy.rego -i input.json
```
For partial evaluation, run:
```sh
opa eval --partial --format=pretty 'data.authz.allow' -d policy.rego \
--unknowns input.object.owner --unknowns input.object.org_owner \
--unknowns input.object.acl_user_list --unknowns input.object.acl_group_list \
-i input.json
```
### Playground
Use the [Open Policy Agent Playground](https://play.openpolicyagent.org/) while editing to getting linting, code formatting, and help debugging!
You can use the contents of input.json as a starting point for your own testing input. Paste the contents of policy.rego into the left-hand side of the playground, and the contents of input.json into the "Input" section. Click "Evaluate" and you should see something like the following in the output.
```json
{
"allow": true,
"check_scope_allow_list": true,
"org": 0,
"org_member": 0,
"org_memberships": [],
"permission_allow": true,
"role_allow": true,
"scope_allow": true,
"scope_org": 0,
"scope_org_member": 0,
"scope_site": 1,
"scope_user": 0,
"site": 1,
"user": 0
}
```
## Levels
Permissions are evaluated at four levels: site, user, org, org_member.
For each level, two checks are performed:
- Do the subject's permissions allow them to perform this action?
- Does the subject's scope allow them to perform this action?
Each of these checks gets a "vote", which must one of three values:
- -1 to deny (usually because of a negative permission)
- 0 to abstain (no matching permission)
- 1 to allow
If a level abstains, then the decision gets deferred to the next level. When
there is no "next" level to defer to it is equivalent to being denied.
### Scope
Additionally, each input has a "scope" that can be thought of as a second set of permissions, where each permission belongs to one of the four levelsexactly the same as role permissions. An action is only allowed if it is allowed by both the subject's permissions _and_ their current scope. This is to allow issuing tokens for a subject that have a subset of the full subjects permissions.
For example, you may have a scope like...
```json
{
"by_org_id": {
"<org_id>": {
"member": [{ "resource_type": "workspace", "action": "*" }]
}
}
}
```
...to limit the token to only accessing workspaces owned by the user within a specific org. This provides some assurances for an admin user, that the token can only access intended resources, rather than having full access to everything.
The final policy decision is determined by evaluating each of these checks in their proper precedence order from the `allow` rule.
## Unknown values
This policy is specifically constructed to compress to a set of queries if 'input.object.owner' and 'input.object.org_owner' are unknown. There is no specific set of rules that will guarantee that this policy has this property, however, there are some tricks. We have tests that enforce this property, so any changes that pass the tests will be okay.
Some general rules to follow:
1. Do not use unknown values in any [comprehensions](https://www.openpolicyagent.org/docs/latest/policy-language/#comprehensions) or iterations.
2. Use the unknown values as minimally as possible.
3. Avoid making code branches based on the value of the unknown field.
Unknown values are like a "set" of possible values (which is why rule 1 usually breaks things).
For example, in the org level rules, we calculate the "vote" for all orgs, rather than just the `input.object.org_owner`. This way, if the `org_owner` changes, then we don't need to recompute any votes; we already have it for the changed value. This means we don't need branching, because the end result is just a lookup table.
+83 -35
View File
@@ -58,22 +58,68 @@ This can be represented by the following truth table, where Y represents _positi
- `+site.app.*.read`: allowed to perform the `read` action against all objects of type `app` in a given Coder deployment.
- `-user.workspace.*.create`: user is not allowed to create workspaces.
## Levels
A user can be given (or deprived) a permission at several levels. Currently,
those levels are:
- Site-wide level
- Organization level
- User level
- Organization member level
The site-wide level is the most authoritative. Any permission granted or denied at the side-wide level is absolute. After checking the site-wide level, depending of if the resource is owned by an organization or not, it will check the other levels.
- If the resource is owned by an organization, the next most authoritative level is the organization level. It acts like the site-wide level, but only for resources within the corresponding organization. The user can use that permission on any resource within that organization.
- After the organization level is the member level. This level only applies to resources that are owned by both the organization _and_ the user.
- If the resource is not owned by an organization, the next level to check is the user level. This level only applies to resources owned by the user and that are not owned by any organization.
```
┌──────────┐
│ Site │
└─────┬────┘
┌──────────┴───────────┐
┌──┤ Owned by an org? ├──┐
│ └──────────────────────┘ │
┌──┴──┐ ┌──┴─┐
│ Yes │ │ No │
└──┬──┘ └──┬─┘
┌────────┴─────────┐ ┌─────┴────┐
│ Organization │ │ User │
└────────┬─────────┘ └──────────┘
┌─────┴──────┐
│ Member │
└────────────┘
```
## Roles
A _role_ is a set of permissions. When evaluating a role's permission to form an action, all the relevant permissions for the role are combined at each level. Permissions at a higher level override permissions at a lower level.
The following table shows the per-level role evaluation.
Y indicates that the role provides positive permissions, N indicates the role provides negative permissions, and _indicates the role does not provide positive or negative permissions. YN_ indicates that the value in the cell does not matter for the access result.
The following tables show the per-level role evaluation. Y indicates that the role provides positive permissions, N indicates the role provides negative permissions, and _indicates the role does not provide positive or negative permissions. YN_ indicates that the value in the cell does not matter for the access result. The table varies depending on if the resource belongs to an organization or not.
| Role (example) | Site | Org | User | Result |
|-----------------|------|------|------|--------|
| site-admin | Y | YN\_ | YN\_ | Y |
| no-permission | N | YN\_ | YN\_ | N |
| org-admin | \_ | Y | YN\_ | Y |
| non-org-member | \_ | N | YN\_ | N |
| user | \_ | \_ | Y | Y |
| | \_ | \_ | N | N |
| unauthenticated | \_ | \_ | \_ | N |
If the resource is owned by an organization, such as a template or a workspace:
| Role (example) | Site | Org | OrgMember | Result |
|--------------------------|------|------|-----------|--------|
| site-admin | Y | YN\_ | YN\_ | Y |
| negative-site-permission | N | YN\_ | YN\_ | N |
| org-admin | \_ | Y | YN\_ | Y |
| non-org-member | \_ | N | YN\_ | N |
| member-owned | \_ | \_ | Y | Y |
| not-member-owned | \_ | \_ | N | N |
| unauthenticated | \_ | \_ | \_ | N |
If the resource is not owned by an organization:
| Role (example) | Site | User | Result |
|--------------------------|------|------|--------|
| site-admin | Y | YN\_ | Y |
| negative-site-permission | N | YN\_ | N |
| user-owned | \_ | Y | Y |
| not-user-owned | \_ | N | N |
| unauthenticated | \_ | \_ | N |
## Scopes
@@ -91,15 +137,17 @@ The use case for specifying this type of permission in a role is limited, and do
Example of a scope for a workspace agent token, using an `allow_list` containing a single resource id.
```javascript
"scope": {
"name": "workspace_agent",
"display_name": "Workspace_Agent",
// The ID of the given workspace the agent token correlates to.
"allow_list": ["10d03e62-7703-4df5-a358-4f76577d4e2f"],
"site": [/* ... perms ... */],
"org": {/* ... perms ... */},
"user": [/* ... perms ... */]
}
{
"scope": {
"name": "workspace_agent",
"display_name": "Workspace_Agent",
// The ID of the given workspace the agent token correlates to.
"allow_list": ["10d03e62-7703-4df5-a358-4f76577d4e2f"],
"site": [/* ... perms ... */],
"org": {/* ... perms ... */},
"user": [/* ... perms ... */]
}
}
```
## OPA (Open Policy Agent)
@@ -124,31 +172,31 @@ To learn more about OPA and Rego, see https://www.openpolicyagent.org/docs.
There are two types of evaluation in OPA:
- **Full evaluation**: Produces a decision that can be enforced.
This is the default evaluation mode, where OPA evaluates the policy using `input` data that contains all known values and returns output data with the `allow` variable.
This is the default evaluation mode, where OPA evaluates the policy using `input` data that contains all known values and returns output data with the `allow` variable.
- **Partial evaluation**: Produces a new policy that can be evaluated later when the _unknowns_ become _known_.
This is an optimization in OPA where it evaluates as much of the policy as possible without resolving expressions that depend on _unknown_ values from the `input`.
To learn more about partial evaluation, see this [OPA blog post](https://blog.openpolicyagent.org/partial-evaluation-162750eaf422).
This is an optimization in OPA where it evaluates as much of the policy as possible without resolving expressions that depend on _unknown_ values from the `input`.
To learn more about partial evaluation, see this [OPA blog post](https://blog.openpolicyagent.org/partial-evaluation-162750eaf422).
Application of Full and Partial evaluation in `rbac` package:
- **Full Evaluation** is handled by the `RegoAuthorizer.Authorize()` method in [`authz.go`](authz.go).
This method determines whether a subject (user) can perform a specific action on an object.
It performs a full evaluation of the Rego policy, which returns the `allow` variable to decide whether access is granted (`true`) or denied (`false` or undefined).
This method determines whether a subject (user) can perform a specific action on an object.
It performs a full evaluation of the Rego policy, which returns the `allow` variable to decide whether access is granted (`true`) or denied (`false` or undefined).
- **Partial Evaluation** is handled by the `RegoAuthorizer.Prepare()` method in [`authz.go`](authz.go).
This method compiles OPAs partial evaluation queries into `SQL WHERE` clauses.
These clauses are then used to enforce authorization directly in database queries, rather than in application code.
This method compiles OPAs partial evaluation queries into `SQL WHERE` clauses.
These clauses are then used to enforce authorization directly in database queries, rather than in application code.
Authorization Patterns:
- Fetch-then-authorize: an object is first retrieved from the database, and a single authorization check is performed using full evaluation via `Authorize()`.
- Authorize-while-fetching: Partial evaluation via `Prepare()` is used to inject SQL filters directly into queries, allowing efficient authorization of many objects of the same type.
`dbauthz` methods that enforce authorization directly in the SQL query are prefixed with `Authorized`, for example, `GetAuthorizedWorkspaces`.
`dbauthz` methods that enforce authorization directly in the SQL query are prefixed with `Authorized`, for example, `GetAuthorizedWorkspaces`.
## Testing
- OPA Playground: https://play.openpolicyagent.org/
- OPA CLI (`opa eval`): useful for experimenting with different inputs and understanding how the policy behaves under various conditions.
`opa eval` returns the constraints that must be satisfied for a rule to evaluate to `true`.
`opa eval` returns the constraints that must be satisfied for a rule to evaluate to `true`.
- `opa eval` requires an `input.json` file containing the input data to run the policy against.
You can generate this file using the [gen_input.go](../../scripts/rbac-authz/gen_input.go) script.
Note: the script currently produces a fixed input. You may need to tweak it for your specific use case.
@@ -196,12 +244,12 @@ The script [`benchmark_authz.sh`](../../scripts/rbac-authz/benchmark_authz.sh) r
- To run benchmark on the current branch:
```bash
benchmark_authz.sh --single
```
```bash
benchmark_authz.sh --single
```
- To compare benchmarks between 2 branches:
```bash
benchmark_authz.sh --compare main prebuild_policy
```
```bash
benchmark_authz.sh --compare main prebuild_policy
```
+4
View File
@@ -165,6 +165,10 @@ func (role Role) regoValue() ast.Value {
ast.StringTerm("org"),
ast.NewTerm(regoSlice(p.Org)),
},
[2]*ast.Term{
ast.StringTerm("member"),
ast.NewTerm(regoSlice(p.Member)),
},
),
))
}
+113 -50
View File
@@ -287,7 +287,7 @@ func TestFilter(t *testing.T) {
func TestAuthorizeDomain(t *testing.T) {
t.Parallel()
defOrg := uuid.New()
unuseID := uuid.New()
unusedID := uuid.New()
allUsersGroup := "Everyone"
// orphanedUser has no organization
@@ -318,21 +318,21 @@ func TestAuthorizeDomain(t *testing.T) {
testAuthorize(t, "UserACLList", user, []authTestCase{
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
user.ID: ResourceWorkspace.AvailableActions(),
}),
actions: ResourceWorkspace.AvailableActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
user.ID: {policy.WildcardSymbol},
}),
actions: ResourceWorkspace.AvailableActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(unusedID).WithACLUserList(map[string][]policy.Action{
user.ID: {policy.ActionRead, policy.ActionUpdate},
}),
actions: []policy.Action{policy.ActionCreate, policy.ActionDelete},
@@ -350,21 +350,21 @@ func TestAuthorizeDomain(t *testing.T) {
testAuthorize(t, "GroupACLList", user, []authTestCase{
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: ResourceWorkspace.AvailableActions(),
}),
actions: ResourceWorkspace.AvailableActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: {policy.WildcardSymbol},
}),
actions: ResourceWorkspace.AvailableActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
resource: ResourceWorkspace.WithOwner(unusedID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: {policy.ActionRead, policy.ActionUpdate},
}),
actions: []policy.Action{policy.ActionCreate, policy.ActionDelete},
@@ -389,13 +389,14 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.AnyOrganization().WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceTemplate.AnyOrganization(), actions: []policy.Action{policy.ActionCreate}, allow: false},
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
// No org + me
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
@@ -403,8 +404,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other us
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
})
@@ -435,8 +436,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
@@ -444,8 +445,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
})
@@ -455,6 +456,7 @@ func TestAuthorizeDomain(t *testing.T) {
Scope: must(ExpandScope(ScopeAll)),
Roles: Roles{
must(RoleByName(ScopedRoleOrgAdmin(defOrg))),
must(RoleByName(ScopedRoleOrgMember(defOrg))),
must(RoleByName(RoleMember())),
},
}
@@ -469,13 +471,14 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceExceptConnect, allow: true},
{resource: ResourceWorkspace.InOrg(defOrg), actions: workspaceConnect, allow: false},
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
// No org + me
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: workspaceExceptConnect, allow: true},
@@ -483,9 +486,9 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: false},
// Other org + other user
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: false},
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: false},
})
@@ -512,8 +515,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: true},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: true},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
@@ -521,8 +524,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(unuseID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.InOrg(unusedID), actions: ResourceWorkspace.AvailableActions(), allow: true},
{resource: ResourceWorkspace.WithOwner("not-me"), actions: ResourceWorkspace.AvailableActions(), allow: true},
})
@@ -546,13 +549,14 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), allow: true},
{resource: ResourceWorkspace.InOrg(defOrg), allow: false},
{resource: ResourceWorkspace.WithOwner(user.ID), allow: true},
// No org + me
{resource: ResourceWorkspace.WithOwner(user.ID), allow: false},
{resource: ResourceWorkspace.All(), allow: false},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), allow: false},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: false},
@@ -560,8 +564,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me"), allow: false},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), allow: false},
{resource: ResourceWorkspace.WithOwner("not-me"), allow: false},
}),
@@ -580,8 +584,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.All()},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID)},
{resource: ResourceWorkspace.InOrg(unuseID)},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID)},
{resource: ResourceWorkspace.InOrg(unusedID)},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")},
@@ -589,8 +593,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me")},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
{resource: ResourceWorkspace.InOrg(unuseID)},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me")},
{resource: ResourceWorkspace.InOrg(unusedID)},
{resource: ResourceWorkspace.WithOwner("not-me")},
}),
@@ -609,8 +613,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceTemplate.All()},
// Other org + me
{resource: ResourceTemplate.InOrg(unuseID).WithOwner(user.ID)},
{resource: ResourceTemplate.InOrg(unuseID)},
{resource: ResourceTemplate.InOrg(unusedID).WithOwner(user.ID)},
{resource: ResourceTemplate.InOrg(unusedID)},
// Other org + other user
{resource: ResourceTemplate.InOrg(defOrg).WithOwner("not-me")},
@@ -618,8 +622,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceTemplate.WithOwner("not-me")},
// Other org + other use
{resource: ResourceTemplate.InOrg(unuseID).WithOwner("not-me")},
{resource: ResourceTemplate.InOrg(unuseID)},
{resource: ResourceTemplate.InOrg(unusedID).WithOwner("not-me")},
{resource: ResourceTemplate.InOrg(unusedID)},
{resource: ResourceTemplate.WithOwner("not-me")},
}),
@@ -647,6 +651,7 @@ func TestAuthorizeDomain(t *testing.T) {
ResourceType: "*",
Action: policy.ActionRead,
}},
Member: []Permission{},
},
},
},
@@ -668,8 +673,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.All(), allow: false},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), allow: false},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), allow: true},
@@ -677,8 +682,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me"), allow: false},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
{resource: ResourceWorkspace.InOrg(unuseID), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), allow: false},
{resource: ResourceWorkspace.InOrg(unusedID), allow: false},
{resource: ResourceWorkspace.WithOwner("not-me"), allow: false},
}),
@@ -699,8 +704,8 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.All()},
// Other org + me
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID)},
{resource: ResourceWorkspace.InOrg(unuseID)},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner(user.ID)},
{resource: ResourceWorkspace.InOrg(unusedID)},
// Other org + other user
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me")},
@@ -708,11 +713,67 @@ func TestAuthorizeDomain(t *testing.T) {
{resource: ResourceWorkspace.WithOwner("not-me")},
// Other org + other use
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
{resource: ResourceWorkspace.InOrg(unuseID)},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me")},
{resource: ResourceWorkspace.InOrg(unusedID)},
{resource: ResourceWorkspace.WithOwner("not-me")},
}))
// Org member vs user permissions
user = Subject{
ID: "me",
Scope: must(ExpandScope(ScopeAll)),
Roles: Roles{
{
Identifier: RoleIdentifier{Name: "OrgMemberVsUser"},
Site: []Permission{},
User: []Permission{
{
Negate: false,
ResourceType: ResourceApiKey.Type,
Action: policy.ActionRead,
},
},
ByOrgID: map[string]OrgPermissions{
defOrg.String(): {
Org: []Permission{
{
Negate: false,
ResourceType: ResourceTemplate.Type,
Action: policy.ActionRead,
},
},
Member: []Permission{
{
Negate: false,
ResourceType: ResourceWorkspace.Type,
Action: policy.ActionRead,
},
},
},
},
},
},
}
testAuthorize(t, "OrgMemberVsUser", user, []authTestCase{
// AnyOrg can read because of the default
{resource: ResourceTemplate.AnyOrganization(), actions: []policy.Action{policy.ActionRead}, allow: true},
{resource: ResourceTemplate.InOrg(defOrg), actions: []policy.Action{policy.ActionRead}, allow: true},
// Cannot read workspace in AnyOrganization, because it must also be owned by the user
{resource: ResourceWorkspace.AnyOrganization(), actions: []policy.Action{policy.ActionRead}, allow: false},
// AnyOrg + I am the owner, it is true. Albeit a bit of a weird case.
{resource: ResourceWorkspace.WithOwner("me").AnyOrganization(), actions: []policy.Action{policy.ActionRead}, allow: true},
// AnyOrg + not the owner
{resource: ResourceWorkspace.WithOwner(uuid.NewString()).AnyOrganization(), actions: []policy.Action{policy.ActionRead}, allow: false},
// User based permission
{resource: ResourceApiKey.WithOwner("me"), actions: []policy.Action{policy.ActionRead}, allow: true},
// User perms cannot be used for org based resources
{resource: ResourceApiKey.WithOwner("me").InOrg(defOrg), actions: []policy.Action{policy.ActionRead}, allow: false},
{resource: ResourceApiKey.WithOwner("me").AnyOrganization(), actions: []policy.Action{policy.ActionRead}, allow: false},
})
}
// TestAuthorizeLevels ensures level overrides are acting appropriately
@@ -737,6 +798,7 @@ func TestAuthorizeLevels(t *testing.T) {
Action: "*",
},
},
Member: []Permission{},
},
},
},
@@ -1150,6 +1212,7 @@ func TestAuthorizeScope(t *testing.T) {
Org: Permissions(map[string][]policy.Action{
ResourceWorkspace.Type: {policy.ActionRead},
}),
Member: []Permission{},
},
},
},
@@ -1316,9 +1379,9 @@ type authTestCase struct {
func testAuthorize(t *testing.T, name string, subject Subject, sets ...[]authTestCase) {
t.Helper()
authorizer := NewAuthorizer(prometheus.NewRegistry())
for _, cases := range sets {
for i, c := range cases {
caseName := fmt.Sprintf("%s/%d", name, i)
for i, cases := range sets {
for j, c := range cases {
caseName := fmt.Sprintf("%s/Set%d/Case%d", name, i, j)
t.Run(caseName, func(t *testing.T) {
t.Parallel()
for _, a := range c.actions {
+15 -4
View File
@@ -23,8 +23,13 @@
"action": "*"
}
],
"org": {},
"user": []
"user": [],
"by_org_id": {
"bf7b72bd-a2b1-4ef2-962c-1d698e0483f6": {
"org": [],
"member": []
}
}
}
],
"groups": ["b617a647-b5d0-4cbe-9e40-26f89710bf18"],
@@ -38,13 +43,19 @@
"action": "*"
}
],
"org": {},
"user": [],
"by_org_id": {
"bf7b72bd-a2b1-4ef2-962c-1d698e0483f6": {
"org": [],
"member": []
}
},
"allow_list": [
{
"type": "workspace",
"id": "*"
}]
}
]
}
}
}
+329 -295
View File
@@ -2,392 +2,426 @@ package authz
import rego.v1
# A great playground: https://play.openpolicyagent.org/
# Helpful cli commands to debug.
# opa eval --format=pretty 'data.authz.allow' -d policy.rego -i input.json
# opa eval --partial --format=pretty 'data.authz.allow' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner --unknowns input.object.acl_user_list --unknowns input.object.acl_group_list -i input.json
# Check the POLICY.md file before editing this!
#
# This policy is specifically constructed to compress to a set of queries if the
# object's 'owner' and 'org_owner' fields are unknown. There is no specific set
# of rules that will guarantee that this policy has this property. However, there
# are some tricks. A unit test will enforce this property, so any edits that pass
# the unit test will be ok.
# https://play.openpolicyagent.org/
#
# Tricks: (It's hard to really explain this, fiddling is required)
# 1. Do not use unknown fields in any comprehension or iteration.
# 2. Use the unknown fields as minimally as possible.
# 3. Avoid making code branches based on the value of the unknown field.
# Unknown values are like a "set" of possible values.
# (This is why rule 1 usually breaks things)
# For example:
# In the org section, we calculate the 'allow' number for all orgs, rather
# than just the input.object.org_owner. This is because if the org_owner
# changes, then we don't need to recompute any 'allow' sets. We already have
# the 'allow' for the changed value. So the answer is in a lookup table.
# The final statement 'num := allow[input.object.org_owner]' does not have
# different code branches based on the org_owner. 'num's value does, but
# that is the whole point of partial evaluation.
# bool_flip(b) returns the logical negation of a boolean value 'b'.
# You cannot do 'x := !false', but you can do 'x := bool_flip(false)'
bool_flip(b) := false if {
b
}
#==============================================================================#
# Site level rules #
#==============================================================================#
bool_flip(b) := true if {
not b
}
# number(set) maps a set of boolean values to one of the following numbers:
# -1: deny (if 'false' value is in the set) => set is {true, false} or {false}
# 0: no decision (if the set is empty) => set is {}
# 1: allow (if only 'true' values are in the set) => set is {true}
# Return -1 if the set contains any 'false' value (i.e., an explicit deny)
number(set) := -1 if {
false in set
}
# Return 0 if the set is empty (no matching permissions)
number(set) := 0 if {
count(set) == 0
}
# Return 1 if the set is non-empty and contains no 'false' values (i.e., only allows)
number(set) := 1 if {
not false in set
set[_]
}
# Permission evaluation is structured into three levels: site, org, and user.
# For each level, two variables are computed:
# - <level>: the decision based on the subject's full set of roles for that level
# - scope_<level>: the decision based on the subject's scoped roles for that level
#
# Each of these variables is assigned one of three values:
# -1 => negative (deny)
# 0 => abstain (no matching permission)
# 1 => positive (allow)
#
# These values are computed by calling the corresponding <level>_allow functions.
# The final decision is derived from combining these values (see 'allow' rule).
# -------------------
# Site Level Rules
# -------------------
# Site level permissions allow the subject to use that permission on any object.
# For example, a site-level workspace.read permission means that the subject can
# see every workspace in the deployment, regardless of organization or owner.
default site := 0
site := site_allow(input.subject.roles)
site := check_site_permissions(input.subject.roles)
default scope_site := 0
scope_site := site_allow([input.subject.scope])
# site_allow receives a list of roles and returns a single number:
# -1 if any matching permission denies access
# 1 if there's at least one allow and no denies
# 0 if there are no matching permissions
site_allow(roles) := num if {
# allow is a set of boolean values (sets don't contain duplicates)
scope_site := check_site_permissions([input.subject.scope])
check_site_permissions(roles) := vote if {
allow := {is_allowed |
# Iterate over all site permissions in all roles
# Iterate over all site permissions in all roles, and check which ones match
# the action and object type.
perm := roles[_].site[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# is_allowed is either 'true' or 'false' if a matching permission exists.
# If a negative matching permission was found, then we vote to disallow it.
# If the permission is not negative, then we vote to allow it.
is_allowed := bool_flip(perm.negate)
}
num := number(allow)
vote := to_vote(allow)
}
# -------------------
# Org Level Rules
# -------------------
#==============================================================================#
# User level rules #
#==============================================================================#
# org_members is the list of organizations the actor is apart of.
# TODO: Should there be an org_members for the scope too? Without it,
# the membership is determined by the user's roles, not their scope permissions.
# So if an owner (who is not an org member) has an org scope, that org scope
# will fail to return '1'. Since we assume all non members return '-1' for org
# level permissions.
# Adding a second org_members set might affect the partial evaluation.
# This is being left until org scopes are used.
org_members := {orgID |
input.subject.roles[_].by_org_id[orgID]
}
# User level rules apply to all objects owned by the subject which are not also
# owned by an org. Permissions for objects which are "jointly" owned by an org
# instead defer to the org member level rules.
# 'org' is the same as 'site' except we need to iterate over each organization
# that the actor is a member of.
default org := 0
org := org_allow(input.subject.roles, "org")
default scope_org := 0
scope_org := org_allow([input.subject.scope], "org")
# org_allow_set is a helper function that iterates over all orgs that the actor
# is a member of. For each organization it sets the numerical allow value
# for the given object + action if the object is in the organization.
# The resulting value is a map that looks something like:
# {"10d03e62-7703-4df5-a358-4f76577d4e2f": 1, "5750d635-82e0-4681-bd44-815b18669d65": 1}
# The caller can use this output[<object.org_owner>] to get the final allow value.
#
# The reason we calculate this for all orgs, and not just the input.object.org_owner
# is that sometimes the input.object.org_owner is unknown. In those cases
# we have a list of org_ids that can we use in a SQL 'WHERE' clause.
org_allow_set(roles, key) := allow_set if {
allow_set := {id: num |
id := org_members[_]
set := {is_allowed |
# Iterate over all org permissions in all roles
perm := roles[_].by_org_id[id][key][_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# is_allowed is either 'true' or 'false' if a matching permission exists.
is_allowed := bool_flip(perm.negate)
}
num := number(set)
}
}
org_allow(roles, key) := num if {
# If the object has "any_org" set to true, then use the other
# org_allow block.
not input.object.any_org
allow := org_allow_set(roles, key)
# Return only the org value of the input's org.
# The reason why we do not do this up front, is that we need to make sure
# this policy compresses down to simple queries. One way to ensure this is
# to keep unknown values out of comprehensions.
# (https://www.openpolicyagent.org/docs/latest/policy-language/#comprehensions)
num := allow[input.object.org_owner]
}
# This block states if "object.any_org" is set to true, then disregard the
# organization id the object is associated with. Instead, we check if the user
# can do the action on any organization.
# This is useful for UI elements when we want to conclude, "Can the user create
# a new template in any organization?"
# It is easier than iterating over every organization the user is apart of.
org_allow(roles, key) := num if {
input.object.any_org # if this is false, this code block is not used
allow := org_allow_set(roles, key)
# allow is a map of {"<org_id>": <number>}. We only care about values
# that are 1, and ignore the rest.
num := number([
keep |
# for every value in the mapping
value := allow[_]
# only keep values > 0.
# 1 = allow, 0 = abstain, -1 = deny
# We only need 1 explicit allow to allow the action.
# deny's and abstains are intentionally ignored.
value > 0
# result set is a set of [true,false,...]
# which "number()" will convert to a number.
keep := true
])
}
# 'org_mem' is set to true if the user is an org member
# If 'any_org' is set to true, use the other block to determine org membership.
org_mem if {
not input.object.any_org
input.object.org_owner != ""
input.object.org_owner in org_members
}
org_mem if {
input.object.any_org
count(org_members) > 0
}
org_ok if {
org_mem
}
# If the object has no organization, then the user is also considered part of
# the non-existent org.
org_ok if {
input.object.org_owner == ""
not input.object.any_org
}
# -------------------
# User Level Rules
# -------------------
# 'user' is the same as 'site', except it only applies if the user owns the object and
# the user is apart of the org (if the object has an org).
default user := 0
user := user_allow(input.subject.roles)
user := check_user_permissions(input.subject.roles)
default scope_user := 0
scope_user := user_allow([input.subject.scope])
user_allow(roles) := num if {
input.object.owner != ""
scope_user := check_user_permissions([input.subject.scope])
check_user_permissions(roles) := vote if {
# The object must be owned by the subject.
input.subject.id = input.object.owner
# If there is an org, use org_member permissions instead
input.object.org_owner == ""
not input.object.any_org
allow := {is_allowed |
# Iterate over all user permissions in all roles
# Iterate over all user permissions in all roles, and check which ones match
# the action and object type.
perm := roles[_].user[_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# is_allowed is either 'true' or 'false' if a matching permission exists.
# If a negative matching permission was found, then we vote to disallow it.
# If the permission is not negative, then we vote to allow it.
is_allowed := bool_flip(perm.negate)
}
num := number(allow)
vote := to_vote(allow)
}
# Scope allow_list is a list of resource (Type, ID) tuples explicitly allowed by the scope.
# If the list contains `(*,*)`, then all resources are allowed.
scope_allow_list if {
input.subject.scope.allow_list[_] == {"type": "*", "id": "*"}
#==============================================================================#
# Org level rules #
#==============================================================================#
# Org level permissions are similar to `site`, except we need to iterate over
# each organization that the subject is a member of, and check against the
# organization that the object belongs to.
# For example, an organization-level workspace.read permission means that the
# subject can see every workspace in the organization, regardless of owner.
# org_memberships is the set of organizations the subject is apart of.
org_memberships := {org_id |
input.subject.roles[_].by_org_id[org_id]
}
# This is a shortcut if the allow_list contains (type, *), then allow all IDs of that type.
scope_allow_list if {
input.subject.scope.allow_list[_] == {"type": input.object.type, "id": "*"}
# TODO: Should there be a scope_org_memberships too? Without it, the membership
# is determined by the user's roles, not their scope permissions.
#
# If an owner (who is not an org member) has an org scope, that org scope will
# fail to return '1', since we assume all non-members return '-1' for org level
# permissions. Adding a second set of org memberships might affect the partial
# evaluation. This is being left until org scopes are used.
default org := 0
org := check_org_permissions(input.subject.roles, "org")
default scope_org := 0
scope_org := check_org_permissions([input.subject.scope], "org")
# check_all_org_permissions creates a map from org ids to votes at each org
# level, for each org that the subject is a member of. It doesn't actually check
# if the object is in the same org. Instead we look up the correct vote from
# this map based on the object's org id in `check_org_permissions`.
# For example, the `org_map` will look something like this:
#
# {"<org_id_a>": 1, "<org_id_b>": 0, "<org_id_c>": -1}
#
# The caller then uses `output[input.object.org_owner]` to get the correct vote.
#
# We have to create this map, rather than just getting the vote of the object's
# org id because the org id _might_ be unknown. In order to make sure that this
# policy compresses down to simple queries we need to keep unknown values out of
# comprehensions.
check_all_org_permissions(roles, key) := {org_id: vote |
org_id := org_memberships[_]
allow := {is_allowed |
# Iterate over all site permissions in all roles, and check which ones match
# the action and object type.
perm := roles[_].by_org_id[org_id][key][_]
perm.action in [input.action, "*"]
perm.resource_type in [input.object.type, "*"]
# If a negative matching permission was found, then we vote to disallow it.
# If the permission is not negative, then we vote to allow it.
is_allowed := bool_flip(perm.negate)
}
vote := to_vote(allow)
}
# A comprehension that iterates over the allow_list and checks if the
# (object.type, object.id) is in the allowed ids.
scope_allow_list if {
# If the wildcard is listed in the allow_list, we do not care about the
# object.id. This line is included to prevent partial compilations from
# ever needing to include the object.id.
not {"type": "*", "id": "*"} in input.subject.scope.allow_list
# This is equivalent to the above line, as `type` is known at partial query time.
not {"type": input.object.type, "id": "*"} in input.subject.scope.allow_list
# This check handles the case where the org id is known.
check_org_permissions(roles, key) := vote if {
# Disallow setting any_org at the same time as an org id.
not input.object.any_org
# allows_ids is the set of all ids allowed for the given object.type
allowed_ids := {allowed_id |
# Iterate over all allow list elements
ele := input.subject.scope.allow_list[_]
ele.type in [input.object.type, "*"]
allowed_id := ele.id
}
allow_map := check_all_org_permissions(roles, key)
# Return if the object.id is in the allowed ids
# This rule is evaluated at the end so the partial query can use the object.id
# against this precomputed set of allowed ids.
input.object.id in allowed_ids
# Return only the vote of the object's org.
vote := allow_map[input.object.org_owner]
}
# -------------------
# Role-Specific Rules
# -------------------
# This check handles the case where we want to know if the user has the
# appropriate permission for any organization, without needing to know which.
# This is used in several places in the UI to determine if certain parts of the
# app should be accessible.
# For example, can the user create a new template in any organization? If yes,
# then we should show the "New template" button.
check_org_permissions(roles, key) := vote if {
# Require `any_org` to be set
input.object.any_org
allow_map := check_all_org_permissions(roles, key)
# Since we're checking if the subject has the permission in _any_ org, we're
# essentially trying to find the highest vote from any org.
vote := max({vote |
some vote in allow_map
})
}
# is_org_member checks if the subject belong to the same organization as the
# object.
is_org_member if {
not input.object.any_org
input.object.org_owner != ""
input.object.org_owner in org_memberships
}
# ...if 'any_org' is set to true, we check if the subject is a member of any
# org.
is_org_member if {
input.object.any_org
count(org_memberships) > 0
}
#==============================================================================#
# Org member level rules #
#==============================================================================#
# Org member level permissions apply to all objects owned by the subject _and_
# the corresponding org. Permissions for objects which are not jointly owned
# instead defer to the user level rules.
#
# The rules for this level are very similar to the rules for the organization
# level, and so we reuse the `check_org_permissions` function from those rules.
default org_member := 0
org_member := vote if {
# Object must be jointly owned by the user
input.object.owner != ""
input.subject.id = input.object.owner
vote := check_org_permissions(input.subject.roles, "member")
}
default scope_org_member := 0
scope_org_member := vote if {
# Object must be jointly owned by the user
input.object.owner != ""
input.subject.id = input.object.owner
vote := check_org_permissions([input.subject.scope], "member")
}
#==============================================================================#
# Role rules #
#==============================================================================#
# role_allow specifies all of the conditions under which a role can grant
# permission. These rules intentionally use the "unification" operator rather
# than the equality and inequality operators, because those operators do not
# work on partial values.
# https://www.openpolicyagent.org/docs/policy-language#unification-
# Site level authorization
role_allow if {
site = 1
}
# User level authorization
role_allow if {
not site = -1
user = 1
}
# Org level authorization
role_allow if {
not site = -1
org = 1
}
# Org member authorization
role_allow if {
not site = -1
not org = -1
# If we are not a member of an org, and the object has an org, then we are
# not authorized. This is an "implied -1" for not being in the org.
org_ok
user = 1
org_member = 1
}
# -------------------
# Scope-Specific Rules
# -------------------
#==============================================================================#
# Scope rules #
#==============================================================================#
# scope_allow specifies all of the conditions under which a scope can grant
# permission. These rules intentionally use the "unification" (=) operator
# rather than the equality (==) and inequality (!=) operators, because those
# operators do not work on partial values.
# https://www.openpolicyagent.org/docs/policy-language#unification-
# Site level scope enforcement
scope_allow if {
scope_allow_list
object_is_included_in_scope_allow_list
scope_site = 1
}
# User level scope enforcement
scope_allow if {
scope_allow_list
# User scope permissions must be allowed by the scope, and not denied
# by the site. The object *must not* be owned by an organization.
object_is_included_in_scope_allow_list
not scope_site = -1
scope_org = 1
}
scope_allow if {
scope_allow_list
not scope_site = -1
not scope_org = -1
# If we are not a member of an org, and the object has an org, then we are
# not authorized. This is an "implied -1" for not being in the org.
org_ok
scope_user = 1
}
# -------------------
# ACL-Specific Rules
# Access Control List
# -------------------
# Org level scope enforcement
scope_allow if {
# Org member scope permissions must be allowed by the scope, and not denied
# by the site. The object *must* be owned by an organization.
object_is_included_in_scope_allow_list
not scope_site = -1
scope_org = 1
}
# Org member level scope enforcement
scope_allow if {
# Org member scope permissions must be allowed by the scope, and not denied
# by the site or org. The object *must* be owned by an organization.
object_is_included_in_scope_allow_list
not scope_site = -1
not scope_org = -1
scope_org_member = 1
}
# If *.* is allowed, then all objects are in scope.
object_is_included_in_scope_allow_list if {
{"type": "*", "id": "*"} in input.subject.scope.allow_list
}
# If <type>.* is allowed, then all objects of that type are in scope.
object_is_included_in_scope_allow_list if {
{"type": input.object.type, "id": "*"} in input.subject.scope.allow_list
}
# Check if the object type and ID match one of the allow list entries.
object_is_included_in_scope_allow_list if {
# Check that the wildcard rules do not apply. This prevents partial inputs
# from needing to include `input.object.id`.
not {"type": "*", "id": "*"} in input.subject.scope.allow_list
not {"type": input.object.type, "id": "*"} in input.subject.scope.allow_list
# Check which IDs from the allow list match the object type
allowed_ids_for_object_type := {it.id |
some it in input.subject.scope.allow_list
it.type in [input.object.type, "*"]
}
# Check if the input object ID is in the set of allowed IDs for the same
# object type. We do this at the end to keep `input.object.id` out of the
# comprehension because it might be unknown.
input.object.id in allowed_ids_for_object_type
}
#==============================================================================#
# ACL rules #
#==============================================================================#
# ACL for users
acl_allow if {
# Should you have to be a member of the org too?
# TODO: Should you have to be a member of the org too?
perms := input.object.acl_user_list[input.subject.id]
# Either the input action or wildcard
[input.action, "*"][_] in perms
# Check if either the action or * is allowed
some action in [input.action, "*"]
action in perms
}
# ACL for groups
acl_allow if {
# If there is no organization owner, the object cannot be owned by an
# org_scoped team.
org_mem
group := input.subject.groups[_]
# org-scoped group.
is_org_member
some group in input.subject.groups
perms := input.object.acl_group_list[group]
# Either the input action or wildcard
[input.action, "*"][_] in perms
# Check if either the action or * is allowed
some action in [input.action, "*"]
action in perms
}
# ACL for 'all_users' special group
# ACL for the special "Everyone" groups
acl_allow if {
org_mem
# If there is no organization owner, the object cannot be owned by an
# org-scoped group.
is_org_member
perms := input.object.acl_group_list[input.object.org_owner]
[input.action, "*"][_] in perms
# Check if either the action or * is allowed
some action in [input.action, "*"]
action in perms
}
# -------------------
# Final Allow
#
# The 'allow' block is quite simple. Any set with `-1` cascades down in levels.
# Authorization looks for any `allow` statement that is true. Multiple can be true!
# Note that the absence of `allow` means "unauthorized".
# An explicit `"allow": true` is required.
#
# Scope is also applied. The default scope is "wildcard:wildcard" allowing
# all actions. If the scope is not "1", then the action is not authorized.
#
# Allow query:
# data.authz.role_allow = true
# data.authz.scope_allow = true
# -------------------
#==============================================================================#
# Allow #
#==============================================================================#
# The `allow` block is quite simple. Any check that voted no will cascade down.
# Authorization looks for any `allow` statement that is true. Multiple can be
# true! Note that the absence of `allow` means "unauthorized". An explicit
# `"allow": true` is required.
#
# We check both the subject's permissions (given by their roles or by ACL) and
# the subject's scope. (The default scope is "*:*", allowing all actions.) Both
# a permission check (either from roles or ACL) and the scope check must vote to
# allow or the action is not authorized.
# A subject can be given permission by a role
permission_allow if role_allow
# A subject can be given permission by ACL
permission_allow if acl_allow
# The role or the ACL must allow the action. Scopes can be used to limit,
# so scope_allow must always be true.
allow if {
role_allow
# Must be allowed by the subject's permissions
permission_allow
# ...and allowed by the scope
scope_allow
}
# ACL list must also have the scope_allow to pass
allow if {
acl_allow
scope_allow
#==============================================================================#
# Utilities #
#==============================================================================#
# bool_flip returns the logical negation of a boolean value. You can't do
# 'x := not false', but you can do 'x := bool_flip(false)'
bool_flip(b) := false if {
b
}
bool_flip(b) if {
not b
}
# to_vote gives you a voting value from a set or list of booleans.
# {false,..} => deny (-1)
# {} => abstain (0)
# {true} => allow (1)
# Any set which contains a `false` should be considered a vote to deny.
to_vote(set) := -1 if {
false in set
}
# A set which is empty should be considered abstaining.
to_vote(set) := 0 if {
count(set) == 0
}
# A set which only contains true should be considered a vote to allow.
to_vote(set) := 1 if {
not false in set
true in set
}
+28 -10
View File
@@ -295,15 +295,11 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceOauth2App.Type: {policy.ActionRead},
ResourceWorkspaceProxy.Type: {policy.ActionRead},
}),
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceUser, ResourceOrganizationMember),
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build, ssh, or exec
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
// Users cannot do create/update/delete on themselves, but they
// can read their own details.
ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
// Can read their own organization member record
ResourceOrganizationMember.Type: {policy.ActionRead},
// Users can create provisioner daemons scoped to themselves.
ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
})...,
@@ -431,6 +427,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
Member: []Permission{},
},
},
}
@@ -454,6 +451,16 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Can read available roles.
ResourceAssignOrgRole.Type: {policy.ActionRead},
}),
Member: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceUser, ResourceOrganizationMember),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build, ssh, or exec
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
// Can read their own organization member record
ResourceOrganizationMember.Type: {policy.ActionRead},
// Users can create provisioner daemons scoped to themselves.
ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
})...,
),
},
},
}
@@ -476,6 +483,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceOrganization.Type: {policy.ActionRead},
ResourceOrganizationMember.Type: {policy.ActionRead},
}),
Member: []Permission{},
},
},
}
@@ -502,6 +510,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceGroupMember.Type: ResourceGroupMember.AvailableActions(),
ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate},
}),
Member: []Permission{},
},
},
}
@@ -531,6 +540,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
}),
Member: []Permission{},
},
},
}
@@ -568,6 +578,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Action: policy.ActionDeleteAgent,
},
},
Member: []Permission{},
},
},
}
@@ -680,9 +691,10 @@ func (perm Permission) Valid() error {
}
// Role is a set of permissions at multiple levels:
// - Site level permissions apply EVERYWHERE
// - Org level permissions apply to EVERYTHING in a given ORG
// - User level permissions are the lowest
// - Site permissions apply EVERYWHERE
// - Org permissions apply to EVERYTHING in a given ORG
// - User permissions apply to all resources the user owns
// - OrgMember permissions apply to resources in the given org that the user owns
// This is the type passed into the rego as a json payload.
// Users of this package should instead **only** use the role names, and
// this package will expand the role names into their json payloads.
@@ -703,7 +715,8 @@ type Role struct {
}
type OrgPermissions struct {
Org []Permission `json:"org"`
Org []Permission `json:"org"`
Member []Permission `json:"member"`
}
// Valid will check all it's permissions and ensure they are all correct
@@ -720,7 +733,12 @@ func (role Role) Valid() error {
for orgID, orgPermissions := range role.ByOrgID {
for _, perm := range orgPermissions.Org {
if err := perm.Valid(); err != nil {
errs = append(errs, xerrors.Errorf("org=%q: %w", orgID, err))
errs = append(errs, xerrors.Errorf("org=%q: org %w", orgID, err))
}
}
for _, perm := range orgPermissions.Member {
if err := perm.Valid(); err != nil {
errs = append(errs, xerrors.Errorf("org=%q: member: %w", orgID, err))
}
}
}
+9 -7
View File
@@ -33,10 +33,11 @@ func BenchmarkRBACValueAllocation(b *testing.B) {
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
}).WithACLUserList(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
})
}).
WithACLUserList(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
})
jsonSubject := authSubject{
ID: actor.ID,
@@ -107,7 +108,7 @@ func TestRegoInputValue(t *testing.T) {
t.Parallel()
// This is the input that would be passed to the rego policy.
jsonInput := map[string]interface{}{
jsonInput := map[string]any{
"subject": authSubject{
ID: actor.ID,
Roles: must(actor.Roles.Expand()),
@@ -138,7 +139,7 @@ func TestRegoInputValue(t *testing.T) {
t.Parallel()
// This is the input that would be passed to the rego policy.
jsonInput := map[string]interface{}{
jsonInput := map[string]any{
"subject": authSubject{
ID: actor.ID,
Roles: must(actor.Roles.Expand()),
@@ -146,7 +147,7 @@ func TestRegoInputValue(t *testing.T) {
Scope: must(actor.Scope.Expand()),
},
"action": action,
"object": map[string]interface{}{
"object": map[string]any{
"type": obj.Type,
},
}
@@ -282,5 +283,6 @@ func equalRoles(t *testing.T, a, b Role) {
bv, ok := b.ByOrgID[ak]
require.True(t, ok, "org permissions missing: %s", ak)
require.ElementsMatchf(t, av.Org, bv.Org, "org %s permissions", ak)
require.ElementsMatchf(t, av.Member, bv.Member, "member %s permissions", ak)
}
}
+10 -2
View File
@@ -324,11 +324,19 @@ func (p *DBTokenProvider) authorizeRequest(ctx context.Context, roles *rbac.Subj
// rbacResourceOwned is for the level "authenticated". We still need to
// make sure the API key has permissions to connect to the actor's own
// workspace. Scopes would prevent this.
rbacResourceOwned rbac.Object = rbac.ResourceWorkspace.WithOwner(roles.ID)
// TODO: This is an odd repercussion of the org_member permission level.
// This Object used to not specify an org restriction, and `InOrg` would
// actually have a significantly different meaning (only sharing with
// other authenticated users in the same org, whereas the existing behavior
// is to share with any authenticated user). Because workspaces are always
// jointly owned by an organization, there _must_ be an org restriction on
// the object to check the proper permissions. AnyOrg is almost the same,
// but technically excludes users who are not in any organization. This is
// the closest we can get though without more significant refactoring.
rbacResourceOwned rbac.Object = rbac.ResourceWorkspace.WithOwner(roles.ID).AnyOrganization()
)
if dbReq.AccessMethod == AccessMethodTerminal {
rbacAction = policy.ActionSSH
rbacResourceOwned = rbac.ResourceWorkspace.WithOwner(roles.ID)
}
// Do a standard RBAC check. This accounts for share level "owner" and any
+18 -12
View File
@@ -56,9 +56,11 @@ type Role struct {
OrganizationID string `json:"organization_id,omitempty" table:"organization id" format:"uuid"`
DisplayName string `json:"display_name" table:"display name"`
SitePermissions []Permission `json:"site_permissions" table:"site permissions"`
UserPermissions []Permission `json:"user_permissions" table:"user permissions"`
// OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.
OrganizationPermissions []Permission `json:"organization_permissions" table:"organization permissions"`
UserPermissions []Permission `json:"user_permissions" table:"user permissions"`
// OrganizationMemberPermissions are specific for the organization in the field 'OrganizationID' above.
OrganizationMemberPermissions []Permission `json:"organization_member_permissions" table:"organization member permissions"`
}
// CustomRoleRequest is used to edit custom roles.
@@ -66,9 +68,11 @@ type CustomRoleRequest struct {
Name string `json:"name" table:"name,default_sort" validate:"username"`
DisplayName string `json:"display_name" table:"display name"`
SitePermissions []Permission `json:"site_permissions" table:"site permissions"`
UserPermissions []Permission `json:"user_permissions" table:"user permissions"`
// OrganizationPermissions are specific to the organization the role belongs to.
OrganizationPermissions []Permission `json:"organization_permissions" table:"organization permissions"`
UserPermissions []Permission `json:"user_permissions" table:"user permissions"`
// OrganizationMemberPermissions are specific to the organization the role belongs to.
OrganizationMemberPermissions []Permission `json:"organization_member_permissions" table:"organization member permissions"`
}
// FullName returns the role name scoped to the organization ID. This is useful if
@@ -85,11 +89,12 @@ func (r Role) FullName() string {
// CreateOrganizationRole will create a custom organization role
func (c *Client) CreateOrganizationRole(ctx context.Context, role Role) (Role, error) {
req := CustomRoleRequest{
Name: role.Name,
DisplayName: role.DisplayName,
SitePermissions: role.SitePermissions,
OrganizationPermissions: role.OrganizationPermissions,
UserPermissions: role.UserPermissions,
Name: role.Name,
DisplayName: role.DisplayName,
SitePermissions: role.SitePermissions,
UserPermissions: role.UserPermissions,
OrganizationPermissions: role.OrganizationPermissions,
OrganizationMemberPermissions: role.OrganizationMemberPermissions,
}
res, err := c.Request(ctx, http.MethodPost,
@@ -108,11 +113,12 @@ func (c *Client) CreateOrganizationRole(ctx context.Context, role Role) (Role, e
// UpdateOrganizationRole will update an existing custom organization role
func (c *Client) UpdateOrganizationRole(ctx context.Context, role Role) (Role, error) {
req := CustomRoleRequest{
Name: role.Name,
DisplayName: role.DisplayName,
SitePermissions: role.SitePermissions,
OrganizationPermissions: role.OrganizationPermissions,
UserPermissions: role.UserPermissions,
Name: role.Name,
DisplayName: role.DisplayName,
SitePermissions: role.SitePermissions,
UserPermissions: role.UserPermissions,
OrganizationPermissions: role.OrganizationPermissions,
OrganizationMemberPermissions: role.OrganizationMemberPermissions,
}
res, err := c.Request(ctx, http.MethodPut,
+118 -64
View File
@@ -112,6 +112,13 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -147,20 +154,21 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|------------------------------|----------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» assignable` | boolean | false | | |
| `» built_in` | boolean | false | | Built in roles are immutable |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| site_permissions` | array | false | | |
| user_permissions` | array | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------------------------|----------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» assignable` | boolean | false | | |
| `» built_in` | boolean | false | | Built in roles are immutable |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_member_permissions` | array | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| site_permissions` | array | false | | |
| `» user_permissions` | array | false | | |
#### Enumerated Values
@@ -247,6 +255,13 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
{
"display_name": "string",
"name": "string",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -288,6 +303,13 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -323,18 +345,19 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|------------------------------|----------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| site_permissions` | array | false | | |
| user_permissions` | array | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------------------------|----------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_member_permissions` | array | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| site_permissions` | array | false | | |
| `» user_permissions` | array | false | | |
#### Enumerated Values
@@ -421,6 +444,13 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
{
"display_name": "string",
"name": "string",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -462,6 +492,13 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -497,18 +534,19 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|------------------------------|----------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| site_permissions` | array | false | | |
| user_permissions` | array | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------------------------|----------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_member_permissions` | array | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| site_permissions` | array | false | | |
| `» user_permissions` | array | false | | |
#### Enumerated Values
@@ -605,6 +643,13 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization}/memb
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -640,18 +685,19 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization}/memb
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|------------------------------|----------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| site_permissions` | array | false | | |
| user_permissions` | array | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------------------------|----------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_member_permissions` | array | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| site_permissions` | array | false | | |
| `» user_permissions` | array | false | | |
#### Enumerated Values
@@ -968,6 +1014,13 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -1003,20 +1056,21 @@ curl -X GET http://coder-server:8080/api/v2/users/roles \
Status Code **200**
| Name | Type | Required | Restrictions | Description |
|------------------------------|----------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» assignable` | boolean | false | | |
| `» built_in` | boolean | false | | Built in roles are immutable |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| site_permissions` | array | false | | |
| user_permissions` | array | false | | |
| Name | Type | Required | Restrictions | Description |
|-------------------------------------|----------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `[array item]` | array | false | | |
| `» assignable` | boolean | false | | |
| `» built_in` | boolean | false | | Built in roles are immutable |
| `» display_name` | string | false | | |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
| `» organization_member_permissions` | array | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `»» action` | [codersdk.RBACAction](schemas.md#codersdkrbacaction) | false | | |
| `»» negate` | boolean | false | | Negate makes this a negative permission |
| `»» resource_type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
| organization_permissions` | array | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| site_permissions` | array | false | | |
| `» user_permissions` | array | false | | |
#### Enumerated Values
+49 -25
View File
@@ -1137,6 +1137,13 @@
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -1163,16 +1170,17 @@
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------------------|-----------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `assignable` | boolean | false | | |
| `built_in` | boolean | false | | Built in roles are immutable |
| `display_name` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| Name | Type | Required | Restrictions | Description |
|-----------------------------------|-----------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `assignable` | boolean | false | | |
| `built_in` | boolean | false | | Built in roles are immutable |
| `display_name` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `organization_member_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
## codersdk.AuditAction
@@ -2522,6 +2530,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
{
"display_name": "string",
"name": "string",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -2548,13 +2563,14 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------------------|-----------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------|
| `display_name` | string | false | | |
| `name` | string | false | | |
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific to the organization the role belongs to. |
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| Name | Type | Required | Restrictions | Description |
|-----------------------------------|-----------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------|
| `display_name` | string | false | | |
| `name` | string | false | | |
| `organization_member_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization member permissions are specific to the organization the role belongs to. |
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific to the organization the role belongs to. |
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
## codersdk.DAUEntry
@@ -7404,6 +7420,13 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
"display_name": "string",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"organization_member_permissions": [
{
"action": "application_connect",
"negate": true,
"resource_type": "*"
}
],
"organization_permissions": [
{
"action": "application_connect",
@@ -7430,14 +7453,15 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
### Properties
| Name | Type | Required | Restrictions | Description |
|----------------------------|-----------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------|
| `display_name` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| Name | Type | Required | Restrictions | Description |
|-----------------------------------|-----------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------|
| `display_name` | string | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `organization_member_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization member permissions are specific for the organization in the field 'OrganizationID' above. |
| `organization_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | Organization permissions are specific for the organization in the field 'OrganizationID' above. |
| `site_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
| `user_permissions` | array of [codersdk.Permission](#codersdkpermission) | false | | |
## codersdk.RoleSyncSettings
+8
View File
@@ -311,5 +311,13 @@ func validOrganizationRoleRequest(ctx context.Context, req codersdk.CustomRoleRe
return false
}
if len(req.OrganizationMemberPermissions) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request, not allowed to assign organization member permissions for an organization role.",
Detail: "organization scoped roles may not contain organization member permissions",
})
return false
}
return true
}
+1
View File
@@ -199,6 +199,7 @@ export const createCustomRole = async (
},
],
user_permissions: [],
organization_member_permissions: [],
});
return role;
};
+10 -2
View File
@@ -1513,11 +1513,15 @@ export interface CustomRoleRequest {
readonly name: string;
readonly display_name: string;
readonly site_permissions: readonly Permission[];
readonly user_permissions: readonly Permission[];
/**
* OrganizationPermissions are specific to the organization the role belongs to.
*/
readonly organization_permissions: readonly Permission[];
readonly user_permissions: readonly Permission[];
/**
* OrganizationMemberPermissions are specific to the organization the role belongs to.
*/
readonly organization_member_permissions: readonly Permission[];
}
// From codersdk/deployment.go
@@ -4128,11 +4132,15 @@ export interface Role {
readonly organization_id?: string;
readonly display_name: string;
readonly site_permissions: readonly Permission[];
readonly user_permissions: readonly Permission[];
/**
* OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.
*/
readonly organization_permissions: readonly Permission[];
readonly user_permissions: readonly Permission[];
/**
* OrganizationMemberPermissions are specific for the organization in the field 'OrganizationID' above.
*/
readonly organization_member_permissions: readonly Permission[];
}
// From codersdk/rbacroles.go
@@ -66,9 +66,11 @@ const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
initialValues: {
name: role?.name || "",
display_name: role?.display_name || "",
site_permissions: role?.site_permissions || [],
organization_permissions: role?.organization_permissions || [],
user_permissions: role?.user_permissions || [],
site_permissions: role?.site_permissions ?? [],
user_permissions: role?.user_permissions ?? [],
organization_permissions: role?.organization_permissions ?? [],
organization_member_permissions:
role?.organization_member_permissions ?? [],
},
validationSchema,
onSubmit,
@@ -197,8 +197,9 @@ export const MockRoles: (AssignableRoles | Role)[] = [
action: "stop",
},
],
organization_permissions: [],
user_permissions: [],
organization_permissions: [],
organization_member_permissions: [],
assignable: true,
built_in: true,
},
@@ -292,8 +293,9 @@ export const MockRoles: (AssignableRoles | Role)[] = [
action: "read",
},
],
organization_permissions: [],
user_permissions: [],
organization_permissions: [],
organization_member_permissions: [],
assignable: true,
built_in: true,
},
@@ -407,8 +409,9 @@ export const MockRoles: (AssignableRoles | Role)[] = [
action: "create",
},
],
organization_permissions: [],
user_permissions: [],
organization_permissions: [],
organization_member_permissions: [],
assignable: true,
built_in: true,
},
@@ -462,8 +465,9 @@ export const MockRoles: (AssignableRoles | Role)[] = [
action: "read",
},
],
organization_permissions: [],
user_permissions: [],
organization_permissions: [],
organization_member_permissions: [],
built_in: true,
},
];
+24 -13
View File
@@ -279,45 +279,50 @@ export const MockOwnerRole: TypesGen.Role = {
name: "owner",
display_name: "Owner",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: "",
organization_permissions: [],
organization_member_permissions: [],
};
export const MockUserAdminRole: TypesGen.Role = {
name: "user_admin",
display_name: "User Admin",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: "",
organization_permissions: [],
organization_member_permissions: [],
};
export const MockTemplateAdminRole: TypesGen.Role = {
name: "template_admin",
display_name: "Template Admin",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: "",
organization_permissions: [],
organization_member_permissions: [],
};
export const MockAuditorRole: TypesGen.Role = {
name: "auditor",
display_name: "Auditor",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: "",
organization_permissions: [],
organization_member_permissions: [],
};
export const MockWorkspaceCreationBanRole: TypesGen.Role = {
name: "organization-workspace-creation-ban",
display_name: "Organization Workspace Creation Ban",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: "",
organization_permissions: [],
organization_member_permissions: [],
};
export const MockMemberRole: TypesGen.SlimRole = {
@@ -329,27 +334,30 @@ export const MockOrganizationAdminRole: TypesGen.Role = {
name: "organization-admin",
display_name: "Organization Admin",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: MockOrganization.id,
organization_permissions: [],
organization_member_permissions: [],
};
export const MockOrganizationUserAdminRole: TypesGen.Role = {
name: "organization-user-admin",
display_name: "Organization User Admin",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: MockOrganization.id,
organization_permissions: [],
organization_member_permissions: [],
};
export const MockOrganizationTemplateAdminRole: TypesGen.Role = {
name: "organization-template-admin",
display_name: "Organization Template Admin",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: MockOrganization.id,
organization_permissions: [],
organization_member_permissions: [],
};
export const MockOrganizationAuditorRole: TypesGen.AssignableRoles = {
@@ -358,18 +366,20 @@ export const MockOrganizationAuditorRole: TypesGen.AssignableRoles = {
assignable: true,
built_in: false,
site_permissions: [],
organization_permissions: [],
user_permissions: [],
organization_id: MockOrganization.id,
organization_permissions: [],
organization_member_permissions: [],
};
export const MockRoleWithOrgPermissions: TypesGen.AssignableRoles = {
name: "my-role-1",
display_name: "My Role 1",
organization_id: MockOrganization.id,
assignable: true,
built_in: false,
site_permissions: [],
user_permissions: [],
organization_id: MockOrganization.id,
organization_permissions: [
{
negate: false,
@@ -452,14 +462,15 @@ export const MockRoleWithOrgPermissions: TypesGen.AssignableRoles = {
action: "create",
},
],
user_permissions: [],
organization_member_permissions: [],
};
export const MockRole2WithOrgPermissions: TypesGen.Role = {
name: "my-role-1",
display_name: "My Role 1",
organization_id: MockOrganization.id,
site_permissions: [],
user_permissions: [],
organization_id: MockOrganization.id,
organization_permissions: [
{
negate: false,
@@ -467,7 +478,7 @@ export const MockRole2WithOrgPermissions: TypesGen.Role = {
action: "create",
},
],
user_permissions: [],
organization_member_permissions: [],
};
// assignableRole takes a role and a boolean. The boolean implies if the