n8n/packages/cli/test/integration/access-control/instance-roles.test.ts

317 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
createTeamProject,
mockInstance,
randomCredentialPayload,
testDb,
} from '@n8n/backend-test-utils';
import { InstanceSettingsLoaderConfig } from '@n8n/config';
import { GLOBAL_MEMBER_ROLE, ScopeRepository } from '@n8n/db';
import type { User } from '@n8n/db';
import { Container } from '@n8n/di';
import { InstanceRedactionEnforcementService } from '@/modules/redaction/instance-redaction-enforcement.service';
import { SecuritySettingsService } from '@/services/security-settings.service';
import { createGlobalRoleUser } from './shared-setup';
import { saveCredential } from '../shared/db/credentials';
import { cleanupRolesAndScopes, createCustomRoleWithScopeSlugs } from '../shared/db/roles';
import { createOwner } from '../shared/db/users';
import { initCredentialsTypes, setupTestServer } from '../shared/utils';
/**
* Authorization safety net for *instance* (global) custom roles.
*
* The project-level matrix in this directory proves scope-driven access for
* `roleType: 'project'` roles. These tests do the same for `roleType: 'global'`
* roles, guarding that instance-level gates flow through scope checks
* (`@GlobalScope` / `hasGlobalScope`) rather than literal role-slug equality.
* Each behavior gets a positive (has scope -> allowed) and a negative
* (lacks scope -> 403 / restricted) case, both driven by a custom global role.
*/
describe('Instance (global) custom role authorization', () => {
// Mocked so the security-settings handler returns deterministically once the
// scope gate is passed; authorization itself runs through the real middleware.
mockInstance(SecuritySettingsService);
mockInstance(InstanceRedactionEnforcementService);
mockInstance(InstanceSettingsLoaderConfig, { securityPolicyManagedByEnv: false });
const testServer = setupTestServer({
endpointGroups: ['credentials', 'role', 'security-settings'],
enabledFeatures: ['feat:sharing', 'feat:customRoles', 'feat:personalSpacePolicy'],
});
beforeAll(async () => {
await initCredentialsTypes();
});
afterEach(async () => {
// Users hold FK refs to the custom roles, so truncate them before the role
// cleanup (which deletes by FK-respecting `repository.delete`). Runs in
// `afterEach` because the DI `Connection` is only live during test hooks.
await testDb.truncate(['SharedCredentials', 'CredentialsEntity', 'ProjectRelation', 'User']);
await cleanupRolesAndScopes();
});
describe('credential listing — all vs own (credential:list)', () => {
let owner: User;
beforeEach(async () => {
await testDb.truncate(['SharedCredentials', 'CredentialsEntity']);
owner = await createOwner();
});
test('a custom global role with credential:list sees every credential', async () => {
const { user, agent } = await createGlobalRoleUser(
testServer,
['credential:read', 'credential:list'],
'Global Credential Lister',
);
const ownerCredential = await saveCredential(randomCredentialPayload(), {
user: owner,
role: 'credential:owner',
});
const ownCredential = await saveCredential(randomCredentialPayload(), {
user,
role: 'credential:owner',
});
const response = await agent.get('/credentials').expect(200);
const ids = response.body.data.map((c: { id: string }) => c.id);
expect(ids).toContain(ownerCredential.id);
expect(ids).toContain(ownCredential.id);
});
test('a custom global role without credential:list sees only its own credentials', async () => {
// A role lacking any global credential scope must not see other users'
// credentials — only the ones it owns.
const { user, agent } = await createGlobalRoleUser(
testServer,
['workflow:list'],
'Global Non-Credential Role',
);
const ownerCredential = await saveCredential(randomCredentialPayload(), {
user: owner,
role: 'credential:owner',
});
const ownCredential = await saveCredential(randomCredentialPayload(), {
user,
role: 'credential:owner',
});
const response = await agent.get('/credentials').expect(200);
const ids = response.body.data.map((c: { id: string }) => c.id);
expect(ids).toContain(ownCredential.id);
expect(ids).not.toContain(ownerCredential.id);
});
});
describe('role administration (role:manage / role:read)', () => {
test('a custom global role with role:manage can create, read assignments for, and delete roles', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
['role:manage'],
'Global Role Manager',
);
const created = await agent
.post('/roles')
.send({ displayName: 'Created By Custom', roleType: 'project', scopes: ['workflow:read'] })
.expect(200);
await agent.get(`/roles/${GLOBAL_MEMBER_ROLE.slug}/assignments`).expect(200);
await agent.delete(`/roles/${created.body.data.slug}`).expect(200);
});
test('a custom global role without role:manage cannot create, read assignments for, or delete roles', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
['workflow:list'],
'Global Role Outsider',
);
const target = await createCustomRoleWithScopeSlugs(['workflow:read'], {
roleType: 'project',
displayName: 'Delete Target',
});
await agent
.post('/roles')
.send({ displayName: 'Forbidden Role', roleType: 'project', scopes: ['workflow:read'] })
.expect(403);
await agent.get(`/roles/${GLOBAL_MEMBER_ROLE.slug}/assignments`).expect(403);
await agent.delete(`/roles/${target.slug}`).expect(403);
});
test('a custom global role with role:read can list a roles members', async () => {
const { agent } = await createGlobalRoleUser(testServer, ['role:read'], 'Global Role Reader');
await agent.get(`/roles/${GLOBAL_MEMBER_ROLE.slug}/members`).expect(200);
});
test('a custom global role without role:read cannot list a roles members', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
['workflow:list'],
'Global Members Outsider',
);
await agent.get(`/roles/${GLOBAL_MEMBER_ROLE.slug}/members`).expect(403);
});
});
describe('project-role administration (role:manageProject)', () => {
// A custom global role granted ONLY role:manageProject — the least-privilege
// "project role administrator". It must manage project roles yet be barred
// from every other roleType, and never gain role:manage's global reach.
const MANAGE_PROJECT = ['role:manageProject'];
test('role:manageProject can create a project role', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
MANAGE_PROJECT,
'Project Role Manager',
);
await agent
.post('/roles')
.send({
displayName: 'Created Project Role',
roleType: 'project',
scopes: ['workflow:read'],
})
.expect(200);
});
test('role:manageProject cannot create a global role', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
MANAGE_PROJECT,
'Project Role Manager',
);
await agent
.post('/roles')
.send({ displayName: 'Blocked Global Role', roleType: 'global', scopes: ['workflow:read'] })
.expect(403);
});
test('role:manageProject can update and delete a project role', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
MANAGE_PROJECT,
'Project Role Manager',
);
const projectRole = await createCustomRoleWithScopeSlugs(['workflow:read'], {
roleType: 'project',
displayName: 'Editable Project Role',
});
await agent.patch(`/roles/${projectRole.slug}`).send({ displayName: 'Renamed' }).expect(200);
await agent.delete(`/roles/${projectRole.slug}`).expect(200);
});
test('role:manageProject cannot update or delete a global role', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
MANAGE_PROJECT,
'Project Role Manager',
);
const globalRole = await createCustomRoleWithScopeSlugs(['workflow:read'], {
roleType: 'global',
displayName: 'Off-Limits Global Role',
});
await agent.patch(`/roles/${globalRole.slug}`).send({ displayName: 'Nope' }).expect(403);
await agent.delete(`/roles/${globalRole.slug}`).expect(403);
});
test('role:manageProject can read assignments and project members for a project role', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
MANAGE_PROJECT,
'Project Role Manager',
);
const owner = await createOwner();
const project = await createTeamProject('Project Role Members', owner);
const projectRole = await createCustomRoleWithScopeSlugs(['workflow:read'], {
roleType: 'project',
displayName: 'Assignable Project Role',
});
await agent.get(`/roles/${projectRole.slug}/assignments`).expect(200);
await agent.get(`/roles/${projectRole.slug}/assignments/${project.id}/members`).expect(200);
});
test('role:manageProject cannot read assignments or project members for a global role', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
MANAGE_PROJECT,
'Project Role Manager',
);
const owner = await createOwner();
const project = await createTeamProject('Global Role Members', owner);
await agent.get(`/roles/${GLOBAL_MEMBER_ROLE.slug}/assignments`).expect(403);
await agent
.get(`/roles/${GLOBAL_MEMBER_ROLE.slug}/assignments/${project.id}/members`)
.expect(403);
});
test('role:manage still manages roles of every roleType (backwards compatible)', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
['role:manage'],
'Full Role Manager',
);
const project = await agent
.post('/roles')
.send({
displayName: 'Manage Project Role',
roleType: 'project',
scopes: ['workflow:read'],
})
.expect(200);
const global = await agent
.post('/roles')
.send({ displayName: 'Manage Global Role', roleType: 'global', scopes: ['role:read'] })
.expect(200);
await agent.delete(`/roles/${project.body.data.slug}`).expect(200);
await agent.delete(`/roles/${global.body.data.slug}`).expect(200);
});
test('role:manageProject is present in the scope catalog after startup sync', async () => {
const scopes = await Container.get(ScopeRepository).findByList(['role:manageProject']);
expect(scopes).toHaveLength(1);
});
});
describe('security settings (securitySettings:manage)', () => {
test('a custom global role with securitySettings:manage can read and update security settings', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
['securitySettings:manage'],
'Global Settings Manager',
);
await agent.get('/settings/security').expect(200);
await agent.post('/settings/security').send({}).expect(200);
});
test('a custom global role without securitySettings:manage gets 403', async () => {
const { agent } = await createGlobalRoleUser(
testServer,
['workflow:list'],
'Global Settings Outsider',
);
await agent.get('/settings/security').expect(403);
await agent.post('/settings/security').send({}).expect(403);
});
});
});