diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 1b19947ea65..2fee0942a44 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -1055,44 +1055,94 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions { }) } - // Uses allPermsExcept to automatically include permissions for new resources. - memberPerms := append( - allPermsExcept( - ResourceWorkspaceDormant, - ResourcePrebuiltWorkspace, - ResourceUser, - ResourceOrganizationMember, - ResourceBoundaryLog, - ResourceAibridgeInterception, - // Chat access requires the agents-access role. - ResourceChat, - ), + // Enumerate the per-member resources explicitly so new resources do + // not auto-grant to org members. Adding a resource to the codebase + // requires an explicit decision to expose it here. + // + // Member-level grants only fire when input.object.owner == + // input.subject.id (see the org_member rule in + // coderd/rbac/policy.rego). Only resources whose RBACObject() calls + // WithOwner(...) at production call sites belong here; see the + // "Intentionally omitted" block at the bottom. + memberPerms := Permissions(map[string][]policy.Action{ + // Workspace lifecycle on resources owned by this member. + ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(), + + // Dormant workspaces share the workspace action set minus the + // build, ssh, and exec actions. + ResourceWorkspaceDormant.Type: { + policy.ActionRead, + policy.ActionDelete, + policy.ActionCreate, + policy.ActionUpdate, + policy.ActionWorkspaceStop, + policy.ActionCreateAgent, + policy.ActionDeleteAgent, + policy.ActionUpdateAgent, + }, - 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, - policy.ActionUpdateAgent, - }, - // Can read their own organization member record. - ResourceOrganizationMember.Type: { - policy.ActionRead, - }, - // Members can create and update AI Bridge interceptions but - // cannot read them back. - ResourceAibridgeInterception.Type: { - policy.ActionCreate, - policy.ActionUpdate, - }, - })..., - ) + // Upload and read template files the member created during + // workspace build (File.RBACObject sets WithOwner(CreatedBy)). + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + + // Create and read user-scoped provisioner daemons. The Upsert + // path in dbauthz sets WithOwner(tag_owner) when scope=user, so + // members can run their own daemons. Read is granted for + // symmetry with workspace ownership: members can inspect + // daemons they spawned even though no production call site + // currently uses the member-scope read path (read on the bare + // InOrg object continues to require Org-level perms). + ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}, + + // Tasks ride along with workspaces and are owner-scoped. + ResourceTask.Type: ResourceTask.AvailableActions(), + + // Read-self group-membership record. GroupMember.RBACObject + // sets WithOwner to the user's own ID. + ResourceGroupMember.Type: {policy.ActionRead}, + + // Read-self org-member record. + ResourceOrganizationMember.Type: {policy.ActionRead}, + + // Members can create and update AI Bridge interceptions they + // initiate (dbauthz layer sets WithOwner(InitiatorID)) but + // cannot read them back. Chat access requires the + // agents-access role and is intentionally not granted here. + ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate}, + + // Own session tokens and workspace agent auth keys. + ResourceApiKey.Type: ResourceApiKey.AvailableActions(), + + // User-scoped notification surfaces. All three resources are + // addressed by WithOwner(user_id) at the call sites. + ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate}, + ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(), + ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(), + + // Intentionally omitted at Member scope (resources without an + // Owner field on their RBACObject; Member-level grants never + // fire for them). Listed here so a future maintainer who sees + // these dropped relative to the legacy allPermsExcept(...) + // wildcard does not "restore" them: + // + // - ResourceTemplate: templates have no owner. Org-member + // template.use is authorized via the ACL path + // (acl_group_list[org_owner] "Everyone" group, populated + // on each template's GroupACL). + // - ResourceGroup: groups have no owner. "Groups I'm a + // member of can read themselves" is granted via the + // per-group GroupACL. + // - ResourceWorkspaceProxy, ResourceProvisionerJobs, + // ResourceWorkspaceAgentResourceMonitor, + // ResourceWorkspaceAgentDevcontainers, + // ResourceTailnetCoordinator, ResourceReplicas: these + // resources have no DB model that sets Owner; all + // production call sites use the bare resource or + // .InOrg(...) only. Access for these flows through Org + // perms on the appropriate role (e.g. ProvisionerDaemon + // above), or through system / agent / template-admin + // roles defined elsewhere. + }) if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersEveryone { memberPerms = append(memberPerms, Permission{ @@ -1140,45 +1190,75 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions { } // service account-scoped permissions (resources owned by the - // service account). Uses allPermsExcept to automatically include - // permissions for new resources. - memberPerms := append( - allPermsExcept( - ResourceWorkspaceDormant, - ResourcePrebuiltWorkspace, - ResourceUser, - ResourceOrganizationMember, - ResourceBoundaryLog, - ResourceAibridgeInterception, - // Chat access requires the agents-access role. - ResourceChat, - ), + // service account). Enumerated explicitly so new resources do not + // auto-grant to service accounts. + // + // Member-level grants only fire when input.object.owner == + // input.subject.id (see the org_member rule in + // coderd/rbac/policy.rego). Only resources whose RBACObject() calls + // WithOwner(...) at production call sites belong here; see the + // "Intentionally omitted" block at the bottom. + memberPerms := Permissions(map[string][]policy.Action{ + // Workspace lifecycle on resources owned by this service account. + ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(), + + // Dormant workspaces share the workspace action set minus the + // build, ssh, and exec actions. + ResourceWorkspaceDormant.Type: { + policy.ActionRead, + policy.ActionDelete, + policy.ActionCreate, + policy.ActionUpdate, + policy.ActionWorkspaceStop, + policy.ActionCreateAgent, + policy.ActionDeleteAgent, + policy.ActionUpdateAgent, + }, - 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, - policy.ActionUpdateAgent, - }, - // Can read their own organization member record. - ResourceOrganizationMember.Type: { - policy.ActionRead, - }, - // Service accounts can create and update AI Bridge - // interceptions but cannot read them back. - ResourceAibridgeInterception.Type: { - policy.ActionCreate, - policy.ActionUpdate, - }, - })..., - ) + // Upload and read template files the service account created + // during workspace build (File.RBACObject sets + // WithOwner(CreatedBy)). + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + + // Create and read user-scoped provisioner daemons. The Upsert + // path in dbauthz sets WithOwner(tag_owner) when scope=user, so + // service accounts can run their own daemons. Read is granted + // for symmetry with workspace ownership: service accounts can + // inspect daemons they spawned even though no production call + // site currently uses the member-scope read path (read on the + // bare InOrg object continues to require Org-level perms). + ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}, + + // Tasks ride along with workspaces and are owner-scoped. + ResourceTask.Type: ResourceTask.AvailableActions(), + + // Read-self group-membership record. GroupMember.RBACObject + // sets WithOwner to the user's own ID. + ResourceGroupMember.Type: {policy.ActionRead}, + + // Read-self org-member record. + ResourceOrganizationMember.Type: {policy.ActionRead}, + + // Service accounts can create and update AI Bridge + // interceptions they initiate (dbauthz layer sets + // WithOwner(InitiatorID)) but cannot read them back. Chat + // access requires the agents-access role and is intentionally + // not granted here. + ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate}, + + // Own session tokens and workspace agent auth keys. + ResourceApiKey.Type: ResourceApiKey.AvailableActions(), + + // User-scoped notification surfaces. All three resources are + // addressed by WithOwner(user_id) at the call sites. + ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate}, + ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(), + ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(), + + // Intentionally omitted at Member scope. See + // OrgMemberPermissions above for the rationale; the service + // account role mirrors the same partition. + }) return OrgRolePermissions{Org: orgPerms, Member: memberPerms} }