Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add an organization member permission level#19953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
aslilac wants to merge16 commits intolilac/by-org-id
base:lilac/by-org-id
Choose a base branch
Loading
fromlilac/organization-member-level
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
16 commits
Select commitHold shift + click to select a range
77432a2
start adding an organization member permission level
aslilacSep 24, 2025
3f0a5bf
:^)
aslilacSep 24, 2025
b748e6a
you
aslilacSep 25, 2025
74a6c99
fixish
aslilacSep 26, 2025
c24d0dc
ok well one of you did something
aslilacSep 26, 2025
8135c68
(probably terrible) vibes
aslilacSep 26, 2025
945b0cb
update readme table for rbac document
EmyrkOct 1, 2025
4344ed2
update rego policy to match new table
EmyrkOct 2, 2025
ff6552e
update roles
EmyrkOct 2, 2025
70651c6
update tests and policy
EmyrkOct 2, 2025
dcf52f8
fix policy with any_org partial
EmyrkOct 2, 2025
6c64621
Add more perms to org members
EmyrkOct 2, 2025
52f1d1c
`ByOrgID`
aslilacOct 6, 2025
b8446de
chore: update rego to combined org + member permissions
EmyrkOct 6, 2025
a648977
Merge branch 'main' into lilac/organization-member-level
aslilacOct 7, 2025
fd71845
Merge branch 'lilac/by-org-id' into lilac/organization-member-level
aslilacOct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletionscoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

21 changes: 21 additions & 0 deletionscoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

21 changes: 12 additions & 9 deletionscoderd/rbac/README.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,18 @@ A _role_ is a set of permissions. When evaluating a role's permission to form an
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.

| 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 |
| Role (example) | Site | Org | OrgMember | User | Result |
|-----------------|------|------|-----------|------|--------|
| site-admin | Y | YN\_ | YN\_ | YN\_ | Y |
| no-permission | N | YN\_ | YN\_ | YN\_ | N |
| org-admin | \_ | Y | YN\_ | YN\_ | Y |
| non-org-member | \_ | N | YN\_ | YN\_ | N |
| org-object | \_ | \_ | Y | YN\_ | N |
| org-object | \_ | \_ | N | YN\_ | N |
| org-object | \_ | \_ | \_ | YN\_ | N |
| no-org | \_ | \_ | YN\_ | Y | Y |
| no-org | \_ | \_ | YN\_ | N | N |
| unauthenticated | \_ | \_ | \_ | \_ | N |

## Scopes

Expand Down
8 changes: 8 additions & 0 deletionscoderd/rbac/astvalue.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,9 +165,17 @@ 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)),
},
),
))
}
orgMemberMap := ast.NewObject()
for k, p := range role.OrgMember {
orgMemberMap.Insert(ast.StringTerm(k), ast.NewTerm(regoSlice(p)))
}
return ast.NewObject(
[2]*ast.Term{
ast.StringTerm("site"),
Expand Down
23 changes: 18 additions & 5 deletionscoderd/rbac/authz_internal_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -389,7 +389,9 @@ 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},
// ResourceWorkspace WITHOUT an organization. Should never happen in prod. The default member role omits these
// permissions.
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},

{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},

Expand DownExpand Up@@ -455,6 +457,7 @@ func TestAuthorizeDomain(t *testing.T) {
Scope: must(ExpandScope(ScopeAll)),
Roles: Roles{
must(RoleByName(ScopedRoleOrgAdmin(defOrg))),
must(RoleByName(ScopedRoleOrgMember(defOrg))),
must(RoleByName(RoleMember())),
},
}
Expand All@@ -469,7 +472,8 @@ 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},
// Workspace is not in any organization, will never happen in prod.
{resource: ResourceWorkspace.WithOwner(user.ID), actions: ResourceWorkspace.AvailableActions(), allow: false},

{resource: ResourceWorkspace.All(), actions: ResourceWorkspace.AvailableActions(), allow: false},

Expand DownExpand Up@@ -546,7 +550,8 @@ 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},
// Workspace with no ownership will never happen in prod.
{resource: ResourceWorkspace.WithOwner(user.ID), allow: false},

{resource: ResourceWorkspace.All(), allow: false},

Expand DownExpand Up@@ -647,6 +652,11 @@ func TestAuthorizeDomain(t *testing.T) {
ResourceType: "*",
Action: policy.ActionRead,
}},
Member: []Permission{{
Negate: false,
ResourceType: "*",
Action: policy.ActionRead,
}},
},
},
},
Expand DownExpand Up@@ -1150,6 +1160,9 @@ func TestAuthorizeScope(t *testing.T) {
Org: Permissions(map[string][]policy.Action{
ResourceWorkspace.Type: {policy.ActionRead},
}),
Member: Permissions(map[string][]policy.Action{
ResourceWorkspace.Type: {policy.ActionRead},
}),
},
},
},
Expand DownExpand Up@@ -1316,9 +1329,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 {
forsi, cases := range sets {
for i, c := range cases {
caseName := fmt.Sprintf("%s/%d", name, i)
caseName := fmt.Sprintf("%s/%d-%d", name, si, i)
t.Run(caseName, func(t *testing.T) {
t.Parallel()
for _, a := range c.actions {
Expand Down
54 changes: 48 additions & 6 deletionscoderd/rbac/policy.rego
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,9 @@ scope_user := user_allow([input.subject.scope])

user_allow(roles) := num if {
input.object.owner != ""
# if there is an org, use org_member permissions instead
input.object.org_owner == ""
not input.object.any_org
input.subject.id = input.object.owner

allow := {is_allowed |
Expand All@@ -246,6 +249,28 @@ user_allow(roles) := num if {
num := number(allow)
}

# -------------------
# Organization Member Owner Rules
# -------------------

# 'org_member' applies if the object is owned by both the user and an organization.
# It replaces the `user` permissions in this case.
default org_member := 0
org_member := num if {
# Object must be jointly owned by the user
input.object.owner != ""
input.subject.id = input.object.owner
num := org_allow(input.subject.roles, "member")
}

default scope_org_member := 0
scope_org_member := num if {
# Object must be jointly owned by the user
input.object.owner != ""
input.subject.id = input.object.owner
num := org_allow([input.subject.scope], "member")
}

# 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 {
Expand DownExpand Up@@ -285,16 +310,16 @@ scope_allow_list if {
# Role-Specific Rules
# -------------------

role_allow if {
role_allow if { # site level authed
site = 1
}

role_allow if {
role_allow if { # org level authed
not site = -1
org = 1
}

role_allow if {
role_allow if { # user level authed
not site = -1
not org = -1

Expand All@@ -304,22 +329,30 @@ role_allow if {
user = 1
}

role_allow if { # org member auth
not site = -1
not org = -1

# Organization member owner permissions require both ownership and org membership
org_member = 1
}

# -------------------
# Scope-Specific Rules
# -------------------

scope_allow if {
scope_allow if { # scope site level authed
scope_allow_list
scope_site = 1
}

scope_allow if {
scope_allow if { # scope org level authed
scope_allow_list
not scope_site = -1
scope_org = 1
}

scope_allow if {
scope_allow if { # scope user level authed
scope_allow_list
not scope_site = -1
not scope_org = -1
Expand All@@ -330,6 +363,15 @@ scope_allow if {
scope_user = 1
}

scope_allow if { # scope org member auth
scope_allow_list
not scope_site = -1
not scope_org = -1

# Organization member owner permissions require both ownership and org membership
scope_org_member = 1
}

# -------------------
# ACL-Specific Rules
# Access Control List
Expand Down
34 changes: 24 additions & 10 deletionscoderd/rbac/roles.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -297,15 +297,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
}),
User: 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},
// 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},
})...,
),
ByOrgID: map[string]OrgPermissions{},
Expand DownExpand Up@@ -454,6 +448,19 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Can read available roles.
ResourceAssignOrgRole.Type: {policy.ActionRead},
}),
Member: Permissions(map[string][]policy.Action{
// Users can create provisioner daemons scoped to themselves.
// All provisioners still need an organization relation as well.
ResourceProvisionerDaemon.Type: ResourceProvisionerDaemon.AvailableActions(),
// All group members can read their own group membership
ResourceGroupMember.Type: {policy.ActionRead},
ResourceInboxNotification.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(),
// 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},
}),
},
},
}
Expand DownExpand Up@@ -680,9 +687,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.
Expand All@@ -703,7 +711,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
Expand All@@ -723,6 +732,11 @@ func (role Role) Valid() error {
errs = append(errs, xerrors.Errorf("org=%q: %w", orgID, err))
}
}
for _, perm := range orgPermissions.Member {
if err := perm.Valid(); err != nil {
errs = append(errs, xerrors.Errorf("org=%q: %w", orgID, err))
}
}
}

for _, perm := range role.User {
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp