Skip to content

Commit 7abe86b

Browse files
authored
test(credentials): cover Chat's OAuth-only, admin-demotion and integrations guards on credential sharing (#8844)
* test(credentials): cover Chat's OAuth-only, admin-demotion and integrations guards on credential sharing * test(credentials): demote and remove against an existing admin grant
1 parent f7ef6ed commit 7abe86b

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,13 +8,20 @@ import {
88
credentialMember,
99
member,
1010
organization,
11+
permissionGroup,
12+
permissionGroupMember,
13+
permissionGroupWorkspace,
1114
permissions,
1215
user,
1316
workspace,
1417
} from '@sim/db/schema'
18+
import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock'
1519
import { generateId } from '@sim/utils/id'
1620
import { and, eq, inArray } from 'drizzle-orm'
1721
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
22+
23+
vi.mock('@/lib/core/config/env-flags', () => ({ ...envFlagsMock, isAccessControlEnabled: true }))
24+
1825
import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
1926
import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
2027
import '@/app/api/v2/credentials/[credentialId]/members/route'
@@ -28,18 +35,21 @@ const adminId = generateId()
2835
const teammateId = generateId()
2936
const writerId = generateId()
3037
const outsiderId = generateId()
38+
const coAdminId = generateId()
39+
const restrictedAdminId = generateId()
3140
const credentialId = generateId()
32-
const userIds = [adminId, teammateId, writerId, outsiderId]
41+
const serviceAccountCredentialId = generateId()
42+
const userIds = [adminId, teammateId, writerId, outsiderId, coAdminId, restrictedAdminId]
3343

34-
function chat(userId: string, chatWorkspaceId = workspaceId) {
44+
function chat(userId: string, chatWorkspaceId = workspaceId, targetCredentialId = credentialId) {
3545
const transport = createScopedCliTransport(ORIGIN, {
3646
userId,
3747
workspaceId: chatWorkspaceId,
3848
chatId: generateId(),
3949
})
4050
return (path: string, init?: { method: string; body?: unknown }) =>
4151
withWorkspaceInvocationScope({ workspaceId: chatWorkspaceId, organizationId }, () =>
42-
transport(`${ORIGIN}/api/v2/credentials/${credentialId}/members${path}`, {
52+
transport(`${ORIGIN}/api/v2/credentials/${targetCredentialId}/members${path}`, {
4353
method: init?.method ?? 'GET',
4454
...(init?.body === undefined
4555
? {}
@@ -110,6 +120,8 @@ describe('chat-delegated credential sharing', () => {
110120
[
111121
[adminId, workspaceId, 'admin'],
112122
[adminId, otherWorkspaceId, 'admin'],
123+
[coAdminId, workspaceId, 'admin'],
124+
[restrictedAdminId, workspaceId, 'admin'],
113125
[teammateId, workspaceId, 'write'],
114126
[writerId, workspaceId, 'write'],
115127
[outsiderId, otherWorkspaceId, 'write'],
@@ -141,6 +153,34 @@ describe('chat-delegated credential sharing', () => {
141153
displayName: 'Slack fixture',
142154
createdBy: adminId,
143155
})
156+
await db.insert(credential).values({
157+
id: serviceAccountCredentialId,
158+
type: 'service_account',
159+
workspaceId,
160+
providerId: 'slack',
161+
displayName: 'Service account fixture',
162+
createdBy: adminId,
163+
})
164+
const groupId = generateId()
165+
await db.insert(permissionGroup).values({
166+
id: groupId,
167+
organizationId,
168+
name: 'No integrations',
169+
createdBy: adminId,
170+
config: { hideIntegrationsTab: true },
171+
})
172+
await db.insert(permissionGroupWorkspace).values({
173+
id: generateId(),
174+
permissionGroupId: groupId,
175+
workspaceId,
176+
organizationId,
177+
})
178+
await db.insert(permissionGroupMember).values({
179+
id: generateId(),
180+
permissionGroupId: groupId,
181+
organizationId,
182+
userId: restrictedAdminId,
183+
})
144184
})
145185

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

0 commit comments

Comments
 (0)