@@ -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'
1519import { generateId } from '@sim/utils/id'
1620import { and , eq , inArray } from 'drizzle-orm'
1721import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest'
22+
23+ vi . mock ( '@/lib/core/config/env-flags' , ( ) => ( { ...envFlagsMock , isAccessControlEnabled : true } ) )
24+
1825import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
1926import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
2027import '@/app/api/v2/credentials/[credentialId]/members/route'
@@ -28,18 +35,21 @@ const adminId = generateId()
2835const teammateId = generateId ( )
2936const writerId = generateId ( )
3037const outsiderId = generateId ( )
38+ const coAdminId = generateId ( )
39+ const restrictedAdminId = generateId ( )
3140const 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