Skip to content

fix(credentials): grant admin credential role to workspace admins#4736

Open
serhiizghama wants to merge 2 commits into
simstudioai:mainfrom
serhiizghama:fix/workspace-admin-credential-role
Open

fix(credentials): grant admin credential role to workspace admins#4736
serhiizghama wants to merge 2 commits into
simstudioai:mainfrom
serhiizghama:fix/workspace-admin-credential-role

Conversation

@serhiizghama
Copy link
Copy Markdown

Problem

Workspace admin users (users with permissionType = 'admin' in the permissions table for a workspace) receive a member role on credential_member when a workspace-scoped secret is created. This prevents them from editing or deleting the secret, even though they have admin access to the workspace.

Root cause: three code paths that create credential_member rows determined the role solely by checking memberUserId === ownerUserId, ignoring the permissions table:

  • ensureWorkspaceCredentialMemberships in lib/credentials/environment.ts
  • syncWorkspaceEnvCredentials (calls the above)
  • createWorkspaceEnvCredentials (inline bulk insert)
  • POST handler in app/api/credentials/route.ts

Solution

Add a getWorkspaceAdminUserIds(workspaceId) helper that returns a Set<string> containing:

  • the workspace owner (workspace.ownerId)
  • all users with permissionType = 'admin' on the workspace (permissions table)

Use this set instead of the bare owner ID check in all four affected paths so workspace admins receive admin on credential_member.

Testing

  1. Create a workspace with two members: owner and an admin user (set via the permissions panel)
  2. As the owner, go to Settings → Secrets and create a workspace-scoped secret
  3. Log in as the admin user — they should now be able to edit and delete the secret

Before this fix: admin user got member role and saw a permission error.
After this fix: admin user gets admin role and can manage the secret.

Add getWorkspaceAdminUserIds helper that queries the permissions table for
users with permissionType='admin' on the workspace (plus the owner).

Update ensureWorkspaceCredentialMemberships, syncWorkspaceEnvCredentials,
and createWorkspaceEnvCredentials to use adminUserIds when determining the
credential_member role, so workspace admins receive 'admin' instead of
'member' and can edit or delete shared workspace secrets.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

@serhiizghama is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 25, 2026

PR Summary

Medium Risk
Changes who receives admin on credential_member for workspace secrets, which gates secret edit/delete; scope is small and aligned with workspace admin permissions, but existing memberships may stay wrong until re-synced.

Overview
Fixes a permissions bug where workspace admins (users with workspace admin in permissions, not only the owner) were assigned member on credential_member when workspace secrets were created or synced, blocking edit/delete despite workspace admin access.

Adds getWorkspaceAdminUserIds, which returns the owner plus all workspace permissionType = 'admin' users, and uses that set everywhere credential_member roles are assigned: the credentials POST handler for env_workspace / service_account, ensureWorkspaceCredentialMemberships, syncWorkspaceEnvCredentials, and createWorkspaceEnvCredentials. The creator still gets admin via an explicit check alongside the admin set.

Existing rows are not backfilled by this change; only new creates and env sync paths that run membership ensure get the corrected roles.

Reviewed by Cursor Bugbot for commit 7819ed3. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Greptile Summary

This PR fixes workspace admin users receiving a member role on credential_member records instead of the expected admin role when workspace-scoped secrets are created. It introduces a getWorkspaceAdminUserIds helper that returns the workspace owner plus all users with permissionType = 'admin' in the permissions table, and threads this set through all four affected code paths.

  • getWorkspaceAdminUserIds — new exported helper in environment.ts; queries workspace and permissions in parallel, returns a Set<string> used for role determination.
  • ensureWorkspaceCredentialMemberships / syncWorkspaceEnvCredentials / createWorkspaceEnvCredentials — now accept/fetch adminUserIds and use adminUserIds.has(memberUserId) instead of the previous owner-only equality check.
  • POST /api/credentials — fetches adminUserIds alongside workspaceMemberUserIds and preserves the creator-always-gets-admin fallback (|| memberUserId === session.user.id).

Confidence Score: 4/5

Safe to merge; the fix correctly identifies all affected insertion paths and the admin-set construction is sound. The only concerns are extra DB round-trips and a now-misleading parameter name in the internal helper.

The core logic is correct across all four code paths. Both syncWorkspaceEnvCredentials and createWorkspaceEnvCredentials now issue three workspace-table queries and two permissions queries per call where one of each would suffice — that's a present inefficiency introduced by this PR, not a future risk. No data-correctness bugs or auth regressions were found.

apps/sim/lib/credentials/environment.ts — redundant DB queries in the two bulk-credential functions and the misleading ownerUserId parameter name in the private helper.

Important Files Changed

Filename Overview
apps/sim/lib/credentials/environment.ts Adds getWorkspaceAdminUserIds helper and plumbs it through ensureWorkspaceCredentialMemberships, syncWorkspaceEnvCredentials, and createWorkspaceEnvCredentials. The logic is correct, but calling both getWorkspaceMemberUserIds and getWorkspaceAdminUserIds alongside an existing workspace query results in 3 workspace-table hits and 2 permissions-table hits per operation where 1 each would suffice.
apps/sim/app/api/credentials/route.ts POST handler updated to fetch adminUserIds and use it for role assignment; also preserves the creator-always-gets-admin fallback. Change is logically correct and handles the service_account type as well.

Sequence Diagram

sequenceDiagram
    participant Client
    participant POST as POST /api/credentials
    participant ENV as environment.ts helpers
    participant DB as Database

    Client->>POST: "POST {type: env_workspace/service_account}"
    POST->>DB: BEGIN TRANSACTION
    POST->>DB: INSERT credential
    POST->>ENV: getWorkspaceMemberUserIds(workspaceId)
    ENV->>DB: SELECT workspace (ownerId)
    ENV->>DB: SELECT permissions (all members)
    ENV-->>POST: memberUserIds[]
    POST->>ENV: getWorkspaceAdminUserIds(workspaceId)
    ENV->>DB: SELECT workspace (ownerId) ← duplicate query
    ENV->>DB: "SELECT permissions WHERE permissionType='admin'"
    ENV-->>POST: adminIds (Set)
    loop each memberUserId
        POST->>DB: "INSERT credentialMember (role = adminIds.has(uid) OR uid==creator ? admin : member)"
    end
    POST->>DB: COMMIT
    POST-->>Client: 200 OK
Loading

Reviews (1): Last reviewed commit: "fix(credentials): apply admin role check..." | Re-trigger Greptile

Comment on lines +155 to 163
const [[workspaceRow], memberUserIds, adminUserIds] = await Promise.all([
db
.select({ ownerId: workspace.ownerId })
.from(workspace)
.where(eq(workspace.id, workspaceId))
.limit(1),
getWorkspaceMemberUserIds(workspaceId),
getWorkspaceAdminUserIds(workspaceId),
])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant workspace queries from helper functions

syncWorkspaceEnvCredentials (and createWorkspaceEnvCredentials at line 246) now issues three separate workspace-table SELECTs per call: one directly in the Promise.all, one inside getWorkspaceMemberUserIds, and one inside getWorkspaceAdminUserIds. The permissions table is also queried twice (once for all members, once filtered to admin). A single SELECT permissions WHERE entityId = workspaceId joined with the workspace owner fetch would cover both sets, eliminating the extra round-trips.

Comment on lines 89 to 94
async function ensureWorkspaceCredentialMemberships(
credentialId: string,
memberUserIds: string[],
ownerUserId: string
ownerUserId: string,
adminUserIds: Set<string>
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ownerUserId parameter now serves only as invitedBy

After this change, ownerUserId is no longer used for role determination inside ensureWorkspaceCredentialMembershipsadminUserIds handles that entirely. The parameter is still used for invitedBy, but its name no longer reflects that narrowed purpose. Callers reading the signature may incorrectly assume it still drives the admin-role check. Consider renaming it to invitedByUserId (or a similar name) to avoid confusion.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant