mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: migrate agents-access to org-scoped system role for proper chat RBAC (#24438)
The agents-access role previously granted chat permissions at user
scope, but chats are org-scoped objects. Rego skips user-level perms
when org_owner is set, making the grants invisible. Handler-level
band-aids used synthetic non-org-scoped objects as a workaround.
- Migrates agents-access from users.rbac_roles (site-level) to
organization_members.roles (org-scoped) via DB migration
- Redefines agents-access as a predefined org-scoped builtin role
alongside organization-admin, organization-auditor, etc., with
Member permissions granting chat create/read/update
- Excludes ResourceChat from OrgMemberPermissions so org membership
alone no longer grants chat access
- Fixes handler Authorize checks to use org-scoped objects with
semantically correct actions (ActionUpdate for message/tool operations)
- Grants org admins the ability to assign agents-access
Closes #24250
Fixes CODAGT-174
Note: this does not update the "Usage" endpoints. Tracked by CODAGT-161.
> 🤖
This commit is contained in:
+40
-33
@@ -143,7 +143,7 @@ func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAd
|
||||
func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} }
|
||||
func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} }
|
||||
func RoleAuditor() RoleIdentifier { return RoleIdentifier{Name: auditor} }
|
||||
func RoleAgentsAccess() RoleIdentifier { return RoleIdentifier{Name: agentsAccess} }
|
||||
func RoleAgentsAccess() string { return agentsAccess }
|
||||
|
||||
func RoleOrgAdmin() string {
|
||||
return orgAdmin
|
||||
@@ -199,6 +199,10 @@ func ScopedRoleOrgWorkspaceCreationBan(organizationID uuid.UUID) RoleIdentifier
|
||||
return RoleIdentifier{Name: RoleOrgWorkspaceCreationBan(), OrganizationID: organizationID}
|
||||
}
|
||||
|
||||
func ScopedRoleAgentsAccess(organizationID uuid.UUID) RoleIdentifier {
|
||||
return RoleIdentifier{Name: RoleAgentsAccess(), OrganizationID: organizationID}
|
||||
}
|
||||
|
||||
func allPermsExcept(excepts ...Objecter) []Permission {
|
||||
resources := AllResources()
|
||||
var perms []Permission
|
||||
@@ -404,21 +408,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ByOrgID: map[string]OrgPermissions{},
|
||||
}.withCachedRegoValue()
|
||||
|
||||
agentsAccessRole := Role{
|
||||
Identifier: RoleAgentsAccess(),
|
||||
DisplayName: "Coder Agents User",
|
||||
Site: []Permission{},
|
||||
User: Permissions(map[string][]policy.Action{
|
||||
ResourceChat.Type: {
|
||||
policy.ActionCreate,
|
||||
policy.ActionRead,
|
||||
policy.ActionUpdate,
|
||||
policy.ActionDelete,
|
||||
},
|
||||
}),
|
||||
ByOrgID: map[string]OrgPermissions{},
|
||||
}.withCachedRegoValue()
|
||||
|
||||
builtInRoles = map[string]func(orgID uuid.UUID) Role{
|
||||
// admin grants all actions to all resources.
|
||||
owner: func(_ uuid.UUID) Role {
|
||||
@@ -449,14 +438,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
return userAdminRole
|
||||
},
|
||||
|
||||
// agentsAccess grants all actions on chat resources owned
|
||||
// by the user. Without this role, members can still read,
|
||||
// update, and delete their own chats via org membership,
|
||||
// but cannot create chats or trigger AI inference.
|
||||
agentsAccess: func(_ uuid.UUID) Role {
|
||||
return agentsAccessRole
|
||||
},
|
||||
|
||||
// orgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
orgAdmin: func(organizationID uuid.UUID) Role {
|
||||
@@ -606,6 +587,30 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
},
|
||||
}
|
||||
},
|
||||
// agentsAccess grants org members permission to create, read, and
|
||||
// update chats. ActionDelete is intentionally excluded: no dbauthz
|
||||
// function checks it on ResourceChat. Hard-deletion goes through
|
||||
// ResourceSystem (dbpurge).
|
||||
agentsAccess: func(organizationID uuid.UUID) Role {
|
||||
return Role{
|
||||
Identifier: RoleIdentifier{Name: agentsAccess, OrganizationID: organizationID},
|
||||
DisplayName: "Coder Agents User",
|
||||
Site: []Permission{},
|
||||
User: []Permission{},
|
||||
ByOrgID: map[string]OrgPermissions{
|
||||
organizationID.String(): {
|
||||
Org: []Permission{},
|
||||
Member: Permissions(map[string][]policy.Action{
|
||||
ResourceChat.Type: {
|
||||
policy.ActionCreate,
|
||||
policy.ActionRead,
|
||||
policy.ActionUpdate,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,10 +665,13 @@ var assignRoles = map[string]map[string]bool{
|
||||
orgTemplateAdmin: true,
|
||||
orgWorkspaceCreationBan: true,
|
||||
customOrganizationRole: true,
|
||||
agentsAccess: true,
|
||||
},
|
||||
orgUserAdmin: {
|
||||
orgMember: true,
|
||||
orgMember: true,
|
||||
agentsAccess: true,
|
||||
},
|
||||
|
||||
prebuildsOrchestrator: {
|
||||
orgMember: true,
|
||||
},
|
||||
@@ -886,20 +894,13 @@ func SiteBuiltInRoles() []Role {
|
||||
for _, roleF := range builtInRoles {
|
||||
// Must provide some non-nil uuid to filter out org roles.
|
||||
role := roleF(uuid.New())
|
||||
if !role.Identifier.IsOrgRole() && role.Identifier != RoleAgentsAccess() {
|
||||
if !role.Identifier.IsOrgRole() {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
// AgentsAccessRole returns the agents-access role for use by callers
|
||||
// that need to include it conditionally (e.g. when the agents
|
||||
// experiment is enabled).
|
||||
func AgentsAccessRole() Role {
|
||||
return builtInRoles[agentsAccess](uuid.Nil)
|
||||
}
|
||||
|
||||
// ChangeRoleSet is a helper function that finds the difference of 2 sets of
|
||||
// roles. When setting a user's new roles, it is equivalent to adding and
|
||||
// removing roles. This set determines the changes, so that the appropriate
|
||||
@@ -1041,7 +1042,10 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
|
||||
ResourceUser,
|
||||
ResourceOrganizationMember,
|
||||
ResourceAibridgeInterception,
|
||||
// Chat access requires the agents-access role.
|
||||
ResourceChat,
|
||||
),
|
||||
|
||||
Permissions(map[string][]policy.Action{
|
||||
// Reduced permission set on dormant workspaces. No build,
|
||||
// ssh, or exec.
|
||||
@@ -1123,7 +1127,10 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
|
||||
ResourceUser,
|
||||
ResourceOrganizationMember,
|
||||
ResourceAibridgeInterception,
|
||||
// Chat access requires the agents-access role.
|
||||
ResourceChat,
|
||||
),
|
||||
|
||||
Permissions(map[string][]policy.Action{
|
||||
// Reduced permission set on dormant workspaces. No build,
|
||||
// ssh, or exec.
|
||||
|
||||
+31
-12
@@ -49,11 +49,6 @@ func TestBuiltInRoles(t *testing.T) {
|
||||
require.NoError(t, r.Valid(), "invalid role")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("agents-access", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, rbac.AgentsAccessRole().Valid(), "invalid role")
|
||||
})
|
||||
}
|
||||
|
||||
// permissionGranted checks whether a permission list contains a
|
||||
@@ -204,7 +199,21 @@ func TestRolePermissions(t *testing.T) {
|
||||
orgUserAdmin := authSubject{Name: "org_user_admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgUserAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
orgTemplateAdmin := authSubject{Name: "org_template_admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgTemplateAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
orgAdminBanWorkspace := authSubject{Name: "org_admin_workspace_ban", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID), rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
agentsAccessUser := authSubject{Name: "chat_access", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAgentsAccess()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
agentsAccessUser := func() authSubject {
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
agentsRole, err := rbac.RoleByName(rbac.ScopedRoleAgentsAccess(orgID))
|
||||
require.NoError(t, err)
|
||||
return authSubject{
|
||||
Name: "agents_access",
|
||||
Actor: rbac.Subject{
|
||||
ID: currentUser.String(),
|
||||
Roles: rbac.Roles{memberRole, agentsRole},
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue(),
|
||||
}
|
||||
}()
|
||||
|
||||
orgMemberMe := func() authSubject {
|
||||
memberRole, err := rbac.RoleByName(rbac.RoleMember())
|
||||
require.NoError(t, err)
|
||||
@@ -580,8 +589,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
}),
|
||||
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin},
|
||||
true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1096,12 +1105,21 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatUsage",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Name: "ChatUsageCRU",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, orgMemberMe},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
true: {owner, orgAdmin, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatUsageDelete",
|
||||
Actions: []policy.Action{policy.ActionDelete},
|
||||
Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, orgMemberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1258,6 +1276,7 @@ func TestListRoles(t *testing.T) {
|
||||
fmt.Sprintf("organization-user-admin:%s", orgID.String()),
|
||||
fmt.Sprintf("organization-template-admin:%s", orgID.String()),
|
||||
fmt.Sprintf("organization-workspace-creation-ban:%s", orgID.String()),
|
||||
fmt.Sprintf("agents-access:%s", orgID.String()),
|
||||
},
|
||||
orgRoleNames)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user