Skip to content

Commit 6b860ec

Browse files
waleedlatif1claude
andcommitted
fix(security): align workspace env admin gate with hasWorkspaceAdminAccess
Use the same admin check the secrets UI uses (owner, admin permission, or org-admin) so owners and org-admins are not wrongly denied their own decrypted workspace secrets, while read-only members remain restricted to names only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4fa9b4e commit 6b860ec

2 files changed

Lines changed: 28 additions & 8 deletions

File tree

apps/sim/app/api/workspaces/[id]/environment/route.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ describe('GET /api/workspaces/[id]/environment', () => {
4747
vi.clearAllMocks()
4848
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
4949
permissionsMockFns.mockGetWorkspaceById.mockResolvedValue({ id: WORKSPACE_ID })
50+
permissionsMockFns.mockHasWorkspaceAdminAccess.mockResolvedValue(false)
5051
mockGetPersonalAndWorkspaceEnv.mockResolvedValue(ENV_RESULT)
5152
})
5253

5354
it('returns decrypted workspace values to workspace admins', async () => {
5455
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin')
56+
permissionsMockFns.mockHasWorkspaceAdminAccess.mockResolvedValue(true)
5557

5658
const res = await GET(createRequest(), createContext())
5759
expect(res.status).toBe(200)
@@ -62,9 +64,10 @@ describe('GET /api/workspaces/[id]/environment', () => {
6264
})
6365

6466
it.each(['write', 'read'] as const)(
65-
'returns only variable names (empty values) to %s members',
67+
'returns only variable names (empty values) to non-admin %s members',
6668
async (permission) => {
6769
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue(permission)
70+
permissionsMockFns.mockHasWorkspaceAdminAccess.mockResolvedValue(false)
6871

6972
const res = await GET(createRequest(), createContext())
7073
expect(res.status).toBe(200)
@@ -77,6 +80,17 @@ describe('GET /api/workspaces/[id]/environment', () => {
7780
}
7881
)
7982

83+
it('returns decrypted values to an admin-access user whose permission row is not "admin" (owner/org-admin)', async () => {
84+
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write')
85+
permissionsMockFns.mockHasWorkspaceAdminAccess.mockResolvedValue(true)
86+
87+
const res = await GET(createRequest(), createContext())
88+
expect(res.status).toBe(200)
89+
90+
const body = await res.json()
91+
expect(body.data.workspace).toEqual(ENV_RESULT.workspaceDecrypted)
92+
})
93+
8094
it('rejects users without any workspace permission', async () => {
8195
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue(null)
8296

apps/sim/app/api/workspaces/[id]/environment/route.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {
2323
getPersonalAndWorkspaceEnv,
2424
invalidateEffectiveDecryptedEnvCache,
2525
} from '@/lib/environment/utils'
26-
import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
26+
import {
27+
getUserEntityPermissions,
28+
getWorkspaceById,
29+
hasWorkspaceAdminAccess,
30+
} from '@/lib/workspaces/permissions/utils'
2731

2832
const logger = createLogger('WorkspaceEnvironmentAPI')
2933

@@ -53,14 +57,16 @@ export const GET = withRouteHandler(
5357
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
5458
}
5559

56-
const { workspaceEncrypted, workspaceDecrypted, personalDecrypted, conflicts } =
57-
await getPersonalAndWorkspaceEnv(userId, workspaceId)
60+
const [isAdmin, { workspaceEncrypted, workspaceDecrypted, personalDecrypted, conflicts }] =
61+
await Promise.all([
62+
hasWorkspaceAdminAccess(userId, workspaceId),
63+
getPersonalAndWorkspaceEnv(userId, workspaceId),
64+
])
5865

5966
// Only workspace admins may read plaintext secrets; others get variable names with empty values.
60-
const workspace =
61-
permission === 'admin'
62-
? workspaceDecrypted
63-
: Object.fromEntries(Object.keys(workspaceEncrypted).map((key) => [key, '']))
67+
const workspace = isAdmin
68+
? workspaceDecrypted
69+
: Object.fromEntries(Object.keys(workspaceEncrypted).map((key) => [key, '']))
6470

6571
return NextResponse.json(
6672
{

0 commit comments

Comments
 (0)