@@ -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'
1518import { deleteWorkspaceFixture , insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
19+ import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock'
1620import { generateId } from '@sim/utils/id'
1721import { and , eq , inArray } from 'drizzle-orm'
1822import { afterAll , beforeAll , describe , expect , it , vi } from 'vitest'
23+
24+ vi . mock ( '@/lib/core/config/env-flags' , ( ) => ( { ...envFlagsMock , isAccessControlEnabled : true } ) )
25+
1926import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
2027import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
2128import '@/app/api/v2/credentials/[credentialId]/members/route'
@@ -29,18 +36,21 @@ const adminId = generateId()
2936const teammateId = generateId ( )
3037const writerId = generateId ( )
3138const outsiderId = generateId ( )
39+ const coAdminId = generateId ( )
40+ const restrictedAdminId = generateId ( )
3241const 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