mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-29 03:55:15 +02:00
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
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 role’s 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 role’s 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);
|
||
});
|
||
});
|
||
});
|