2024-01-16 05:15:29 -08:00
import { Service } from 'typedi' ;
2023-01-27 05:56:56 -08:00
import type { INode , Workflow } from 'n8n-workflow' ;
2024-02-21 04:04:30 -08:00
import { CredentialAccessError , NodeOperationError , WorkflowOperationError } from 'n8n-workflow' ;
2024-01-16 05:15:29 -08:00
2022-11-22 05:24:29 -08:00
import config from '@/config' ;
2024-01-23 04:58:31 -08:00
import { License } from '@/License' ;
2023-07-31 02:37:09 -07:00
import { OwnershipService } from '@/services/ownership.service' ;
2023-11-10 06:04:26 -08:00
import { UserRepository } from '@db/repositories/user.repository' ;
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository' ;
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository' ;
2022-11-11 02:14:45 -08:00
2024-01-16 05:15:29 -08:00
@Service ( )
2022-11-11 02:14:45 -08:00
export class PermissionChecker {
2024-01-16 05:15:29 -08:00
constructor (
private readonly userRepository : UserRepository ,
private readonly sharedCredentialsRepository : SharedCredentialsRepository ,
private readonly sharedWorkflowRepository : SharedWorkflowRepository ,
private readonly ownershipService : OwnershipService ,
2024-01-23 04:58:31 -08:00
private readonly license : License ,
2024-01-16 05:15:29 -08:00
) { }
2022-11-11 02:14:45 -08:00
/ * *
* Check if a user is permitted to execute a workflow .
* /
2024-02-21 05:47:02 -08:00
async check ( workflowId : string , userId : string , nodes : INode [ ] ) {
2022-11-11 02:14:45 -08:00
// allow if no nodes in this workflow use creds
2024-02-21 05:47:02 -08:00
const credIdsToNodes = this . mapCredIdsToNodes ( nodes ) ;
2022-11-11 02:14:45 -08:00
const workflowCredIds = Object . keys ( credIdsToNodes ) ;
if ( workflowCredIds . length === 0 ) return ;
// allow if requesting user is instance owner
2024-01-16 05:15:29 -08:00
const user = await this . userRepository . findOneOrFail ( {
2023-01-13 09:12:22 -08:00
where : { id : userId } ,
2022-11-11 02:14:45 -08:00
} ) ;
2023-12-19 04:52:42 -08:00
if ( user . hasGlobalScope ( 'workflow:execute' ) ) return ;
2022-11-11 02:14:45 -08:00
2024-02-02 03:21:53 -08:00
const isSharingEnabled = this . license . isSharingEnabled ( ) ;
2022-11-11 02:14:45 -08:00
// allow if all creds used in this workflow are a subset of
// all creds accessible to users who have access to this workflow
2022-11-22 05:24:29 -08:00
let workflowUserIds = [ userId ] ;
2022-11-21 23:37:52 -08:00
2024-02-21 05:47:02 -08:00
if ( workflowId && isSharingEnabled ) {
workflowUserIds = await this . sharedWorkflowRepository . getSharedUserIds ( workflowId ) ;
2022-11-21 23:37:52 -08:00
}
2022-11-11 02:14:45 -08:00
2024-02-02 03:21:53 -08:00
const accessibleCredIds = isSharingEnabled
? await this . sharedCredentialsRepository . getAccessibleCredentialIds ( workflowUserIds )
: await this . sharedCredentialsRepository . getOwnedCredentialIds ( workflowUserIds ) ;
2022-11-11 02:14:45 -08:00
const inaccessibleCredIds = workflowCredIds . filter ( ( id ) = > ! accessibleCredIds . includes ( id ) ) ;
if ( inaccessibleCredIds . length === 0 ) return ;
// if disallowed, flag only first node using first inaccessible cred
2024-02-21 04:04:30 -08:00
const inaccessibleCredId = inaccessibleCredIds [ 0 ] ;
const nodeToFlag = credIdsToNodes [ inaccessibleCredId ] [ 0 ] ;
2022-11-11 02:14:45 -08:00
2024-02-21 05:47:02 -08:00
throw new CredentialAccessError ( nodeToFlag , inaccessibleCredId , workflowId ) ;
2022-11-11 02:14:45 -08:00
}
2024-01-16 05:15:29 -08:00
async checkSubworkflowExecutePolicy (
2022-12-21 07:42:07 -08:00
subworkflow : Workflow ,
2023-12-06 04:27:11 -08:00
parentWorkflowId : string ,
node? : INode ,
2022-12-21 07:42:07 -08:00
) {
/ * *
* Important considerations : both the current workflow and the parent can have empty IDs .
* This happens when a user is executing an unsaved workflow manually running a workflow
* loaded from a file or code , for instance .
* This is an important topic to keep in mind for all security checks
* /
if ( ! subworkflow . id ) {
// It's a workflow from code and not loaded from DB
// No checks are necessary since it doesn't have any sort of settings
return ;
}
let policy =
subworkflow . settings ? . callerPolicy ? ? config . getEnv ( 'workflows.callerPolicyDefaultOption' ) ;
2024-01-23 04:58:31 -08:00
if ( ! this . license . isSharingEnabled ( ) ) {
2022-12-21 07:42:07 -08:00
// Community version allows only same owner workflows
policy = 'workflowsFromSameOwner' ;
}
2023-12-06 04:27:11 -08:00
const parentWorkflowOwner =
2024-01-16 05:15:29 -08:00
await this . ownershipService . getWorkflowOwnerCached ( parentWorkflowId ) ;
2023-12-06 04:27:11 -08:00
2024-01-16 05:15:29 -08:00
const subworkflowOwner = await this . ownershipService . getWorkflowOwnerCached ( subworkflow . id ) ;
2022-12-21 07:42:07 -08:00
2023-12-06 04:27:11 -08:00
const description =
subworkflowOwner . id === parentWorkflowOwner . id
2022-12-21 07:42:07 -08:00
? 'Change the settings of the sub-workflow so it can be called by this one.'
2023-12-06 04:27:11 -08:00
: ` ${ subworkflowOwner . firstName } ( ${ subworkflowOwner . email } ) can make this change. You may need to tell them the ID of the sub-workflow, which is ${ subworkflow . id } ` ;
const errorToThrow = new WorkflowOperationError (
` Target workflow ID ${ subworkflow . id } may not be called ` ,
node ,
description ,
2022-12-21 07:42:07 -08:00
) ;
if ( policy === 'none' ) {
throw errorToThrow ;
}
if ( policy === 'workflowsFromAList' ) {
if ( parentWorkflowId === undefined ) {
throw errorToThrow ;
}
2023-03-24 05:11:48 -07:00
const allowedCallerIds = subworkflow . settings . callerIds
2022-12-21 07:42:07 -08:00
? . split ( ',' )
. map ( ( id ) = > id . trim ( ) )
. filter ( ( id ) = > id !== '' ) ;
if ( ! allowedCallerIds ? . includes ( parentWorkflowId ) ) {
throw errorToThrow ;
}
}
2023-12-06 04:27:11 -08:00
if ( policy === 'workflowsFromSameOwner' && subworkflowOwner ? . id !== parentWorkflowOwner . id ) {
throw errorToThrow ;
2022-12-21 07:42:07 -08:00
}
}
2024-02-21 05:47:02 -08:00
private mapCredIdsToNodes ( nodes : INode [ ] ) {
return nodes . reduce < { [ credentialId : string ] : INode [ ] } > ( ( map , node ) = > {
if ( node . disabled || ! node . credentials ) return map ;
Object . values ( node . credentials ) . forEach ( ( cred ) = > {
if ( ! cred . id ) {
throw new NodeOperationError ( node , 'Node uses invalid credential' , {
description : 'Please recreate the credential.' ,
level : 'warning' ,
} ) ;
}
map [ cred . id ] = map [ cred . id ] ? [ . . . map [ cred . id ] , node ] : [ node ] ;
} ) ;
return map ;
} , { } ) ;
2022-11-11 02:14:45 -08:00
}
}