Skip to content

Commit d1c7457

Browse files
committed
Merge branch 'feat/project-workspace-column-expand' into codex/project-entity-enforcement
2 parents 50361dc + 1d88662 commit d1c7457

1 file changed

Lines changed: 109 additions & 3 deletions

File tree

‎apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts‎

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ import {
88
credentialMember,
99
member,
1010
organization,
11+
permissionGroup,
12+
permissionGroupMember,
13+
permissionGroupWorkspace,
1114
permissions,
1215
user,
1316
workspace,
1417
} from '@sim/db/schema'
1518
import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
19+
import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock'
1620
import { generateId } from '@sim/utils/id'
1721
import { and, eq, inArray } from 'drizzle-orm'
1822
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
23+
24+
vi.mock('@/lib/core/config/env-flags', () => ({ ...envFlagsMock, isAccessControlEnabled: true }))
25+
1926
import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
2027
import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
2128
import '@/app/api/v2/credentials/[credentialId]/members/route'
@@ -29,18 +36,21 @@ const adminId = generateId()
2936
const teammateId = generateId()
3037
const writerId = generateId()
3138
const outsiderId = generateId()
39+
const coAdminId = generateId()
40+
const restrictedAdminId = generateId()
3241
const credentialId = generateId()
33-
const userIds = [adminId, teammateId, writerId, outsiderId]
42+
const serviceAccountCredentialId = generateId()
43+
const userIds = [adminId, teammateId, writerId, outsiderId, coAdminId, restrictedAdminId]
3444

35-
function chat(userId: string, chatWorkspaceId = workspaceId) {
45+
function chat(userId: string, chatWorkspaceId = workspaceId, targetCredentialId = credentialId) {
3646
const transport = createScopedCliTransport(ORIGIN, {
3747
userId,
3848
workspaceId: chatWorkspaceId,
3949
chatId: generateId(),
4050
})
4151
return (path: string, init?: { method: string; body?: unknown }) =>
4252
withWorkspaceInvocationScope({ workspaceId: chatWorkspaceId, organizationId }, () =>
43-
transport(`${ORIGIN}/api/v2/credentials/${credentialId}/members${path}`, {
53+
transport(`${ORIGIN}/api/v2/credentials/${targetCredentialId}/members${path}`, {
4454
method: init?.method ?? 'GET',
4555
...(init?.body === undefined
4656
? {}
@@ -112,6 +122,8 @@ describe('chat-delegated credential sharing', () => {
112122
[
113123
[adminId, workspaceId, 'admin'],
114124
[adminId, otherWorkspaceId, 'admin'],
125+
[coAdminId, workspaceId, 'admin'],
126+
[restrictedAdminId, workspaceId, 'admin'],
115127
[teammateId, workspaceId, 'write'],
116128
[writerId, workspaceId, 'write'],
117129
[outsiderId, otherWorkspaceId, 'write'],
@@ -143,6 +155,34 @@ describe('chat-delegated credential sharing', () => {
143155
displayName: 'Slack fixture',
144156
createdBy: adminId,
145157
})
158+
await db.insert(credential).values({
159+
id: serviceAccountCredentialId,
160+
type: 'service_account',
161+
workspaceId,
162+
providerId: 'slack',
163+
displayName: 'Service account fixture',
164+
createdBy: adminId,
165+
})
166+
const groupId = generateId()
167+
await db.insert(permissionGroup).values({
168+
id: groupId,
169+
organizationId,
170+
name: 'No integrations',
171+
createdBy: adminId,
172+
config: { hideIntegrationsTab: true },
173+
})
174+
await db.insert(permissionGroupWorkspace).values({
175+
id: generateId(),
176+
permissionGroupId: groupId,
177+
workspaceId,
178+
organizationId,
179+
})
180+
await db.insert(permissionGroupMember).values({
181+
id: generateId(),
182+
permissionGroupId: groupId,
183+
organizationId,
184+
userId: restrictedAdminId,
185+
})
146186
})
147187

148188
afterAll(async () => {
@@ -229,4 +269,70 @@ describe('chat-delegated credential sharing', () => {
229269
})
230270
expect(await activeGrantsFor(teammateId)).toEqual([])
231271
})
272+
273+
it('confines Chat to OAuth credentials even for a workspace admin', async () => {
274+
const asAdmin = chat(adminId, workspaceId, serviceAccountCredentialId)
275+
const refusal = { error: { message: 'Only oauth credentials can be managed by this caller' } }
276+
277+
const listed = await asAdmin(query)
278+
expect(listed.status).toBe(400)
279+
expect(await listed.json()).toMatchObject(refusal)
280+
281+
const shared = await asAdmin(query, {
282+
method: 'POST',
283+
body: { userId: teammateId, role: 'member' },
284+
})
285+
expect(shared.status).toBe(400)
286+
expect(await shared.json()).toMatchObject(refusal)
287+
})
288+
289+
it("refuses demoting or removing a workspace admin's existing grant", async () => {
290+
const asAdmin = chat(adminId)
291+
const granted = await asAdmin(query, {
292+
method: 'POST',
293+
body: { userId: coAdminId, role: 'admin' },
294+
})
295+
expect(granted.status).toBe(201)
296+
expect(await activeGrantsFor(coAdminId)).toEqual([{ role: 'admin' }])
297+
298+
const demoted = await asAdmin(query, {
299+
method: 'POST',
300+
body: { userId: coAdminId, role: 'member' },
301+
})
302+
expect(demoted.status).toBe(400)
303+
expect(await demoted.json()).toMatchObject({
304+
error: {
305+
message: 'Workspace admins are automatically credential admins and cannot be demoted',
306+
},
307+
})
308+
expect(await activeGrantsFor(coAdminId)).toEqual([{ role: 'admin' }])
309+
310+
const removed = await asAdmin(`/${coAdminId}${query}`, { method: 'DELETE' })
311+
expect(removed.status).toBe(400)
312+
expect(await removed.json()).toMatchObject({
313+
error: {
314+
message: 'Workspace admins are automatically credential admins and cannot be removed',
315+
},
316+
})
317+
expect(await activeGrantsFor(coAdminId)).toEqual([{ role: 'admin' }])
318+
})
319+
320+
it('refuses an admin whose permission group withholds integration management', async () => {
321+
const asRestricted = chat(restrictedAdminId)
322+
const blocked = {
323+
error: { code: 'FORBIDDEN', details: { code: 'PERMISSION_GROUP_CAPABILITY_BLOCKED' } },
324+
}
325+
326+
const listed = await asRestricted(query)
327+
expect(listed.status).toBe(403)
328+
expect(await listed.json()).toMatchObject(blocked)
329+
330+
const shared = await asRestricted(query, {
331+
method: 'POST',
332+
body: { userId: teammateId, role: 'member' },
333+
})
334+
expect(shared.status).toBe(403)
335+
expect(await shared.json()).toMatchObject(blocked)
336+
expect(await activeGrantsFor(teammateId)).toEqual([])
337+
})
232338
})

0 commit comments

Comments
 (0)