2024-02-02 03:21:53 -08:00
import { v4 as uuid } from 'uuid' ;
import { Container } from 'typedi' ;
2024-02-21 05:47:02 -08:00
import type { INode , WorkflowSettings } from 'n8n-workflow' ;
2024-06-19 04:33:57 -07:00
import { SubworkflowOperationError , Workflow , randomInt } from 'n8n-workflow' ;
2024-02-02 03:21:53 -08:00
import config from '@/config' ;
2024-05-17 01:53:15 -07:00
import type { User } from '@db/entities/User' ;
2024-02-02 03:21:53 -08:00
import { WorkflowRepository } from '@db/repositories/workflow.repository' ;
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository' ;
import { generateNanoId } from '@/databases/utils/generators' ;
import { License } from '@/License' ;
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials' ;
import { NodeTypes } from '@/NodeTypes' ;
import { OwnershipService } from '@/services/ownership.service' ;
import { PermissionChecker } from '@/UserManagement/PermissionChecker' ;
import { mockInstance } from '../shared/mocking' ;
2024-06-19 04:33:57 -07:00
import { randomCredentialPayload as randomCred , randomName } from '../integration/shared/random' ;
2024-02-02 03:21:53 -08:00
import { LicenseMocker } from '../integration/shared/license' ;
import * as testDb from '../integration/shared/testDb' ;
import type { SaveCredentialFunction } from '../integration/shared/types' ;
import { mockNodeTypesData } from '../unit/Helpers' ;
import { affixRoleToSaveCredential } from '../integration/shared/db/credentials' ;
import { createOwner , createUser } from '../integration/shared/db/users' ;
2024-03-05 10:18:34 -08:00
import { SharedCredentialsRepository } from '@/databases/repositories/sharedCredentials.repository' ;
2024-05-17 01:53:15 -07:00
import { getPersonalProject } from './shared/db/projects' ;
2024-03-05 10:18:34 -08:00
import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity' ;
2024-05-17 01:53:15 -07:00
import { Project } from '@/databases/entities/Project' ;
import { ProjectRepository } from '@/databases/repositories/project.repository' ;
2024-02-02 03:21:53 -08:00
export const toTargetCallErrorMsg = ( subworkflowId : string ) = >
` Target workflow ID ${ subworkflowId } may not be called ` ;
export function createParentWorkflow() {
return Container . get ( WorkflowRepository ) . create ( {
id : generateNanoId ( ) ,
name : randomName ( ) ,
active : false ,
connections : { } ,
nodes : [
{
name : '' ,
typeVersion : 1 ,
type : 'n8n-nodes-base.executeWorkflow' ,
position : [ 0 , 0 ] ,
parameters : { } ,
} ,
] ,
} ) ;
}
export function createSubworkflow ( {
policy ,
callerIds ,
} : {
policy? : WorkflowSettings.CallerPolicy ;
callerIds? : string ;
} = { } ) {
return new Workflow ( {
id : uuid ( ) ,
nodes : [ ] ,
connections : { } ,
active : false ,
nodeTypes : mockNodeTypes ,
settings : {
. . . ( policy ? { callerPolicy : policy } : { } ) ,
. . . ( callerIds ? { callerIds } : { } ) ,
} ,
} ) ;
}
2024-05-17 01:53:15 -07:00
const ownershipService = mockInstance ( OwnershipService ) ;
2024-03-05 10:18:34 -08:00
const createWorkflow = async ( nodes : INode [ ] , workflowOwner? : User ) : Promise < WorkflowEntity > = > {
const workflowDetails = {
2024-06-19 04:33:57 -07:00
id : randomInt ( 1 , 10 ) . toString ( ) ,
2024-03-05 10:18:34 -08:00
name : 'test' ,
active : false ,
connections : { } ,
nodeTypes : mockNodeTypes ,
nodes ,
} ;
const workflowEntity = await Container . get ( WorkflowRepository ) . save ( workflowDetails ) ;
if ( workflowOwner ) {
2024-05-17 01:53:15 -07:00
const project = await getPersonalProject ( workflowOwner ) ;
2024-03-05 10:18:34 -08:00
await Container . get ( SharedWorkflowRepository ) . save ( {
workflow : workflowEntity ,
user : workflowOwner ,
2024-05-17 01:53:15 -07:00
project ,
2024-03-05 10:18:34 -08:00
role : 'workflow:owner' ,
} ) ;
}
return workflowEntity ;
} ;
2024-02-02 03:21:53 -08:00
let saveCredential : SaveCredentialFunction ;
2024-03-05 10:18:34 -08:00
let owner : User ;
let member : User ;
2024-05-17 01:53:15 -07:00
let ownerPersonalProject : Project ;
let memberPersonalProject : Project ;
2024-03-05 10:18:34 -08:00
2024-02-02 03:21:53 -08:00
const mockNodeTypes = mockInstance ( NodeTypes ) ;
mockInstance ( LoadNodesAndCredentials , {
loadedNodes : mockNodeTypesData ( [ 'start' , 'actionNetwork' ] ) ,
} ) ;
let permissionChecker : PermissionChecker ;
beforeAll ( async ( ) = > {
await testDb . init ( ) ;
saveCredential = affixRoleToSaveCredential ( 'credential:owner' ) ;
permissionChecker = Container . get ( PermissionChecker ) ;
2024-03-05 10:18:34 -08:00
[ owner , member ] = await Promise . all ( [ createOwner ( ) , createUser ( ) ] ) ;
2024-05-17 01:53:15 -07:00
ownerPersonalProject = await Container . get ( ProjectRepository ) . getPersonalProjectForUserOrFail (
owner . id ,
) ;
memberPersonalProject = await Container . get ( ProjectRepository ) . getPersonalProjectForUserOrFail (
member . id ,
) ;
2024-03-05 10:18:34 -08:00
} ) ;
2024-02-21 05:47:02 -08:00
2024-03-05 10:18:34 -08:00
describe ( 'check()' , ( ) = > {
2024-02-02 03:21:53 -08:00
beforeEach ( async ( ) = > {
await testDb . truncate ( [ 'Workflow' , 'Credentials' ] ) ;
} ) ;
afterAll ( async ( ) = > {
await testDb . terminate ( ) ;
} ) ;
test ( 'should allow if workflow has no creds' , async ( ) = > {
2024-02-21 05:47:02 -08:00
const nodes : INode [ ] = [
{
id : uuid ( ) ,
name : 'Start' ,
type : 'n8n-nodes-base.start' ,
typeVersion : 1 ,
parameters : { } ,
position : [ 0 , 0 ] ,
} ,
] ;
2024-02-02 03:21:53 -08:00
2024-03-05 10:18:34 -08:00
const workflow = await createWorkflow ( nodes , member ) ;
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( memberPersonalProject ) ;
2024-03-05 10:18:34 -08:00
2024-05-17 01:53:15 -07:00
await expect ( permissionChecker . check ( workflow . id , nodes ) ) . resolves . not . toThrow ( ) ;
2024-03-05 10:18:34 -08:00
} ) ;
2024-05-17 01:53:15 -07:00
test ( 'should allow if workflow creds are valid subset' , async ( ) = > {
2024-03-05 10:18:34 -08:00
const ownerCred = await saveCredential ( randomCred ( ) , { user : owner } ) ;
const memberCred = await saveCredential ( randomCred ( ) , { user : member } ) ;
await Container . get ( SharedCredentialsRepository ) . save (
Container . get ( SharedCredentialsRepository ) . create ( {
2024-05-17 01:53:15 -07:00
projectId : ( await getPersonalProject ( member ) ) . id ,
2024-03-05 10:18:34 -08:00
credentialsId : ownerCred.id ,
role : 'credential:user' ,
} ) ,
) ;
const nodes : INode [ ] = [
{
id : uuid ( ) ,
name : 'Action Network' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] ,
credentials : {
actionNetworkApi : {
id : ownerCred.id ,
name : ownerCred.name ,
} ,
} ,
} ,
{
id : uuid ( ) ,
name : 'Action Network 2' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] ,
credentials : {
actionNetworkApi : {
id : memberCred.id ,
name : memberCred.name ,
} ,
} ,
} ,
] ;
2024-05-17 01:53:15 -07:00
const workflowEntity = await createWorkflow ( nodes , member ) ;
2024-03-05 10:18:34 -08:00
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( memberPersonalProject ) ;
await expect ( permissionChecker . check ( workflowEntity . id , nodes ) ) . resolves . not . toThrow ( ) ;
2024-02-02 03:21:53 -08:00
} ) ;
2024-05-17 01:53:15 -07:00
test ( 'should deny if workflow creds are not valid subset' , async ( ) = > {
2024-03-05 10:18:34 -08:00
const memberCred = await saveCredential ( randomCred ( ) , { user : member } ) ;
2024-05-17 01:53:15 -07:00
const ownerCred = await saveCredential ( randomCred ( ) , { user : owner } ) ;
2024-03-05 10:18:34 -08:00
2024-05-17 01:53:15 -07:00
const nodes = [
2024-03-05 10:18:34 -08:00
{
id : uuid ( ) ,
name : 'Action Network' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
2024-05-17 01:53:15 -07:00
position : [ 0 , 0 ] as [ number , number ] ,
2024-03-05 10:18:34 -08:00
credentials : {
actionNetworkApi : {
id : memberCred.id ,
name : memberCred.name ,
} ,
} ,
} ,
2024-02-21 05:47:02 -08:00
{
id : uuid ( ) ,
2024-05-17 01:53:15 -07:00
name : 'Action Network 2' ,
2024-02-21 05:47:02 -08:00
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
2024-05-17 01:53:15 -07:00
position : [ 0 , 0 ] as [ number , number ] ,
2024-02-21 05:47:02 -08:00
credentials : {
actionNetworkApi : {
id : ownerCred.id ,
name : ownerCred.name ,
2024-02-02 03:21:53 -08:00
} ,
} ,
2024-02-21 05:47:02 -08:00
} ,
] ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
const workflowEntity = await createWorkflow ( nodes , member ) ;
2024-03-05 10:18:34 -08:00
2024-05-17 01:53:15 -07:00
await expect (
permissionChecker . check ( workflowEntity . id , workflowEntity . nodes ) ,
) . rejects . toThrow ( ) ;
2024-02-02 03:21:53 -08:00
} ) ;
2024-05-17 01:53:15 -07:00
test ( 'should allow all credentials if current user is instance owner' , async ( ) = > {
2024-02-02 03:21:53 -08:00
const memberCred = await saveCredential ( randomCred ( ) , { user : member } ) ;
2024-05-17 01:53:15 -07:00
const ownerCred = await saveCredential ( randomCred ( ) , { user : owner } ) ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
const nodes = [
2024-03-05 10:18:34 -08:00
{
id : uuid ( ) ,
name : 'Action Network' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] as [ number , number ] ,
credentials : {
actionNetworkApi : {
id : memberCred.id ,
name : memberCred.name ,
2024-02-02 03:21:53 -08:00
} ,
} ,
2024-03-05 10:18:34 -08:00
} ,
{
id : uuid ( ) ,
name : 'Action Network 2' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] as [ number , number ] ,
credentials : {
actionNetworkApi : {
2024-05-17 01:53:15 -07:00
id : ownerCred.id ,
name : ownerCred.name ,
2024-02-02 03:21:53 -08:00
} ,
} ,
2024-03-05 10:18:34 -08:00
} ,
] ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
const workflowEntity = await createWorkflow ( nodes , owner ) ;
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( ownerPersonalProject ) ;
ownershipService . getProjectOwnerCached . mockResolvedValueOnce ( owner ) ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
await expect (
permissionChecker . check ( workflowEntity . id , workflowEntity . nodes ) ,
) . resolves . not . toThrow ( ) ;
2024-02-02 03:21:53 -08:00
} ) ;
} ) ;
describe ( 'checkSubworkflowExecutePolicy()' , ( ) = > {
2024-05-17 01:53:15 -07:00
let license : LicenseMocker ;
beforeAll ( ( ) = > {
license = new LicenseMocker ( ) ;
license . mock ( Container . get ( License ) ) ;
license . enable ( 'feat:sharing' ) ;
} ) ;
2024-02-02 03:21:53 -08:00
describe ( 'no caller policy' , ( ) = > {
test ( 'should fall back to N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION' , async ( ) = > {
config . set ( 'workflows.callerPolicyDefaultOption' , 'none' ) ;
const parentWorkflow = createParentWorkflow ( ) ;
const subworkflow = createSubworkflow ( ) ; // no caller policy
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValue ( memberPersonalProject ) ;
2024-02-02 03:21:53 -08:00
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , parentWorkflow . id ) ;
await expect ( check ) . rejects . toThrow ( toTargetCallErrorMsg ( subworkflow . id ) ) ;
config . load ( config . default ) ;
} ) ;
} ) ;
describe ( 'overridden caller policy' , ( ) = > {
test ( 'if no sharing, should override policy to workflows-from-same-owner' , async ( ) = > {
license . disable ( 'feat:sharing' ) ;
const parentWorkflow = createParentWorkflow ( ) ;
const subworkflow = createSubworkflow ( { policy : 'any' } ) ; // should be overridden
2024-05-17 01:53:15 -07:00
const firstProject = Container . get ( ProjectRepository ) . create ( { id : uuid ( ) } ) ;
const secondProject = Container . get ( ProjectRepository ) . create ( { id : uuid ( ) } ) ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( firstProject ) ; // parent workflow
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( secondProject ) ; // subworkflow
2024-02-02 03:21:53 -08:00
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , parentWorkflow . id ) ;
await expect ( check ) . rejects . toThrow ( toTargetCallErrorMsg ( subworkflow . id ) ) ;
try {
await permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , uuid ( ) ) ;
} catch ( error ) {
if ( error instanceof SubworkflowOperationError ) {
expect ( error . description ) . toBe (
2024-05-17 01:53:15 -07:00
` An admin for the ${ firstProject . name } project can make this change. You may need to tell them the ID of the sub-workflow, which is ${ subworkflow . id } ` ,
2024-02-02 03:21:53 -08:00
) ;
}
}
license . enable ( 'feat:sharing' ) ;
} ) ;
} ) ;
describe ( 'workflows-from-list caller policy' , ( ) = > {
test ( 'should allow if caller list contains parent workflow ID' , async ( ) = > {
const parentWorkflow = createParentWorkflow ( ) ;
const subworkflow = createSubworkflow ( {
policy : 'workflowsFromAList' ,
callerIds : ` 123,456,bcdef, ${ parentWorkflow . id } ` ,
} ) ;
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , parentWorkflow . id ) ;
await expect ( check ) . resolves . not . toThrow ( ) ;
} ) ;
test ( 'should deny if caller list does not contain parent workflow ID' , async ( ) = > {
const parentWorkflow = createParentWorkflow ( ) ;
const subworkflow = createSubworkflow ( {
policy : 'workflowsFromAList' ,
callerIds : 'xyz' ,
} ) ;
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , parentWorkflow . id ) ;
await expect ( check ) . rejects . toThrow ( ) ;
} ) ;
} ) ;
describe ( 'any caller policy' , ( ) = > {
test ( 'should not throw' , async ( ) = > {
const parentWorkflow = createParentWorkflow ( ) ;
const subworkflow = createSubworkflow ( { policy : 'any' } ) ;
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( new Project ( ) ) ;
2024-02-02 03:21:53 -08:00
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , parentWorkflow . id ) ;
await expect ( check ) . resolves . not . toThrow ( ) ;
} ) ;
} ) ;
describe ( 'workflows-from-same-owner caller policy' , ( ) = > {
test ( 'should deny if the two workflows are owned by different users' , async ( ) = > {
2024-05-17 01:53:15 -07:00
const parentWorkflowProject = Container . get ( ProjectRepository ) . create ( { id : uuid ( ) } ) ;
const subworkflowOwner = Container . get ( ProjectRepository ) . create ( { id : uuid ( ) } ) ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( parentWorkflowProject ) ; // parent workflow
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( subworkflowOwner ) ; // subworkflow
2024-02-02 03:21:53 -08:00
const subworkflow = createSubworkflow ( { policy : 'workflowsFromSameOwner' } ) ;
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , uuid ( ) ) ;
await expect ( check ) . rejects . toThrow ( toTargetCallErrorMsg ( subworkflow . id ) ) ;
} ) ;
test ( 'should allow if both workflows are owned by the same user' , async ( ) = > {
const parentWorkflow = createParentWorkflow ( ) ;
2024-05-17 01:53:15 -07:00
const bothWorkflowsProject = Container . get ( ProjectRepository ) . create ( { id : uuid ( ) } ) ;
2024-02-02 03:21:53 -08:00
2024-05-17 01:53:15 -07:00
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( bothWorkflowsProject ) ; // parent workflow
ownershipService . getWorkflowProjectCached . mockResolvedValueOnce ( bothWorkflowsProject ) ; // subworkflow
2024-02-02 03:21:53 -08:00
const subworkflow = createSubworkflow ( { policy : 'workflowsFromSameOwner' } ) ;
const check = permissionChecker . checkSubworkflowExecutePolicy ( subworkflow , parentWorkflow . id ) ;
await expect ( check ) . resolves . not . toThrow ( ) ;
} ) ;
} ) ;
} ) ;