improvement(governance): org-ws-credential roles clarity#5134
improvement(governance): org-ws-credential roles clarity#5134icecrasher321 wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Backend enforcement adds Product guardrails: PATCH workspace permissions rejects changing org owner/admin roles; org roster shows org admins as admin on all org workspaces. Member and credential UIs disable role changes for inherited roles and show Tests cover credential actor context, org-admin workspace access, and accessible workspace listing elevation. Reviewed by Cursor Bugbot for commit fc753a8. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fc753a8. Configure here.
Greptile SummaryThis PR implements a two-level role-inheritance model: org owners/admins automatically become workspace admins, and workspace admins automatically become credential admins for all shared credentials. It wires that through the API layer, the UI (greyed-out role chips with tooltips), and the
Confidence Score: 4/5The inheritance model is correctly wired in most paths, but
apps/sim/lib/workspaces/utils.ts — Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User] -->|is owner of| WS[Workspace]
A -->|has explicit permission row| WS
A -->|is org owner/admin of| ORG[Organization]
ORG -->|owns| WS
WS -->|resolves to| WPERM{Effective Workspace Permission}
WPERM -->|owner or org-admin| WADMIN[workspace: admin]
WPERM -->|explicit write| WWRITE[workspace: write]
WPERM -->|explicit read| WREAD[workspace: read]
WPERM -->|none| WNONE[no access]
WADMIN -->|shared credential| CADMIN[credential: admin auto-derived]
WWRITE -->|explicit member row| CMEMBER[credential: member]
WREAD -->|explicit member row| CMEMBER
WWRITE -->|explicit admin row| CADMIN2[credential: admin explicit]
WREAD -->|explicit admin row| CADMIN2
CADMIN --> CAN_MANAGE[can use + edit + delete + share]
CADMIN2 --> CAN_MANAGE
CMEMBER --> CAN_USE[can use only]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[User] -->|is owner of| WS[Workspace]
A -->|has explicit permission row| WS
A -->|is org owner/admin of| ORG[Organization]
ORG -->|owns| WS
WS -->|resolves to| WPERM{Effective Workspace Permission}
WPERM -->|owner or org-admin| WADMIN[workspace: admin]
WPERM -->|explicit write| WWRITE[workspace: write]
WPERM -->|explicit read| WREAD[workspace: read]
WPERM -->|none| WNONE[no access]
WADMIN -->|shared credential| CADMIN[credential: admin auto-derived]
WWRITE -->|explicit member row| CMEMBER[credential: member]
WREAD -->|explicit member row| CMEMBER
WWRITE -->|explicit admin row| CADMIN2[credential: admin explicit]
WREAD -->|explicit admin row| CADMIN2
CADMIN --> CAN_MANAGE[can use + edit + delete + share]
CADMIN2 --> CAN_MANAGE
CMEMBER --> CAN_USE[can use only]
|
| const [membership] = await db | ||
| .select({ organizationId: member.organizationId, role: member.role }) | ||
| .from(member) | ||
| .where(eq(member.userId, userId)) | ||
| .limit(1) | ||
|
|
||
| if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) { | ||
| return [] | ||
| } |
There was a problem hiding this comment.
Role filter missing before
.limit(1)
The query fetches any membership for the user and checks the role after, but .limit(1) without a role filter means the DB can return a non-admin row first (ordering is non-deterministic without ORDER BY). A user who is an admin in org-B but whose first returned row happens to be a member row for org-A will silently get [] back — their derived workspace admin access will be invisible in the sidebar, permission checks, and getManageableWorkspaces.
The inArray(member.role, ['owner', 'admin']) predicate needs to be inside the WHERE clause, not just checked on the result after the limit is applied.
| const [membership] = await db | |
| .select({ organizationId: member.organizationId, role: member.role }) | |
| .from(member) | |
| .where(eq(member.userId, userId)) | |
| .limit(1) | |
| if (!membership || (membership.role !== 'owner' && membership.role !== 'admin')) { | |
| return [] | |
| } | |
| const [membership] = await db | |
| .select({ organizationId: member.organizationId, role: member.role }) | |
| .from(member) | |
| .where(and(eq(member.userId, userId), inArray(member.role, ['owner', 'admin']))) | |
| .limit(1) | |
| if (!membership) { | |
| return [] | |
| } |

Summary
Org Admins are auto Workspace Admins. And workspace admins are auto credential admins.
Type of Change
Testing
Tested manually
Checklist