@@ -15,6 +15,8 @@ const mocks = vi.hoisted(() => ({
1515 deleteConnector : vi . fn ( ) ,
1616 syncConnector : vi . fn ( ) ,
1717 resolveBilling : vi . fn ( ) ,
18+ getCredentialActorContext : vi . fn ( ) ,
19+ canUseCredential : vi . fn ( ) ,
1820 resolveTokenIdentity : vi . fn ( ) ,
1921 refreshToken : vi . fn ( ) ,
2022 validateConnectorConfig : vi . fn ( ) ,
@@ -56,6 +58,8 @@ vi.mock('@/lib/knowledge/orchestration/connectors', () => ({
5658} ) )
5759
5860vi . mock ( '@/lib/credentials/access' , ( ) => ( {
61+ getCredentialActorContext : mocks . getCredentialActorContext ,
62+ canUseCredential : mocks . canUseCredential ,
5963 resolveCredentialTokenIdentity : mocks . resolveTokenIdentity ,
6064} ) )
6165
@@ -120,6 +124,17 @@ describe('knowledge connector application use cases', () => {
120124 mocks . resolvePermission . mockResolvedValue ( 'write' )
121125 mocks . resolveKnowledgeBase . mockResolvedValue ( crossWorkspaceContext )
122126 mocks . resolveConnector . mockResolvedValue ( connectorContext )
127+ mocks . getCredentialActorContext . mockResolvedValue ( {
128+ credential : { id : 'credential-1' , workspaceId : 'workspace-a' } ,
129+ member : { role : 'member' } ,
130+ hasWorkspaceAccess : true ,
131+ canWriteWorkspace : true ,
132+ isAdmin : false ,
133+ } )
134+ mocks . canUseCredential . mockImplementation (
135+ ( access : { hasWorkspaceAccess : boolean ; member : unknown ; isAdmin : boolean } ) =>
136+ access . hasWorkspaceAccess && ( Boolean ( access . member ) || access . isAdmin )
137+ )
123138 mocks . resolveTokenIdentity . mockResolvedValue ( { kind : 'oauth' , userId : 'credential-owner' } )
124139 mocks . refreshToken . mockResolvedValue ( 'access-token' )
125140 mocks . validateConnectorConfig . mockResolvedValue ( { valid : true } )
@@ -296,6 +311,7 @@ describe('knowledge connector application use cases', () => {
296311 expect ( mocks . resolvePermission . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
297312 mocks . updateConnector . mock . invocationCallOrder [ 0 ]
298313 )
314+ expect ( mocks . getCredentialActorContext ) . toHaveBeenCalledWith ( 'credential-1' , 'shared-user' )
299315 expect ( mocks . resolveTokenIdentity ) . toHaveBeenCalledWith ( 'credential-1' , 'workspace-a' )
300316 expect ( mocks . refreshToken ) . toHaveBeenCalledWith (
301317 'credential-1' ,
@@ -305,6 +321,122 @@ describe('knowledge connector application use cases', () => {
305321 expect ( mocks . validateConnectorConfig ) . toHaveBeenCalledWith ( 'access-token' , { space : 'ENG' } )
306322 } )
307323
324+ it ( 'rejects connector creation when the writer cannot use the workspace credential' , async ( ) => {
325+ const sameWorkspaceContext = {
326+ ...connectorContext ,
327+ workspaceId : 'workspace-a' ,
328+ knowledgeBaseId : 'knowledge-a' ,
329+ knowledgeBase : { id : 'knowledge-a' , name : 'Workspace A docs' } ,
330+ connector : { ...connectorContext . connector , knowledgeBaseId : 'knowledge-a' } ,
331+ }
332+ mocks . resolveKnowledgeBase . mockResolvedValueOnce ( sameWorkspaceContext )
333+ mocks . getCredentialActorContext . mockResolvedValueOnce ( {
334+ credential : { id : 'credential-1' , workspaceId : 'workspace-a' } ,
335+ member : null ,
336+ hasWorkspaceAccess : true ,
337+ canWriteWorkspace : true ,
338+ isAdmin : false ,
339+ } )
340+ mocks . createConnector . mockImplementationOnce (
341+ async ( input : { resolveAccessToken : ( credentialId : string ) => Promise < string | null > } ) => {
342+ const accessToken = await input . resolveAccessToken ( 'credential-1' )
343+ return accessToken
344+ ? { success : true , connector : sameWorkspaceContext . connector }
345+ : {
346+ success : false ,
347+ error : 'Credential has no access token. Please reconnect your account.' ,
348+ errorCode : 'validation' ,
349+ }
350+ }
351+ )
352+
353+ await expect (
354+ createKnowledgeConnector . execute ( {
355+ principal : delegatedPrincipal ,
356+ input : {
357+ knowledgeBaseId : 'knowledge-a' ,
358+ assertedWorkspaceId : 'workspace-a' ,
359+ connectorType : 'confluence' ,
360+ credentialId : 'credential-1' ,
361+ sourceConfig : { } ,
362+ syncIntervalMinutes : 1440 ,
363+ resolveBillingAttribution : mocks . resolveBilling ,
364+ } ,
365+ } )
366+ ) . rejects . toMatchObject ( {
367+ code : 'validation' ,
368+ message :
369+ 'Credential is not available to you in this workspace. Ask a credential administrator to grant access or select another credential.' ,
370+ } )
371+
372+ expect ( mocks . getCredentialActorContext ) . toHaveBeenCalledWith ( 'credential-1' , 'shared-user' )
373+ expect ( mocks . resolveTokenIdentity ) . not . toHaveBeenCalled ( )
374+ expect ( mocks . refreshToken ) . not . toHaveBeenCalled ( )
375+ } )
376+
377+ it ( 'rejects source-config revalidation after credential membership is removed' , async ( ) => {
378+ const sameWorkspaceContext = {
379+ ...connectorContext ,
380+ workspaceId : 'workspace-a' ,
381+ knowledgeBaseId : 'knowledge-a' ,
382+ knowledgeBase : { id : 'knowledge-a' , name : 'Workspace A docs' } ,
383+ connector : { ...connectorContext . connector , knowledgeBaseId : 'knowledge-a' } ,
384+ }
385+ mocks . resolveConnector . mockResolvedValueOnce ( sameWorkspaceContext )
386+ mocks . updateConnector . mockResolvedValueOnce ( {
387+ success : true ,
388+ connector : { ...sameWorkspaceContext . connector , sourceConfig : { space : 'ENG' } } ,
389+ } )
390+
391+ await updateKnowledgeConnector . execute ( {
392+ principal : delegatedPrincipal ,
393+ input : {
394+ connectorId : 'connector-b' ,
395+ assertedWorkspaceId : 'workspace-a' ,
396+ updates : { sourceConfig : { space : 'ENG' } } ,
397+ } ,
398+ } )
399+
400+ const orchestrationInput = mocks . updateConnector . mock . calls [ 0 ] ?. [ 0 ] as {
401+ validateSourceConfig ?: (
402+ connector : {
403+ connectorType : string
404+ credentialId : string
405+ encryptedApiKey : null
406+ } ,
407+ sourceConfig : Record < string , unknown >
408+ ) => Promise < unknown >
409+ }
410+ if ( ! orchestrationInput . validateSourceConfig ) {
411+ throw new Error ( 'Application command did not provide source-config validation' )
412+ }
413+ mocks . getCredentialActorContext . mockResolvedValueOnce ( {
414+ credential : { id : 'credential-1' , workspaceId : 'workspace-a' } ,
415+ member : null ,
416+ hasWorkspaceAccess : true ,
417+ canWriteWorkspace : true ,
418+ isAdmin : false ,
419+ } )
420+
421+ await expect (
422+ orchestrationInput . validateSourceConfig (
423+ {
424+ connectorType : 'confluence' ,
425+ credentialId : 'credential-1' ,
426+ encryptedApiKey : null ,
427+ } ,
428+ { space : 'ENG' }
429+ )
430+ ) . rejects . toMatchObject ( {
431+ code : 'validation' ,
432+ message :
433+ 'Credential is not available to you in this workspace. Ask a credential administrator to grant access or select another credential.' ,
434+ } )
435+ expect ( mocks . resolveTokenIdentity ) . not . toHaveBeenCalled ( )
436+ expect ( mocks . refreshToken ) . not . toHaveBeenCalled ( )
437+ expect ( mocks . validateConnectorConfig ) . not . toHaveBeenCalled ( )
438+ } )
439+
308440 it . each ( [
309441 [
310442 'create' ,
0 commit comments