2022-11-11 02:14:45 -08:00
import { v4 as uuid } from 'uuid' ;
2023-03-17 09:24:05 -07:00
import { Container } from 'typedi' ;
2023-10-25 07:35:22 -07:00
import type { INodeTypes } from 'n8n-workflow' ;
import { SubworkflowOperationError , Workflow } from 'n8n-workflow' ;
2022-11-11 02:14:45 -08:00
2022-12-22 09:28:23 -08:00
import config from '@/config' ;
2022-12-22 01:14:15 -08:00
import * as Db from '@/Db' ;
2023-03-17 09:24:05 -07:00
import { Role } from '@db/entities/Role' ;
import { User } from '@db/entities/User' ;
import { SharedWorkflow } from '@db/entities/SharedWorkflow' ;
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials' ;
import { NodeTypes } from '@/NodeTypes' ;
2022-12-22 01:14:15 -08:00
import { PermissionChecker } from '@/UserManagement/PermissionChecker' ;
2022-12-22 09:28:23 -08:00
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper' ;
2023-10-09 07:09:23 -07:00
import { OwnershipService } from '@/services/ownership.service' ;
2023-03-17 09:24:05 -07:00
2023-10-09 07:09:23 -07:00
import { mockInstance } from '../integration/shared/utils/' ;
2022-11-11 02:14:45 -08:00
import {
randomCredentialPayload as randomCred ,
randomPositiveDigit ,
} from '../integration/shared/random' ;
2023-03-17 09:24:05 -07:00
import * as testDb from '../integration/shared/testDb' ;
2022-11-11 02:14:45 -08:00
import type { SaveCredentialFunction } from '../integration/shared/types' ;
2023-10-09 07:09:23 -07:00
import { mockNodeTypesData } from './Helpers' ;
2023-09-20 06:21:42 -07:00
2022-11-11 02:14:45 -08:00
let mockNodeTypes : INodeTypes ;
let credentialOwnerRole : Role ;
let workflowOwnerRole : Role ;
let saveCredential : SaveCredentialFunction ;
2023-03-17 09:24:05 -07:00
mockInstance ( LoadNodesAndCredentials , {
2023-10-09 07:09:23 -07:00
loadedNodes : mockNodeTypesData ( [ 'start' , 'actionNetwork' ] ) ,
2023-03-17 09:24:05 -07:00
} ) ;
2022-11-11 02:14:45 -08:00
beforeAll ( async ( ) = > {
2023-01-13 09:12:22 -08:00
await testDb . init ( ) ;
2022-11-11 02:14:45 -08:00
2023-03-17 09:24:05 -07:00
mockNodeTypes = Container . get ( NodeTypes ) ;
2022-11-11 02:14:45 -08:00
credentialOwnerRole = await testDb . getCredentialOwnerRole ( ) ;
workflowOwnerRole = await testDb . getWorkflowOwnerRole ( ) ;
saveCredential = testDb . affixRoleToSaveCredential ( credentialOwnerRole ) ;
} ) ;
beforeEach ( async ( ) = > {
2023-01-25 01:02:28 -08:00
await testDb . truncate ( [ 'SharedWorkflow' , 'SharedCredentials' , 'Workflow' , 'Credentials' , 'User' ] ) ;
2022-11-11 02:14:45 -08:00
} ) ;
afterAll ( async ( ) = > {
2023-01-13 09:12:22 -08:00
await testDb . terminate ( ) ;
2022-11-11 02:14:45 -08:00
} ) ;
describe ( 'PermissionChecker.check()' , ( ) = > {
test ( 'should allow if workflow has no creds' , async ( ) = > {
const userId = uuid ( ) ;
const workflow = new Workflow ( {
id : randomPositiveDigit ( ) . toString ( ) ,
name : 'test' ,
active : false ,
connections : { } ,
nodeTypes : mockNodeTypes ,
nodes : [
{
id : uuid ( ) ,
name : 'Start' ,
type : 'n8n-nodes-base.start' ,
typeVersion : 1 ,
parameters : { } ,
position : [ 0 , 0 ] ,
} ,
] ,
} ) ;
2023-05-02 01:37:19 -07:00
expect ( async ( ) = > PermissionChecker . check ( workflow , userId ) ) . not . toThrow ( ) ;
2022-11-11 02:14:45 -08:00
} ) ;
test ( 'should allow if requesting user is instance owner' , async ( ) = > {
const owner = await testDb . createOwner ( ) ;
const workflow = new Workflow ( {
id : randomPositiveDigit ( ) . toString ( ) ,
name : 'test' ,
active : false ,
connections : { } ,
nodeTypes : mockNodeTypes ,
nodes : [
{
id : uuid ( ) ,
name : 'Action Network' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] ,
credentials : {
actionNetworkApi : {
id : randomPositiveDigit ( ) . toString ( ) ,
name : 'Action Network Account' ,
} ,
} ,
} ,
] ,
} ) ;
2023-05-02 01:37:19 -07:00
expect ( async ( ) = > PermissionChecker . check ( workflow , owner . id ) ) . not . toThrow ( ) ;
2022-11-11 02:14:45 -08:00
} ) ;
test ( 'should allow if workflow creds are valid subset' , async ( ) = > {
const [ owner , member ] = await Promise . all ( [ testDb . createOwner ( ) , testDb . createUser ( ) ] ) ;
const ownerCred = await saveCredential ( randomCred ( ) , { user : owner } ) ;
const memberCred = await saveCredential ( randomCred ( ) , { user : member } ) ;
const workflow = new Workflow ( {
id : randomPositiveDigit ( ) . toString ( ) ,
name : 'test' ,
active : false ,
connections : { } ,
nodeTypes : mockNodeTypes ,
nodes : [
{
id : uuid ( ) ,
name : 'Action Network' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] ,
credentials : {
actionNetworkApi : {
2023-01-02 08:42:32 -08:00
id : ownerCred.id ,
2022-11-11 02:14:45 -08:00
name : ownerCred.name ,
} ,
} ,
} ,
{
id : uuid ( ) ,
name : 'Action Network 2' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] ,
credentials : {
actionNetworkApi : {
2023-01-02 08:42:32 -08:00
id : memberCred.id ,
2022-11-11 02:14:45 -08:00
name : memberCred.name ,
} ,
} ,
} ,
] ,
} ) ;
2023-05-02 01:37:19 -07:00
expect ( async ( ) = > PermissionChecker . check ( workflow , owner . id ) ) . not . toThrow ( ) ;
2022-11-11 02:14:45 -08:00
} ) ;
test ( 'should deny if workflow creds are not valid subset' , async ( ) = > {
const member = await testDb . createUser ( ) ;
const memberCred = await saveCredential ( randomCred ( ) , { user : member } ) ;
const workflowDetails = {
2023-01-02 08:42:32 -08:00
id : randomPositiveDigit ( ) . toString ( ) ,
2022-11-11 02:14:45 -08:00
name : 'test' ,
active : false ,
connections : { } ,
nodeTypes : mockNodeTypes ,
nodes : [
{
id : uuid ( ) ,
name : 'Action Network' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] as [ number , number ] ,
credentials : {
actionNetworkApi : {
2023-01-02 08:42:32 -08:00
id : memberCred.id ,
2022-11-11 02:14:45 -08:00
name : memberCred.name ,
} ,
} ,
} ,
{
id : uuid ( ) ,
name : 'Action Network 2' ,
type : 'n8n-nodes-base.actionNetwork' ,
parameters : { } ,
typeVersion : 1 ,
position : [ 0 , 0 ] as [ number , number ] ,
credentials : {
actionNetworkApi : {
id : 'non-existing-credential-id' ,
name : 'Non-existing credential name' ,
} ,
} ,
} ,
] ,
} ;
const workflowEntity = await Db . collections . Workflow . save ( workflowDetails ) ;
await Db . collections . SharedWorkflow . save ( {
workflow : workflowEntity ,
user : member ,
role : workflowOwnerRole ,
} ) ;
2023-01-02 08:42:32 -08:00
const workflow = new Workflow ( workflowDetails ) ;
2022-11-11 02:14:45 -08:00
2023-05-23 17:01:45 -07:00
await expect ( PermissionChecker . check ( workflow , member . id ) ) . rejects . toThrow ( ) ;
2022-11-11 02:14:45 -08:00
} ) ;
} ) ;
2022-12-22 09:28:23 -08:00
describe ( 'PermissionChecker.checkSubworkflowExecutePolicy' , ( ) = > {
2022-12-23 06:23:36 -08:00
const userId = uuid ( ) ;
const fakeUser = new User ( ) ;
fakeUser . id = userId ;
2023-07-31 02:37:09 -07:00
const ownershipService = mockInstance ( OwnershipService ) ;
2022-12-23 06:23:36 -08:00
const ownerMockRole = new Role ( ) ;
ownerMockRole . name = 'owner' ;
const sharedWorkflowOwner = new SharedWorkflow ( ) ;
sharedWorkflowOwner . role = ownerMockRole ;
const nonOwnerMockRole = new Role ( ) ;
nonOwnerMockRole . name = 'editor' ;
2023-11-01 10:31:34 -07:00
const nonOwnerUser = new User ( ) ;
nonOwnerUser . id = uuid ( ) ;
2023-08-22 06:58:05 -07:00
2022-12-22 09:28:23 -08:00
test ( 'sets default policy from environment when subworkflow has none' , async ( ) = > {
config . set ( 'workflows.callerPolicyDefaultOption' , 'none' ) ;
2023-11-01 10:31:34 -07:00
jest . spyOn ( ownershipService , 'getWorkflowOwnerCached' ) . mockResolvedValue ( fakeUser ) ;
2022-12-22 09:28:23 -08:00
jest . spyOn ( UserManagementHelper , 'isSharingEnabled' ) . mockReturnValue ( true ) ;
const subworkflow = new Workflow ( {
nodes : [ ] ,
connections : { } ,
active : false ,
2023-03-17 09:24:05 -07:00
nodeTypes : mockNodeTypes ,
2022-12-22 09:28:23 -08:00
id : '2' ,
} ) ;
await expect (
2022-12-23 06:23:36 -08:00
PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , userId ) ,
2022-12-22 09:28:23 -08:00
) . rejects . toThrow ( ` Target workflow ID ${ subworkflow . id } may not be called ` ) ;
} ) ;
2023-11-01 10:31:34 -07:00
test ( 'if sharing is disabled, ensures that workflows are owned by same user and reject running workflows belonging to another user even if setting allows execution' , async ( ) = > {
jest . spyOn ( ownershipService , 'getWorkflowOwnerCached' ) . mockResolvedValue ( nonOwnerUser ) ;
2022-12-22 09:28:23 -08:00
jest . spyOn ( UserManagementHelper , 'isSharingEnabled' ) . mockReturnValue ( false ) ;
const subworkflow = new Workflow ( {
nodes : [ ] ,
connections : { } ,
active : false ,
2023-03-17 09:24:05 -07:00
nodeTypes : mockNodeTypes ,
2022-12-22 09:28:23 -08:00
id : '2' ,
2023-11-01 10:31:34 -07:00
settings : {
callerPolicy : 'any' ,
} ,
2022-12-22 09:28:23 -08:00
} ) ;
await expect (
2022-12-23 06:23:36 -08:00
PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , userId ) ,
2022-12-22 09:28:23 -08:00
) . rejects . toThrow ( ` Target workflow ID ${ subworkflow . id } may not be called ` ) ;
// Check description
try {
2023-05-23 17:01:45 -07:00
await PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , '' , 'abcde' ) ;
2022-12-22 09:28:23 -08:00
} catch ( error ) {
if ( error instanceof SubworkflowOperationError ) {
expect ( error . description ) . toBe (
` ${ fakeUser . firstName } ( ${ fakeUser . email } ) can make this change. You may need to tell them the ID of this workflow, which is ${ subworkflow . id } ` ,
) ;
}
}
} ) ;
2023-11-01 10:31:34 -07:00
test ( 'should throw if allowed list does not contain parent workflow id' , async ( ) = > {
2022-12-23 06:23:36 -08:00
const invalidParentWorkflowId = uuid ( ) ;
2022-12-22 09:28:23 -08:00
jest
2023-07-31 02:37:09 -07:00
. spyOn ( ownershipService , 'getWorkflowOwnerCached' )
2022-12-22 09:28:23 -08:00
. mockImplementation ( async ( workflowId ) = > fakeUser ) ;
jest . spyOn ( UserManagementHelper , 'isSharingEnabled' ) . mockReturnValue ( true ) ;
const subworkflow = new Workflow ( {
nodes : [ ] ,
connections : { } ,
active : false ,
2023-03-17 09:24:05 -07:00
nodeTypes : mockNodeTypes ,
2022-12-22 09:28:23 -08:00
id : '2' ,
settings : {
callerPolicy : 'workflowsFromAList' ,
callerIds : '123,456,bcdef ' ,
} ,
} ) ;
await expect (
2022-12-23 06:23:36 -08:00
PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , userId , invalidParentWorkflowId ) ,
2022-12-22 09:28:23 -08:00
) . rejects . toThrow ( ` Target workflow ID ${ subworkflow . id } may not be called ` ) ;
} ) ;
test ( 'sameOwner passes when both workflows are owned by the same user' , async ( ) = > {
2023-11-01 10:31:34 -07:00
jest . spyOn ( ownershipService , 'getWorkflowOwnerCached' ) . mockImplementation ( async ( ) = > fakeUser ) ;
2022-12-22 09:28:23 -08:00
jest . spyOn ( UserManagementHelper , 'isSharingEnabled' ) . mockReturnValue ( false ) ;
const subworkflow = new Workflow ( {
nodes : [ ] ,
connections : { } ,
active : false ,
2023-03-17 09:24:05 -07:00
nodeTypes : mockNodeTypes ,
2022-12-22 09:28:23 -08:00
id : '2' ,
} ) ;
2022-12-23 06:23:36 -08:00
await expect (
PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , userId , userId ) ,
) . resolves . not . toThrow ( ) ;
2022-12-22 09:28:23 -08:00
} ) ;
test ( 'workflowsFromAList works when the list contains the parent id' , async ( ) = > {
2022-12-23 06:23:36 -08:00
const workflowId = uuid ( ) ;
2022-12-22 09:28:23 -08:00
jest
2023-07-31 02:37:09 -07:00
. spyOn ( ownershipService , 'getWorkflowOwnerCached' )
2022-12-22 09:28:23 -08:00
. mockImplementation ( async ( workflowId ) = > fakeUser ) ;
jest . spyOn ( UserManagementHelper , 'isSharingEnabled' ) . mockReturnValue ( true ) ;
const subworkflow = new Workflow ( {
nodes : [ ] ,
connections : { } ,
active : false ,
2023-03-17 09:24:05 -07:00
nodeTypes : mockNodeTypes ,
2022-12-22 09:28:23 -08:00
id : '2' ,
settings : {
callerPolicy : 'workflowsFromAList' ,
2022-12-23 06:23:36 -08:00
callerIds : ` 123,456,bcdef, ${ workflowId } ` ,
2022-12-22 09:28:23 -08:00
} ,
} ) ;
2022-12-23 06:23:36 -08:00
await expect (
PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , userId , workflowId ) ,
) . resolves . not . toThrow ( ) ;
} ) ;
test ( 'should not throw when workflow policy is set to any' , async ( ) = > {
jest
2023-07-31 02:37:09 -07:00
. spyOn ( ownershipService , 'getWorkflowOwnerCached' )
2022-12-23 06:23:36 -08:00
. mockImplementation ( async ( workflowId ) = > fakeUser ) ;
jest . spyOn ( UserManagementHelper , 'isSharingEnabled' ) . mockReturnValue ( true ) ;
const subworkflow = new Workflow ( {
nodes : [ ] ,
connections : { } ,
active : false ,
2023-03-17 09:24:05 -07:00
nodeTypes : mockNodeTypes ,
2022-12-23 06:23:36 -08:00
id : '2' ,
settings : {
callerPolicy : 'any' ,
} ,
} ) ;
await expect (
PermissionChecker . checkSubworkflowExecutePolicy ( subworkflow , userId ) ,
) . resolves . not . toThrow ( ) ;
2022-12-22 09:28:23 -08:00
} ) ;
} ) ;