chore(core): Use roles from database in global roles (#17853)

This commit is contained in:
Andreas Fitzek
2025-08-22 16:02:01 +02:00
committed by GitHub
parent 350f84c49f
commit a8e4387f4d
117 changed files with 875 additions and 410 deletions

View File

@@ -1,6 +1,6 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import type { entities } from '@n8n/db'; import type { entities } from '@n8n/db';
import { DbConnection, DbConnectionOptions } from '@n8n/db'; import { AuthRolesService, DbConnection, DbConnectionOptions } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import type { DataSourceOptions } from '@n8n/typeorm'; import type { DataSourceOptions } from '@n8n/typeorm';
import { DataSource as Connection } from '@n8n/typeorm'; import { DataSource as Connection } from '@n8n/typeorm';
@@ -50,6 +50,8 @@ export async function init() {
const dbConnection = Container.get(DbConnection); const dbConnection = Container.get(DbConnection);
await dbConnection.init(); await dbConnection.init();
await dbConnection.migrate(); await dbConnection.migrate();
await Container.get(AuthRolesService).init();
} }
export function isReady() { export function isReady() {

View File

@@ -0,0 +1,30 @@
import { GLOBAL_SCOPE_MAP, type GlobalRole } from '@n8n/permissions';
import type { Role } from 'entities';
export function buildInRoleToRoleObject(role: GlobalRole): Role {
return {
slug: role,
displayName: role,
scopes: GLOBAL_SCOPE_MAP[role].map((scope) => {
return {
slug: scope,
displayName: scope,
description: null,
};
}),
systemRole: true,
roleType: 'global',
description: `Built-in global role with ${role} permissions.`,
};
}
export const GLOBAL_OWNER_ROLE = buildInRoleToRoleObject('global:owner');
export const GLOBAL_ADMIN_ROLE = buildInRoleToRoleObject('global:admin');
export const GLOBAL_MEMBER_ROLE = buildInRoleToRoleObject('global:member');
export const GLOBAL_ROLES: Record<GlobalRole, Role> = {
'global:owner': GLOBAL_OWNER_ROLE,
'global:admin': GLOBAL_ADMIN_ROLE,
'global:member': GLOBAL_MEMBER_ROLE,
};

View File

@@ -1,4 +1,4 @@
import type { GlobalRole, Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
import type { FindOperator } from '@n8n/typeorm'; import type { FindOperator } from '@n8n/typeorm';
import type express from 'express'; import type express from 'express';
import type { import type {
@@ -105,7 +105,7 @@ export interface PublicUser {
passwordResetToken?: string; passwordResetToken?: string;
createdAt: Date; createdAt: Date;
isPending: boolean; isPending: boolean;
role?: GlobalRole; role?: string;
globalScopes?: Scope[]; globalScopes?: Scope[];
signInType: AuthProviderType; signInType: AuthProviderType;
disabled: boolean; disabled: boolean;

View File

@@ -1,4 +1,4 @@
import type { AuthPrincipal, GlobalRole } from '@n8n/permissions'; import type { AuthPrincipal } from '@n8n/permissions';
import { import {
AfterLoad, AfterLoad,
AfterUpdate, AfterUpdate,
@@ -9,6 +9,8 @@ import {
OneToMany, OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
BeforeInsert, BeforeInsert,
JoinColumn,
ManyToOne,
} from '@n8n/typeorm'; } from '@n8n/typeorm';
import type { IUser, IUserSettings } from 'n8n-workflow'; import type { IUser, IUserSettings } from 'n8n-workflow';
@@ -16,9 +18,11 @@ import { JsonColumn, WithTimestamps } from './abstract-entity';
import type { ApiKey } from './api-key'; import type { ApiKey } from './api-key';
import type { AuthIdentity } from './auth-identity'; import type { AuthIdentity } from './auth-identity';
import type { ProjectRelation } from './project-relation'; import type { ProjectRelation } from './project-relation';
import { Role } from './role';
import type { SharedCredentials } from './shared-credentials'; import type { SharedCredentials } from './shared-credentials';
import type { SharedWorkflow } from './shared-workflow'; import type { SharedWorkflow } from './shared-workflow';
import type { IPersonalizationSurveyAnswers } from './types-db'; import type { IPersonalizationSurveyAnswers } from './types-db';
import { GLOBAL_OWNER_ROLE } from '../constants';
import { isValidEmail } from '../utils/is-valid-email'; import { isValidEmail } from '../utils/is-valid-email';
import { lowerCaser, objectRetriever } from '../utils/transformers'; import { lowerCaser, objectRetriever } from '../utils/transformers';
@@ -53,8 +57,9 @@ export class User extends WithTimestamps implements IUser, AuthPrincipal {
@JsonColumn({ nullable: true }) @JsonColumn({ nullable: true })
settings: IUserSettings | null; settings: IUserSettings | null;
@Column({ type: String }) @ManyToOne(() => Role)
role: GlobalRole; @JoinColumn({ name: 'roleSlug', referencedColumnName: 'slug' })
role: Role;
@OneToMany('AuthIdentity', 'user') @OneToMany('AuthIdentity', 'user')
authIdentities: AuthIdentity[]; authIdentities: AuthIdentity[];
@@ -108,7 +113,7 @@ export class User extends WithTimestamps implements IUser, AuthPrincipal {
@AfterLoad() @AfterLoad()
@AfterUpdate() @AfterUpdate()
computeIsPending(): void { computeIsPending(): void {
this.isPending = this.password === null && this.role !== 'global:owner'; this.isPending = this.password === null && this.role?.slug !== GLOBAL_OWNER_ROLE.slug;
} }
toJSON() { toJSON() {

View File

@@ -16,6 +16,7 @@ export { separate } from './utils/separate';
export { sql } from './utils/sql'; export { sql } from './utils/sql';
export { idStringifier, lowerCaser, objectRetriever, sqlite } from './utils/transformers'; export { idStringifier, lowerCaser, objectRetriever, sqlite } from './utils/transformers';
export * from './constants';
export * from './entities'; export * from './entities';
export * from './entities/types-db'; export * from './entities/types-db';
export { NoXss } from './utils/validators/no-xss.validator'; export { NoXss } from './utils/validators/no-xss.validator';

View File

@@ -1,6 +1,7 @@
import type { GlobalRole } from '@n8n/permissions'; import type { GlobalRole } from '@n8n/permissions';
import { getApiKeyScopesForRole } from '@n8n/permissions'; import { getApiKeyScopesForRole } from '@n8n/permissions';
import { GLOBAL_ROLES } from '../../constants';
import { ApiKey } from '../../entities'; import { ApiKey } from '../../entities';
import type { MigrationContext, ReversibleMigration } from '../migration-types'; import type { MigrationContext, ReversibleMigration } from '../migration-types';
@@ -26,7 +27,10 @@ export class AddScopesColumnToApiKeys1742918400000 implements ReversibleMigratio
); );
for (const { id, role } of apiKeysWithRoles) { for (const { id, role } of apiKeysWithRoles) {
const scopes = getApiKeyScopesForRole(role); const dbRole = GLOBAL_ROLES[role];
const scopes = getApiKeyScopesForRole({
role: dbRole,
});
await queryRunner.manager.update(ApiKey, { id }, { scopes }); await queryRunner.manager.update(ApiKey, { id }, { scopes });
} }
} }

View File

@@ -0,0 +1,48 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
/*
* This migration removes the old 'role' column from the 'user' table
* and ensures that all users have a valid role set in the 'roleSlug' column.
* It also ensures that the 'roleSlug' column is correctly populated with the
* values from the 'role' column before dropping it.
* This is a reversible migration, allowing the role column to be restored if needed.
*/
export class RemoveOldRoleColumn1750252139170 implements ReversibleMigration {
async up({ schemaBuilder: { dropColumns }, escape, runQuery }: MigrationContext) {
const roleTableName = escape.tableName('role');
const userTableName = escape.tableName('user');
const slugColumn = escape.columnName('slug');
const roleColumn = escape.columnName('role');
const roleSlugColumn = escape.columnName('roleSlug');
// Fallback to 'global:member' for users that do not have a correct role set
// This should not happen in a correctly set up system, but we want to ensure
// that all users have a role set, before we add the foreign key constraint
await runQuery(
`UPDATE ${userTableName} SET ${roleSlugColumn} = 'global:member', ${roleColumn} = 'global:member' WHERE NOT EXISTS (SELECT 1 FROM ${roleTableName} WHERE ${slugColumn} = ${roleColumn})`,
);
await runQuery(
`UPDATE ${userTableName} SET ${roleSlugColumn} = ${roleColumn} WHERE ${roleColumn} != ${roleSlugColumn}`,
);
await dropColumns('user', ['role']);
}
async down({ schemaBuilder: { addColumns, column }, escape, runQuery }: MigrationContext) {
const userTableName = escape.tableName('user');
const roleColumn = escape.columnName('role');
const roleSlugColumn = escape.columnName('roleSlug');
await addColumns('user', [column('role').varchar(128).default("'global:member'").notNull]);
await runQuery(
`UPDATE ${userTableName} SET ${roleColumn} = ${roleSlugColumn} WHERE ${roleSlugColumn} != ${roleColumn}`,
);
// Fallback to 'global:member' for users that do not have a correct role set
await runQuery(
`UPDATE ${userTableName} SET ${roleColumn} = 'global:member' WHERE NOT EXISTS (SELECT 1 FROM role WHERE slug = ${roleColumn})`,
);
}
}

View File

@@ -91,6 +91,7 @@ import { AddLastActiveAtColumnToUser1750252139166 } from '../common/175025213916
import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables'; import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables';
import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables'; import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables';
import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable'; import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable';
import { RemoveOldRoleColumn1750252139170 } from '../common/1750252139170-RemoveOldRoleColumn';
import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution'; import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution';
import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables'; import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables';
import type { Migration } from '../migration-types'; import type { Migration } from '../migration-types';
@@ -193,4 +194,5 @@ export const mysqlMigrations: Migration[] = [
LinkRoleToUserTable1750252139168, LinkRoleToUserTable1750252139168,
AddInputsOutputsToTestCaseExecution1752669793000, AddInputsOutputsToTestCaseExecution1752669793000,
CreateDataStoreTables1754475614601, CreateDataStoreTables1754475614601,
RemoveOldRoleColumn1750252139170,
]; ];

View File

@@ -92,6 +92,7 @@ import { AddLastActiveAtColumnToUser1750252139166 } from '../common/175025213916
import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables'; import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables';
import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables'; import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables';
import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable'; import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable';
import { RemoveOldRoleColumn1750252139170 } from '../common/1750252139170-RemoveOldRoleColumn';
import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables'; import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables';
import type { Migration } from '../migration-types'; import type { Migration } from '../migration-types';
@@ -191,4 +192,5 @@ export const postgresMigrations: Migration[] = [
LinkRoleToUserTable1750252139168, LinkRoleToUserTable1750252139168,
AddInputsOutputsToTestCaseExecution1752669793000, AddInputsOutputsToTestCaseExecution1752669793000,
CreateDataStoreTables1754475614601, CreateDataStoreTables1754475614601,
RemoveOldRoleColumn1750252139170,
]; ];

View File

@@ -88,6 +88,7 @@ import { AddLastActiveAtColumnToUser1750252139166 } from '../common/175025213916
import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables'; import { AddScopeTables1750252139166 } from '../common/1750252139166-AddScopeTables';
import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables'; import { AddRolesTables1750252139167 } from '../common/1750252139167-AddRolesTables';
import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable'; import { LinkRoleToUserTable1750252139168 } from '../common/1750252139168-LinkRoleToUserTable';
import { RemoveOldRoleColumn1750252139170 } from '../common/1750252139170-RemoveOldRoleColumn';
import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution'; import { AddInputsOutputsToTestCaseExecution1752669793000 } from '../common/1752669793000-AddInputsOutputsToTestCaseExecution';
import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables'; import { CreateDataStoreTables1754475614601 } from '../common/1754475614601-CreateDataStoreTables';
import type { Migration } from '../migration-types'; import type { Migration } from '../migration-types';
@@ -185,6 +186,7 @@ const sqliteMigrations: Migration[] = [
LinkRoleToUserTable1750252139168, LinkRoleToUserTable1750252139168,
AddInputsOutputsToTestCaseExecution1752669793000, AddInputsOutputsToTestCaseExecution1752669793000,
CreateDataStoreTables1754475614601, CreateDataStoreTables1754475614601,
RemoveOldRoleColumn1750252139170,
]; ];
export { sqliteMigrations }; export { sqliteMigrations };

View File

@@ -16,7 +16,11 @@ export class ProjectRelationRepository extends Repository<ProjectRelation> {
projectId: In(projectIds), projectId: In(projectIds),
role: 'project:personalOwner', role: 'project:personalOwner',
}, },
relations: { user: true }, relations: {
user: {
role: true,
},
},
}); });
} }

View File

@@ -55,19 +55,20 @@ export class UserRepository extends Repository<User> {
email, email,
password: Not(IsNull()), password: Not(IsNull()),
}, },
relations: ['authIdentities'], relations: ['authIdentities', 'role'],
}); });
} }
/** Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }` */ /** Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }` */
async countUsersByRole() { async countUsersByRole() {
const escapedRoleSlug = this.manager.connection.driver.escape('roleSlug');
const rows = (await this.createQueryBuilder() const rows = (await this.createQueryBuilder()
.select(['role', 'COUNT(role) as count']) .select([escapedRoleSlug, `COUNT(${escapedRoleSlug}) as count`])
.groupBy('role') .groupBy(escapedRoleSlug)
.execute()) as Array<{ role: string; count: string }>; .execute()) as Array<{ roleSlug: string; count: string }>;
return rows.reduce( return rows.reduce(
(acc, row) => { (acc, row) => {
acc[row.role] = parseInt(row.count, 10); acc[row.roleSlug] = parseInt(row.count, 10);
return acc; return acc;
}, },
{} as Record<string, number>, {} as Record<string, number>,
@@ -91,20 +92,25 @@ export class UserRepository extends Repository<User> {
const createInner = async (entityManager: EntityManager) => { const createInner = async (entityManager: EntityManager) => {
const newUser = entityManager.create(User, user); const newUser = entityManager.create(User, user);
const savedUser = await entityManager.save<User>(newUser); const savedUser = await entityManager.save<User>(newUser);
const userWithRole = await entityManager.findOne(User, {
where: { id: savedUser.id },
relations: ['role'],
});
if (!userWithRole) throw new Error('Failed to create user!');
const savedProject = await entityManager.save<Project>( const savedProject = await entityManager.save<Project>(
entityManager.create(Project, { entityManager.create(Project, {
type: 'personal', type: 'personal',
name: savedUser.createPersonalProjectName(), name: userWithRole.createPersonalProjectName(),
}), }),
); );
await entityManager.save<ProjectRelation>( await entityManager.save<ProjectRelation>(
entityManager.create(ProjectRelation, { entityManager.create(ProjectRelation, {
projectId: savedProject.id, projectId: savedProject.id,
userId: savedUser.id, userId: userWithRole.id,
role: 'project:personalOwner', role: 'project:personalOwner',
}), }),
); );
return { user: savedUser, project: savedProject }; return { user: userWithRole, project: savedProject };
}; };
if (transactionManager) { if (transactionManager) {
return await createInner(transactionManager); return await createInner(transactionManager);
@@ -127,6 +133,7 @@ export class UserRepository extends Repository<User> {
project: { sharedWorkflows: { workflowId, role: 'workflow:owner' } }, project: { sharedWorkflows: { workflowId, role: 'workflow:owner' } },
}, },
}, },
relations: ['role'],
}); });
} }
@@ -143,6 +150,7 @@ export class UserRepository extends Repository<User> {
projectId, projectId,
}, },
}, },
relations: ['role'],
}); });
} }
@@ -286,6 +294,8 @@ export class UserRepository extends Repository<User> {
this.applyUserListExpand(queryBuilder, expand); this.applyUserListExpand(queryBuilder, expand);
this.applyUserListPagination(queryBuilder, take, skip); this.applyUserListPagination(queryBuilder, take, skip);
this.applyUserListSort(queryBuilder, sortBy); this.applyUserListSort(queryBuilder, sortBy);
queryBuilder.leftJoinAndSelect('user.role', 'role');
queryBuilder.leftJoinAndSelect('role.scopes', 'scopes');
return queryBuilder; return queryBuilder;
} }

View File

@@ -6,21 +6,39 @@ describe('Redactable Decorator', () => {
class TestClass { class TestClass {
@Redactable() @Redactable()
methodWithUser(arg: { methodWithUser(arg: {
user: { id: string; email?: string; firstName?: string; lastName?: string; role: string }; user: {
id: string;
email?: string;
firstName?: string;
lastName?: string;
role: { slug: string };
};
}) { }) {
return arg; return arg;
} }
@Redactable('inviter') @Redactable('inviter')
methodWithInviter(arg: { methodWithInviter(arg: {
inviter: { id: string; email?: string; firstName?: string; lastName?: string; role: string }; inviter: {
id: string;
email?: string;
firstName?: string;
lastName?: string;
role: { slug: string };
};
}) { }) {
return arg; return arg;
} }
@Redactable('invitee') @Redactable('invitee')
methodWithInvitee(arg: { methodWithInvitee(arg: {
invitee: { id: string; email?: string; firstName?: string; lastName?: string; role: string }; invitee: {
id: string;
email?: string;
firstName?: string;
lastName?: string;
role: { slug: string };
};
}) { }) {
return arg; return arg;
} }
@@ -29,7 +47,13 @@ describe('Redactable Decorator', () => {
methodWithMultipleArgs( methodWithMultipleArgs(
firstArg: { something: string }, firstArg: { something: string },
secondArg: { secondArg: {
user: { id: string; email?: string; firstName?: string; lastName?: string; role: string }; user: {
id: string;
email?: string;
firstName?: string;
lastName?: string;
role: { slug: string };
};
}, },
) { ) {
return { firstArg, secondArg }; return { firstArg, secondArg };
@@ -69,7 +93,7 @@ describe('Redactable Decorator', () => {
email: 'test@example.com', email: 'test@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'admin', role: { slug: 'admin' },
}, },
}; };
@@ -91,7 +115,7 @@ describe('Redactable Decorator', () => {
email: 'test@example.com', email: 'test@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'admin', role: { slug: 'admin' },
}, },
}; };
@@ -113,7 +137,7 @@ describe('Redactable Decorator', () => {
email: 'test@example.com', email: 'test@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'admin', role: { slug: 'admin' },
}, },
}; };
@@ -132,7 +156,7 @@ describe('Redactable Decorator', () => {
const input = { const input = {
user: { user: {
id: '123', id: '123',
role: 'admin', role: { slug: 'admin' },
}, },
}; };
@@ -153,7 +177,7 @@ describe('Redactable Decorator', () => {
user: { user: {
id: '123', id: '123',
email: 'test@example.com', email: 'test@example.com',
role: 'admin', role: { slug: 'admin' },
}, },
}; };
@@ -182,7 +206,7 @@ describe('Redactable Decorator', () => {
user: { user: {
id: '123', id: '123',
email: 'test@example.com', email: 'test@example.com',
role: 'admin', role: { slug: 'admin' },
}, },
}; };

View File

@@ -5,7 +5,9 @@ type UserLike = {
email?: string; email?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
role: string; role: {
slug: string;
};
}; };
export class RedactableError extends UnexpectedError { export class RedactableError extends UnexpectedError {
@@ -22,7 +24,7 @@ function toRedactable(userLike: UserLike) {
_email: userLike.email, _email: userLike.email,
_firstName: userLike.firstName, _firstName: userLike.firstName,
_lastName: userLike.lastName, _lastName: userLike.lastName,
globalRole: userLike.role, globalRole: userLike.role.slug,
}; };
} }

View File

@@ -95,6 +95,8 @@ exports[`Scope Information ensure scopes are defined correctly 1`] = `
"workflow:share", "workflow:share",
"workflow:execute", "workflow:execute",
"workflow:move", "workflow:move",
"workflow:activate",
"workflow:deactivate",
"workflow:create", "workflow:create",
"workflow:read", "workflow:read",
"workflow:update", "workflow:update",
@@ -121,6 +123,14 @@ exports[`Scope Information ensure scopes are defined correctly 1`] = `
"dataStore:writeRow", "dataStore:writeRow",
"dataStore:listProject", "dataStore:listProject",
"dataStore:*", "dataStore:*",
"execution:delete",
"execution:read",
"execution:list",
"execution:get",
"execution:*",
"workflowTags:update",
"workflowTags:list",
"workflowTags:*",
"*", "*",
] ]
`; `;

View File

@@ -22,11 +22,13 @@ export const RESOURCES = {
user: ['resetPassword', 'changeRole', 'enforceMfa', ...DEFAULT_OPERATIONS] as const, user: ['resetPassword', 'changeRole', 'enforceMfa', ...DEFAULT_OPERATIONS] as const,
variable: [...DEFAULT_OPERATIONS] as const, variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const, workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const, workflow: ['share', 'execute', 'move', 'activate', 'deactivate', ...DEFAULT_OPERATIONS] as const,
folder: [...DEFAULT_OPERATIONS, 'move'] as const, folder: [...DEFAULT_OPERATIONS, 'move'] as const,
insights: ['list'] as const, insights: ['list'] as const,
oidc: ['manage'] as const, oidc: ['manage'] as const,
dataStore: [...DEFAULT_OPERATIONS, 'readRow', 'writeRow', 'listProject'] as const, dataStore: [...DEFAULT_OPERATIONS, 'readRow', 'writeRow', 'listProject'] as const,
execution: ['delete', 'read', 'list', 'get'] as const,
workflowTags: ['update', 'list'] as const,
} as const; } as const;
export const API_KEY_RESOURCES = { export const API_KEY_RESOURCES = {

View File

@@ -13,7 +13,7 @@ export { hasGlobalScope } from './utilities/has-global-scope.ee';
export { combineScopes } from './utilities/combine-scopes.ee'; export { combineScopes } from './utilities/combine-scopes.ee';
export { rolesWithScope } from './utilities/roles-with-scope.ee'; export { rolesWithScope } from './utilities/roles-with-scope.ee';
export { getGlobalScopes } from './utilities/get-global-scopes.ee'; export { getGlobalScopes } from './utilities/get-global-scopes.ee';
export { getRoleScopes } from './utilities/get-role-scopes.ee'; export { getRoleScopes, getAuthPrincipalScopes } from './utilities/get-role-scopes.ee';
export { getResourcePermissions } from './utilities/get-resource-permissions.ee'; export { getResourcePermissions } from './utilities/get-resource-permissions.ee';
export type { PermissionsRecord } from './utilities/get-resource-permissions.ee'; export type { PermissionsRecord } from './utilities/get-resource-permissions.ee';
export * from './public-api-permissions.ee'; export * from './public-api-permissions.ee';

View File

@@ -1,4 +1,4 @@
import type { ApiKeyScope, GlobalRole } from './types.ee'; import { isApiKeyScope, type ApiKeyScope, type AuthPrincipal, type GlobalRole } from './types.ee';
export const OWNER_API_KEY_SCOPES: ApiKeyScope[] = [ export const OWNER_API_KEY_SCOPES: ApiKeyScope[] = [
'user:read', 'user:read',
@@ -65,14 +65,46 @@ export const MEMBER_API_KEY_SCOPES: ApiKeyScope[] = [
'credential:delete', 'credential:delete',
]; ];
/**
* This is a bit of a mess, because we are handing out scopes in API keys that are only
* valid for the personal project, which is enforced in the public API, because the workflows,
* execution endpoints are limited to the personal project.
* This is a temporary solution until we have a better way to handle personal projects and API key scopes!
*/
export const API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT: ApiKeyScope[] = [
'workflowTags:update',
'workflowTags:list',
'workflow:create',
'workflow:read',
'workflow:update',
'workflow:delete',
'workflow:list',
'workflow:move',
'workflow:activate',
'workflow:deactivate',
'execution:delete',
'execution:read',
'execution:list',
'credential:create',
'credential:move',
'credential:delete',
];
const MAP_ROLE_SCOPES: Record<GlobalRole, ApiKeyScope[]> = { const MAP_ROLE_SCOPES: Record<GlobalRole, ApiKeyScope[]> = {
'global:owner': OWNER_API_KEY_SCOPES, 'global:owner': OWNER_API_KEY_SCOPES,
'global:admin': ADMIN_API_KEY_SCOPES, 'global:admin': ADMIN_API_KEY_SCOPES,
'global:member': MEMBER_API_KEY_SCOPES, 'global:member': MEMBER_API_KEY_SCOPES,
}; };
export const getApiKeyScopesForRole = (role: GlobalRole) => { export const getApiKeyScopesForRole = (user: AuthPrincipal) => {
return MAP_ROLE_SCOPES[role]; return [
...new Set(
user.role.scopes
.map((scope) => scope.slug)
.concat(API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT)
.filter(isApiKeyScope),
),
];
}; };
export const getOwnerOnlyApiKeyScopes = () => { export const getOwnerOnlyApiKeyScopes = () => {

View File

@@ -1,5 +1,5 @@
import { RESOURCES } from './constants.ee'; import { API_KEY_RESOURCES, RESOURCES } from './constants.ee';
import type { Scope, ScopeInformation } from './types.ee'; import type { ApiKeyScope, Scope, ScopeInformation } from './types.ee';
function buildResourceScopes() { function buildResourceScopes() {
const resourceScopes = Object.entries(RESOURCES).flatMap(([resource, operations]) => [ const resourceScopes = Object.entries(RESOURCES).flatMap(([resource, operations]) => [
@@ -11,8 +11,19 @@ function buildResourceScopes() {
return resourceScopes; return resourceScopes;
} }
function buildApiKeyScopes() {
const apiKeyScopes = Object.entries(API_KEY_RESOURCES).flatMap(([resource, operations]) => [
...operations.map((op) => `${resource}:${op}` as const),
]) as ApiKeyScope[];
return new Set(apiKeyScopes);
}
export const ALL_SCOPES = buildResourceScopes(); export const ALL_SCOPES = buildResourceScopes();
// Keep the type of Scope[] to ensure that ApiKeyScopes are a subset of Scopes!
export const ALL_API_KEY_SCOPES = buildApiKeyScopes();
export const scopeInformation: Partial<Record<Scope, ScopeInformation>> = { export const scopeInformation: Partial<Record<Scope, ScopeInformation>> = {
'annotationTag:create': { 'annotationTag:create': {
displayName: 'Create Annotation Tag', displayName: 'Create Annotation Tag',

View File

@@ -10,6 +10,7 @@ import type {
teamRoleSchema, teamRoleSchema,
workflowSharingRoleSchema, workflowSharingRoleSchema,
} from './schemas.ee'; } from './schemas.ee';
import { ALL_API_KEY_SCOPES } from './scope-information';
export type ScopeInformation = { export type ScopeInformation = {
displayName: string; displayName: string;
@@ -76,12 +77,21 @@ export type AllRolesMap = {
workflow: Array<RoleObject<WorkflowSharingRole>>; workflow: Array<RoleObject<WorkflowSharingRole>>;
}; };
export type DbScope = {
slug: Scope;
};
export type DbRole = {
slug: string;
scopes: DbScope[];
};
/** /**
* Represents an authenticated entity in the system that can have specific permissions via a role. * Represents an authenticated entity in the system that can have specific permissions via a role.
* @property role - The global role this principal has * @property role - The global role this principal has
*/ */
export type AuthPrincipal = { export type AuthPrincipal = {
role: GlobalRole; role: DbRole;
}; };
// #region Public API // #region Public API
@@ -101,4 +111,9 @@ type AllApiKeyScopesObject = {
export type ApiKeyScope = AllApiKeyScopesObject[PublicApiKeyResources]; export type ApiKeyScope = AllApiKeyScopesObject[PublicApiKeyResources];
export function isApiKeyScope(scope: Scope): scope is ApiKeyScope {
// We are casting with as for runtime type checking
return ALL_API_KEY_SCOPES.has(scope as ApiKeyScope);
}
// #endregion // #endregion

View File

@@ -1,19 +1,19 @@
import { GLOBAL_SCOPE_MAP } from '../../roles/role-maps.ee'; import { GLOBAL_SCOPE_MAP } from '../../roles/role-maps.ee';
import type { GlobalRole } from '../../types.ee';
import { getGlobalScopes } from '../get-global-scopes.ee'; import { getGlobalScopes } from '../get-global-scopes.ee';
import { createAuthPrinicipal } from './utils';
describe('getGlobalScopes', () => { describe('getGlobalScopes', () => {
test.each(['global:owner', 'global:admin', 'global:member'] as const)( test.each(['global:owner', 'global:admin', 'global:member'] as const)(
'should return correct scopes for %s', 'should return correct scopes for %s',
(role) => { (role) => {
const scopes = getGlobalScopes({ role }); const scopes = getGlobalScopes(createAuthPrinicipal(role));
expect(scopes).toEqual(GLOBAL_SCOPE_MAP[role]); expect(scopes).toEqual(GLOBAL_SCOPE_MAP[role]);
}, },
); );
test('should return empty array for non-existent role', () => { test('should return empty array for non-existent role', () => {
const scopes = getGlobalScopes({ role: 'non:existent' as GlobalRole }); const scopes = getGlobalScopes(createAuthPrinicipal('non:existent'));
expect(scopes).toEqual([]); expect(scopes).toEqual([]);
}); });

View File

@@ -15,6 +15,7 @@ describe('permissions', () => {
externalSecretsProvider: {}, externalSecretsProvider: {},
externalSecret: {}, externalSecret: {},
eventBusDestination: {}, eventBusDestination: {},
execution: {},
ldap: {}, ldap: {},
license: {}, license: {},
logStreaming: {}, logStreaming: {},
@@ -29,6 +30,7 @@ describe('permissions', () => {
variable: {}, variable: {},
workersView: {}, workersView: {},
workflow: {}, workflow: {},
workflowTags: {},
folder: {}, folder: {},
insights: {}, insights: {},
dataStore: {}, dataStore: {},
@@ -130,6 +132,8 @@ describe('permissions', () => {
list: true, list: true,
}, },
dataStore: {}, dataStore: {},
execution: {},
workflowTags: {},
}; };
expect(getResourcePermissions(scopes)).toEqual(permissionRecord); expect(getResourcePermissions(scopes)).toEqual(permissionRecord);

View File

@@ -1,5 +1,6 @@
import type { GlobalRole, Scope } from '../../types.ee'; import type { GlobalRole, Scope } from '../../types.ee';
import { hasGlobalScope } from '../has-global-scope.ee'; import { hasGlobalScope } from '../has-global-scope.ee';
import { createAuthPrinicipal } from './utils';
describe('hasGlobalScope', () => { describe('hasGlobalScope', () => {
describe('single scope checks', () => { describe('single scope checks', () => {
@@ -11,7 +12,7 @@ describe('hasGlobalScope', () => {
] as Array<{ role: GlobalRole; scope: Scope; expected: boolean }>)( ] as Array<{ role: GlobalRole; scope: Scope; expected: boolean }>)(
'$role with $scope -> $expected', '$role with $scope -> $expected',
({ role, scope, expected }) => { ({ role, scope, expected }) => {
expect(hasGlobalScope({ role }, scope)).toBe(expected); expect(hasGlobalScope(createAuthPrinicipal(role), scope)).toBe(expected);
}, },
); );
}); });
@@ -19,7 +20,7 @@ describe('hasGlobalScope', () => {
describe('multiple scopes', () => { describe('multiple scopes', () => {
test('oneOf mode (default)', () => { test('oneOf mode (default)', () => {
expect( expect(
hasGlobalScope({ role: 'global:member' }, [ hasGlobalScope(createAuthPrinicipal('global:member'), [
'tag:create', 'tag:create',
'user:list', 'user:list',
// a member cannot create users // a member cannot create users
@@ -31,7 +32,7 @@ describe('hasGlobalScope', () => {
test('allOf mode', () => { test('allOf mode', () => {
expect( expect(
hasGlobalScope( hasGlobalScope(
{ role: 'global:member' }, createAuthPrinicipal('global:member'),
[ [
'tag:create', 'tag:create',
'user:list', 'user:list',
@@ -45,6 +46,6 @@ describe('hasGlobalScope', () => {
}); });
test('edge cases', () => { test('edge cases', () => {
expect(hasGlobalScope({ role: 'global:owner' }, [])).toBe(false); expect(hasGlobalScope(createAuthPrinicipal('global:owner'), [])).toBe(false);
}); });
}); });

View File

@@ -0,0 +1,40 @@
import { GLOBAL_SCOPE_MAP } from '@/roles/role-maps.ee';
import { globalRoleSchema } from '@/schemas.ee';
import type { AuthPrincipal, GlobalRole, Scope } from '@/types.ee';
function createBuildInAuthPrinicipal(role: GlobalRole): AuthPrincipal {
return {
role: {
slug: role,
scopes:
GLOBAL_SCOPE_MAP[role].map((scope) => {
return {
slug: scope,
};
}) || [],
},
};
}
export function createAuthPrinicipal(role: string, scopes: Scope[] = []): AuthPrincipal {
try {
const isGlobalRole = globalRoleSchema.parse(role);
if (isGlobalRole) {
return createBuildInAuthPrinicipal(isGlobalRole);
}
} catch (error) {
// If the role is not a valid global role, we proceed
// to create a custom role with the provided scopes.
}
return {
role: {
slug: role,
scopes:
scopes.map((scope) => {
return {
slug: scope,
};
}) || [],
},
};
}

View File

@@ -1,4 +1,3 @@
import { GLOBAL_SCOPE_MAP } from '../roles/role-maps.ee';
import type { AuthPrincipal } from '../types.ee'; import type { AuthPrincipal } from '../types.ee';
/** /**
@@ -6,4 +5,5 @@ import type { AuthPrincipal } from '../types.ee';
* @param principal - Contains the role to look up * @param principal - Contains the role to look up
* @returns Array of scopes for the role, or empty array if not found * @returns Array of scopes for the role, or empty array if not found
*/ */
export const getGlobalScopes = (principal: AuthPrincipal) => GLOBAL_SCOPE_MAP[principal.role] ?? []; export const getGlobalScopes = (principal: AuthPrincipal) =>
principal.role.scopes.map((scope) => scope.slug) ?? [];

View File

@@ -1,5 +1,5 @@
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee'; import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
import type { AllRoleTypes, Resource, Scope } from '../types.ee'; import type { AllRoleTypes, AuthPrincipal, Resource, Scope } from '../types.ee';
export const COMBINED_ROLE_MAP = Object.fromEntries( export const COMBINED_ROLE_MAP = Object.fromEntries(
Object.values(ALL_ROLE_MAPS).flatMap((o: Record<string, Scope[]>) => Object.entries(o)), Object.values(ALL_ROLE_MAPS).flatMap((o: Record<string, Scope[]>) => Object.entries(o)),
@@ -10,6 +10,8 @@ export const COMBINED_ROLE_MAP = Object.fromEntries(
* @param role - The role to look up * @param role - The role to look up
* @param filters - Optional resources to filter scopes by * @param filters - Optional resources to filter scopes by
* @returns Array of matching scopes * @returns Array of matching scopes
*
* @deprecated Use the 'getRoleScopes' from the AuthRolesService instead.
*/ */
export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[] { export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[] {
let scopes = COMBINED_ROLE_MAP[role]; let scopes = COMBINED_ROLE_MAP[role];
@@ -18,3 +20,22 @@ export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[]
} }
return scopes; return scopes;
} }
/**
* Gets scopes for an auth principal, optionally filtered by resource types.
* @param user - The auth principal to search scopes for
* @param filters - Optional resources to filter scopes by
* @returns Array of matching scopes
*/
export function getAuthPrincipalScopes(user: AuthPrincipal, filters?: Resource[]): Scope[] {
if (!user.role) {
const e = new Error('AuthPrincipal does not have a role defined');
console.error('AuthPrincipal does not have a role defined', e);
throw e;
}
let scopes = user.role.scopes.map((s) => s.slug);
if (filters) {
scopes = scopes.filter((s) => filters.includes(s.split(':')[0] as Resource));
}
return scopes;
}

View File

@@ -1,6 +1,6 @@
import { getGlobalScopes } from './get-global-scopes.ee';
import { hasScope } from './has-scope.ee'; import { hasScope } from './has-scope.ee';
import type { AuthPrincipal, Scope, ScopeOptions } from '../types.ee'; import type { AuthPrincipal, Scope, ScopeOptions } from '../types.ee';
import { getAuthPrincipalScopes } from './get-role-scopes.ee';
/** /**
* Checks if an auth-principal has specified global scope(s). * Checks if an auth-principal has specified global scope(s).
@@ -12,6 +12,6 @@ export const hasGlobalScope = (
scope: Scope | Scope[], scope: Scope | Scope[],
scopeOptions?: ScopeOptions, scopeOptions?: ScopeOptions,
): boolean => { ): boolean => {
const global = getGlobalScopes(principal); const global = getAuthPrincipalScopes(principal);
return hasScope(scope, { global }, undefined, scopeOptions); return hasScope(scope, { global }, undefined, scopeOptions);
}; };

View File

@@ -1,5 +1,5 @@
import { testDb, createWorkflow, mockInstance } from '@n8n/backend-test-utils'; import { testDb, createWorkflow, mockInstance } from '@n8n/backend-test-utils';
import type { User, ExecutionEntity } from '@n8n/db'; import { type User, type ExecutionEntity, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import type { Response } from 'express'; import type { Response } from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -41,7 +41,7 @@ setupTestServer({ endpointGroups: [] });
mockInstance(Telemetry); mockInstance(Telemetry);
beforeAll(async () => { beforeAll(async () => {
owner = await createUser({ role: 'global:owner' }); owner = await createUser({ role: GLOBAL_OWNER_ROLE });
runner = Container.get(WorkflowRunner); runner = Container.get(WorkflowRunner);
}); });

View File

@@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { Time } from '@n8n/constants'; import { Time } from '@n8n/constants';
import type { AuthenticatedRequest, User } from '@n8n/db'; import type { AuthenticatedRequest, User } from '@n8n/db';
import { InvalidAuthTokenRepository, UserRepository } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, InvalidAuthTokenRepository, UserRepository } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import type { NextFunction, Response } from 'express'; import type { NextFunction, Response } from 'express';
@@ -134,7 +134,7 @@ export class AuthService {
const isWithinUsersLimit = this.license.isWithinUsersLimit(); const isWithinUsersLimit = this.license.isWithinUsersLimit();
if ( if (
config.getEnv('userManagement.isInstanceOwnerSetUp') && config.getEnv('userManagement.isInstanceOwnerSetUp') &&
user.role !== 'global:owner' && user.role.slug !== GLOBAL_OWNER_ROLE.slug &&
!isWithinUsersLimit !isWithinUsersLimit
) { ) {
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
@@ -174,6 +174,7 @@ export class AuthService {
// TODO: Use an in-memory ttl-cache to cache the User object for upto a minute // TODO: Use an in-memory ttl-cache to cache the User object for upto a minute
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { id: jwtPayload.id }, where: { id: jwtPayload.id },
relations: ['role'],
}); });
if ( if (
@@ -237,7 +238,7 @@ export class AuthService {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { id: decodedToken.sub }, where: { id: decodedToken.sub },
relations: ['authIdentities'], relations: ['authIdentities', 'role'],
}); });
if (!user) { if (!user) {

View File

@@ -13,7 +13,7 @@ export const handleEmailLogin = async (
): Promise<User | undefined> => { ): Promise<User | undefined> => {
const user = await Container.get(UserRepository).findOne({ const user = await Container.get(UserRepository).findOne({
where: { email }, where: { email },
relations: ['authIdentities'], relations: ['authIdentities', 'role'],
}); });
if (user?.password && (await Container.get(PasswordUtility).compare(password, user.password))) { if (user?.password && (await Container.get(PasswordUtility).compare(password, user.password))) {

View File

@@ -1,4 +1,11 @@
import { CredentialsEntity, Project, User, SharedCredentials, ProjectRepository } from '@n8n/db'; import {
CredentialsEntity,
Project,
User,
SharedCredentials,
ProjectRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db';
import { Command } from '@n8n/decorators'; import { Command } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
@@ -253,7 +260,11 @@ export class ImportCredentialsCommand extends BaseCommand<z.infer<typeof flagsSc
} }
if (!userId) { if (!userId) {
const owner = await this.transactionManager.findOneBy(User, { role: 'global:owner' }); const owner = await this.transactionManager.findOneBy(User, {
role: {
slug: GLOBAL_OWNER_ROLE.slug,
},
});
if (!owner) { if (!owner) {
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }

View File

@@ -5,6 +5,7 @@ import {
SharedWorkflowRepository, SharedWorkflowRepository,
WorkflowRepository, WorkflowRepository,
UserRepository, UserRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { Command } from '@n8n/decorators'; import { Command } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
@@ -217,7 +218,9 @@ export class ImportWorkflowsCommand extends BaseCommand<z.infer<typeof flagsSche
} }
if (!userId) { if (!userId) {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); const owner = await Container.get(UserRepository).findOneBy({
role: { slug: GLOBAL_OWNER_ROLE.slug },
});
if (!owner) { if (!owner) {
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }

View File

@@ -2,6 +2,7 @@ import { LDAP_FEATURE_NAME, LDAP_DEFAULT_CONFIGURATION } from '@n8n/constants';
import { import {
AuthIdentityRepository, AuthIdentityRepository,
AuthProviderSyncHistoryRepository, AuthProviderSyncHistoryRepository,
GLOBAL_OWNER_ROLE,
ProjectRelationRepository, ProjectRelationRepository,
ProjectRepository, ProjectRepository,
SettingsRepository, SettingsRepository,
@@ -169,7 +170,10 @@ export class Reset extends BaseCommand<z.infer<typeof flagsSchema>> {
} }
private async getOwner() { private async getOwner() {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); const owner = await Container.get(UserRepository).findOne({
where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
relations: ['role'],
});
if (!owner) { if (!owner) {
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
} }

View File

@@ -7,6 +7,7 @@ import {
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
UserRepository, UserRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { Command } from '@n8n/decorators'; import { Command } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
@@ -61,7 +62,9 @@ export class Reset extends BaseCommand {
} }
async getInstanceOwner(): Promise<User> { async getInstanceOwner(): Promise<User> {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); const owner = await Container.get(UserRepository).findOneBy({
role: { slug: GLOBAL_OWNER_ROLE.slug },
});
if (owner) return owner; if (owner) return owner;
@@ -71,7 +74,9 @@ export class Reset extends BaseCommand {
await Container.get(UserRepository).save(user); await Container.get(UserRepository).save(user);
return await Container.get(UserRepository).findOneByOrFail({ role: 'global:owner' }); return await Container.get(UserRepository).findOneByOrFail({
role: { slug: GLOBAL_OWNER_ROLE.slug },
});
} }
async catch(error: Error): Promise<void> { async catch(error: Error): Promise<void> {

View File

@@ -129,7 +129,7 @@ describe('ApiKeysController', () => {
id: '123', id: '123',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:member', role: { slug: 'global:member' },
mfaEnabled: false, mfaEnabled: false,
}); });

View File

@@ -47,7 +47,7 @@ describe('AuthController', () => {
const member = mock<User>({ const member = mock<User>({
id: '123', id: '123',
role: 'global:member', role: { slug: 'global:member' },
mfaEnabled: false, mfaEnabled: false,
}); });

View File

@@ -1,7 +1,7 @@
import { UserUpdateRequestDto } from '@n8n/api-types'; import { UserUpdateRequestDto } from '@n8n/api-types';
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import type { AuthenticatedRequest, User, PublicUser } from '@n8n/db'; import type { AuthenticatedRequest, User, PublicUser } from '@n8n/db';
import { InvalidAuthTokenRepository, UserRepository } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, InvalidAuthTokenRepository, UserRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import type { Response } from 'express'; import type { Response } from 'express';
import { mock, anyObject } from 'jest-mock-extended'; import { mock, anyObject } from 'jest-mock-extended';
@@ -38,7 +38,7 @@ describe('MeController', () => {
email: 'valid@email.com', email: 'valid@email.com',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
mfaEnabled: false, mfaEnabled: false,
}); });
const payload = new UserUpdateRequestDto({ const payload = new UserUpdateRequestDto({
@@ -88,7 +88,7 @@ describe('MeController', () => {
id: '123', id: '123',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
mfaEnabled: false, mfaEnabled: false,
}); });
const req = mock<AuthenticatedRequest>({ user }); const req = mock<AuthenticatedRequest>({ user });
@@ -115,7 +115,7 @@ describe('MeController', () => {
email: 'valid@email.com', email: 'valid@email.com',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
mfaEnabled: true, mfaEnabled: true,
}); });
const req = mock<AuthenticatedRequest>({ user, browserId }); const req = mock<AuthenticatedRequest>({ user, browserId });
@@ -139,7 +139,7 @@ describe('MeController', () => {
email: 'valid@email.com', email: 'valid@email.com',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
mfaEnabled: true, mfaEnabled: true,
}); });
const req = mock<AuthenticatedRequest>({ user, browserId }); const req = mock<AuthenticatedRequest>({ user, browserId });
@@ -165,7 +165,7 @@ describe('MeController', () => {
email: 'valid@email.com', email: 'valid@email.com',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
mfaEnabled: true, mfaEnabled: true,
mfaSecret: 'secret', mfaSecret: 'secret',
}); });

View File

@@ -1,11 +1,12 @@
import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types';
import type { Logger } from '@n8n/backend-common'; import type { Logger } from '@n8n/backend-common';
import type { import {
AuthenticatedRequest, type AuthenticatedRequest,
User, type User,
PublicUser, type PublicUser,
SettingsRepository, type SettingsRepository,
UserRepository, type UserRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import type { Response } from 'express'; import type { Response } from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -65,7 +66,7 @@ describe('OwnerController', () => {
it('should setup the instance owner successfully', async () => { it('should setup the instance owner successfully', async () => {
const user = mock<User>({ const user = mock<User>({
id: 'userId', id: 'userId',
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
authIdentities: [], authIdentities: [],
}); });
const browserId = 'test-browser-id'; const browserId = 'test-browser-id';
@@ -85,7 +86,8 @@ describe('OwnerController', () => {
const result = await controller.setupOwner(req, res, payload); const result = await controller.setupOwner(req, res, payload);
expect(userRepository.findOneOrFail).toHaveBeenCalledWith({ expect(userRepository.findOneOrFail).toHaveBeenCalledWith({
where: { role: 'global:owner' }, where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
relations: ['role'],
}); });
expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false }); expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false });
expect(authService.issueCookie).toHaveBeenCalledWith(res, user, false, browserId); expect(authService.issueCookie).toHaveBeenCalledWith(res, user, false, browserId);

View File

@@ -35,7 +35,7 @@ describe('UsersController', () => {
const request = mock<AuthenticatedRequest>({ const request = mock<AuthenticatedRequest>({
user: { id: '123' }, user: { id: '123' },
}); });
userRepository.findOneBy.mockResolvedValue(mock<User>({ id: '456' })); userRepository.findOne.mockResolvedValue(mock<User>({ id: '456' }));
projectService.getUserOwnedOrAdminProjects.mockResolvedValue([]); projectService.getUserOwnedOrAdminProjects.mockResolvedValue([]);
await controller.changeGlobalRole( await controller.changeGlobalRole(

View File

@@ -33,7 +33,7 @@ export class ApiKeysController {
_res: Response, _res: Response,
@Body body: CreateApiKeyRequestDto, @Body body: CreateApiKeyRequestDto,
) { ) {
if (!this.publicApiKeyService.apiKeyHasValidScopesForRole(req.user.role, body.scopes)) { if (!this.publicApiKeyService.apiKeyHasValidScopesForRole(req.user, body.scopes)) {
throw new BadRequestError('Invalid scopes for user role'); throw new BadRequestError('Invalid scopes for user role');
} }
@@ -80,7 +80,7 @@ export class ApiKeysController {
@Param('id') apiKeyId: string, @Param('id') apiKeyId: string,
@Body body: UpdateApiKeyRequestDto, @Body body: UpdateApiKeyRequestDto,
) { ) {
if (!this.publicApiKeyService.apiKeyHasValidScopesForRole(req.user.role, body.scopes)) { if (!this.publicApiKeyService.apiKeyHasValidScopesForRole(req.user, body.scopes)) {
throw new BadRequestError('Invalid scopes for user role'); throw new BadRequestError('Invalid scopes for user role');
} }
@@ -91,8 +91,7 @@ export class ApiKeysController {
@Get('/scopes', { middlewares: [isApiEnabledMiddleware] }) @Get('/scopes', { middlewares: [isApiEnabledMiddleware] })
async getApiKeyScopes(req: AuthenticatedRequest, _res: Response) { async getApiKeyScopes(req: AuthenticatedRequest, _res: Response) {
const { role } = req.user; const scopes = getApiKeyScopesForRole(req.user);
const scopes = getApiKeyScopesForRole(role);
return scopes; return scopes;
} }
} }

View File

@@ -1,7 +1,7 @@
import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types'; import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common'; import { Logger } from '@n8n/backend-common';
import type { User, PublicUser } from '@n8n/db'; import type { User, PublicUser } from '@n8n/db';
import { UserRepository, AuthenticatedRequest } from '@n8n/db'; import { UserRepository, AuthenticatedRequest, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators'; import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { isEmail } from 'class-validator'; import { isEmail } from 'class-validator';
@@ -60,7 +60,7 @@ export class AuthController {
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password); const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password);
// if the user is an owner, continue with the login // if the user is an owner, continue with the login
if ( if (
preliminaryUser?.role === 'global:owner' || preliminaryUser?.role.slug === GLOBAL_OWNER_ROLE.slug ||
preliminaryUser?.settings?.allowSSOManualLogin preliminaryUser?.settings?.allowSSOManualLogin
) { ) {
user = preliminaryUser; user = preliminaryUser;
@@ -70,7 +70,7 @@ export class AuthController {
} }
} else if (isLdapCurrentAuthenticationMethod()) { } else if (isLdapCurrentAuthenticationMethod()) {
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password); const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password);
if (preliminaryUser?.role === 'global:owner') { if (preliminaryUser?.role.slug === GLOBAL_OWNER_ROLE.slug) {
user = preliminaryUser; user = preliminaryUser;
usedAuthenticationMethod = 'email'; usedAuthenticationMethod = 'email';
} else { } else {

View File

@@ -305,7 +305,9 @@ export class E2EController {
id: uuid(), id: uuid(),
...owner, ...owner,
password: await this.passwordUtility.hash(owner.password), password: await this.passwordUtility.hash(owner.password),
role: 'global:owner', role: {
slug: 'global:owner',
},
}), }),
]; ];
@@ -314,7 +316,9 @@ export class E2EController {
id: uuid(), id: uuid(),
...admin, ...admin,
password: await this.passwordUtility.hash(admin.password), password: await this.passwordUtility.hash(admin.password),
role: 'global:admin', role: {
slug: 'global:admin',
},
}), }),
); );
@@ -324,7 +328,9 @@ export class E2EController {
id: uuid(), id: uuid(),
...payload, ...payload,
password: await this.passwordUtility.hash(password), password: await this.passwordUtility.hash(password),
role: 'global:member', role: {
slug: 'global:member',
},
}), }),
); );
} }

View File

@@ -99,7 +99,10 @@ export class InvitationController {
) { ) {
const { inviterId, firstName, lastName, password } = payload; const { inviterId, firstName, lastName, password } = payload;
const users = await this.userRepository.findManyByIds([inviterId, inviteeId]); const users = await this.userRepository.find({
where: [{ id: inviterId }, { id: inviteeId }],
relations: ['role'],
});
if (users.length !== 2) { if (users.length !== 2) {
this.logger.debug( this.logger.debug(

View File

@@ -109,6 +109,7 @@ export class MeController {
await this.userService.update(userId, payload); await this.userService.update(userId, payload);
const user = await this.userRepository.findOneOrFail({ const user = await this.userRepository.findOneOrFail({
where: { id: userId }, where: { id: userId },
relations: ['role'],
}); });
this.logger.info('User updated successfully', { userId }); this.logger.info('User updated successfully', { userId });

View File

@@ -132,7 +132,10 @@ export class MFAController {
await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode); await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode);
} }
const updatedUser = await this.userRepository.findOneByOrFail({ id: userId }); const updatedUser = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['role'],
});
this.authService.issueCookie(res, updatedUser, false, req.browserId); this.authService.issueCookie(res, updatedUser, false, req.browserId);
} }

View File

@@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common';
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import { Time } from '@n8n/constants'; import { Time } from '@n8n/constants';
import type { CredentialsEntity, User } from '@n8n/db'; import type { CredentialsEntity, User } from '@n8n/db';
import { CredentialsRepository } from '@n8n/db'; import { CredentialsRepository, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import Csrf from 'csrf'; import Csrf from 'csrf';
import type { Response } from 'express'; import type { Response } from 'express';
@@ -44,7 +44,7 @@ describe('OAuth1CredentialController', () => {
id: '123', id: '123',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
const credential = mock<CredentialsEntity>({ const credential = mock<CredentialsEntity>({
id: '1', id: '1',

View File

@@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common';
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import { Time } from '@n8n/constants'; import { Time } from '@n8n/constants';
import type { CredentialsEntity, User } from '@n8n/db'; import type { CredentialsEntity, User } from '@n8n/db';
import { CredentialsRepository } from '@n8n/db'; import { CredentialsRepository, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import Csrf from 'csrf'; import Csrf from 'csrf';
import { type Response } from 'express'; import { type Response } from 'express';
@@ -46,7 +46,7 @@ describe('OAuth2CredentialController', () => {
id: '123', id: '123',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
const credential = mock<CredentialsEntity>({ const credential = mock<CredentialsEntity>({
id: '1', id: '1',

View File

@@ -1,6 +1,11 @@
import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common'; import { Logger } from '@n8n/backend-common';
import { AuthenticatedRequest, SettingsRepository, UserRepository } from '@n8n/db'; import {
AuthenticatedRequest,
GLOBAL_OWNER_ROLE,
SettingsRepository,
UserRepository,
} from '@n8n/db';
import { Body, GlobalScope, Post, RestController } from '@n8n/decorators'; import { Body, GlobalScope, Post, RestController } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
@@ -44,7 +49,8 @@ export class OwnerController {
} }
let owner = await this.userRepository.findOneOrFail({ let owner = await this.userRepository.findOneOrFail({
where: { role: 'global:owner' }, where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
relations: ['role'],
}); });
owner.email = email; owner.email = email;
owner.firstName = firstName; owner.firstName = firstName;

View File

@@ -4,7 +4,7 @@ import {
ResolvePasswordTokenQueryDto, ResolvePasswordTokenQueryDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common'; import { Logger } from '@n8n/backend-common';
import { UserRepository } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, UserRepository } from '@n8n/db';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators'; import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import { hasGlobalScope } from '@n8n/permissions'; import { hasGlobalScope } from '@n8n/permissions';
import { Response } from 'express'; import { Response } from 'express';
@@ -71,7 +71,7 @@ export class PasswordResetController {
return; return;
} }
if (user.role !== 'global:owner' && !this.license.isWithinUsersLimit()) { if (user.role.slug !== GLOBAL_OWNER_ROLE.slug && !this.license.isWithinUsersLimit()) {
this.logger.debug( this.logger.debug(
'Request to send password reset email failed because the user limit was reached', 'Request to send password reset email failed because the user limit was reached',
); );
@@ -147,7 +147,7 @@ export class PasswordResetController {
const user = await this.authService.resolvePasswordResetToken(token); const user = await this.authService.resolvePasswordResetToken(token);
if (!user) throw new NotFoundError(''); if (!user) throw new NotFoundError('');
if (user.role !== 'global:owner' && !this.license.isWithinUsersLimit()) { if (user.role.slug !== GLOBAL_OWNER_ROLE.slug && !this.license.isWithinUsersLimit()) {
this.logger.debug( this.logger.debug(
'Request to resolve password token failed because the user limit was reached', 'Request to resolve password token failed because the user limit was reached',
{ userId: user.id }, { userId: user.id },

View File

@@ -14,7 +14,12 @@ import {
Param, Param,
Query, Query,
} from '@n8n/decorators'; } from '@n8n/decorators';
import { combineScopes, getRoleScopes, hasGlobalScope } from '@n8n/permissions'; import {
combineScopes,
getAuthPrincipalScopes,
getRoleScopes,
hasGlobalScope,
} from '@n8n/permissions';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In, Not } from '@n8n/typeorm'; import { In, Not } from '@n8n/typeorm';
@@ -60,7 +65,7 @@ export class ProjectController {
this.eventService.emit('team-project-created', { this.eventService.emit('team-project-created', {
userId: req.user.id, userId: req.user.id,
role: req.user.role, role: req.user.role.slug,
}); });
return { return {
@@ -68,7 +73,7 @@ export class ProjectController {
role: 'project:admin', role: 'project:admin',
scopes: [ scopes: [
...combineScopes({ ...combineScopes({
global: getRoleScopes(req.user.role), global: getAuthPrincipalScopes(req.user),
project: getRoleScopes('project:admin'), project: getRoleScopes('project:admin'),
}), }),
], ],
@@ -105,7 +110,7 @@ export class ProjectController {
if (result.scopes) { if (result.scopes) {
result.scopes.push( result.scopes.push(
...combineScopes({ ...combineScopes({
global: getRoleScopes(req.user.role), global: getAuthPrincipalScopes(req.user),
project: getRoleScopes(pr.role), project: getRoleScopes(pr.role),
}), }),
); );
@@ -121,13 +126,13 @@ export class ProjectController {
// If the user has the global `project:read` scope then they may not // If the user has the global `project:read` scope then they may not
// own this relationship in that case we use the global user role // own this relationship in that case we use the global user role
// instead of the relation role, which is for another user. // instead of the relation role, which is for another user.
role: req.user.role, role: req.user.role.slug,
scopes: [], scopes: [],
}, },
); );
if (result.scopes) { if (result.scopes) {
result.scopes.push(...combineScopes({ global: getRoleScopes(req.user.role) })); result.scopes.push(...combineScopes({ global: getAuthPrincipalScopes(req.user) }));
} }
results.push(result); results.push(result);
@@ -151,7 +156,7 @@ export class ProjectController {
} }
const scopes: Scope[] = [ const scopes: Scope[] = [
...combineScopes({ ...combineScopes({
global: getRoleScopes(req.user.role), global: getAuthPrincipalScopes(req.user),
project: getRoleScopes('project:personalOwner'), project: getRoleScopes('project:personalOwner'),
}), }),
]; ];
@@ -189,7 +194,7 @@ export class ProjectController {
})), })),
scopes: [ scopes: [
...combineScopes({ ...combineScopes({
global: getRoleScopes(req.user.role), global: getAuthPrincipalScopes(req.user),
...(myRelation ? { project: getRoleScopes(myRelation.role) } : {}), ...(myRelation ? { project: getRoleScopes(myRelation.role) } : {}),
}), }),
], ],
@@ -230,7 +235,7 @@ export class ProjectController {
this.eventService.emit('team-project-updated', { this.eventService.emit('team-project-updated', {
userId: req.user.id, userId: req.user.id,
role: req.user.role, role: req.user.role.slug,
members: relations, members: relations,
projectId, projectId,
}); });
@@ -251,7 +256,7 @@ export class ProjectController {
this.eventService.emit('team-project-deleted', { this.eventService.emit('team-project-deleted', {
userId: req.user.id, userId: req.user.id,
role: req.user.role, role: req.user.role.slug,
projectId, projectId,
removalType: query.transferId !== undefined ? 'transfer' : 'delete', removalType: query.transferId !== undefined ? 'transfer' : 'delete',
targetProjectId: query.transferId, targetProjectId: query.transferId,

View File

@@ -15,6 +15,8 @@ import {
SharedWorkflowRepository, SharedWorkflowRepository,
UserRepository, UserRepository,
AuthenticatedRequest, AuthenticatedRequest,
GLOBAL_ADMIN_ROLE,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { import {
GlobalScope, GlobalScope,
@@ -115,6 +117,9 @@ export class UsersController {
withInviteUrl, withInviteUrl,
inviterId: req.user.id, inviterId: req.user.id,
}); });
if (listQueryOptions.select && !listQueryOptions.select?.includes('role')) {
delete user.role;
}
return { return {
...user, ...user,
projectRelations: u.projectRelations?.map((pr) => ({ projectRelations: u.projectRelations?.map((pr) => ({
@@ -137,12 +142,16 @@ export class UsersController {
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) { async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
const user = await this.userRepository.findOneOrFail({ const user = await this.userRepository.findOneOrFail({
where: { id: req.params.id }, where: { id: req.params.id },
relations: ['role'],
}); });
if (!user) { if (!user) {
throw new NotFoundError('User not found'); throw new NotFoundError('User not found');
} }
if (req.user.role === 'global:admin' && user.role === 'global:owner') { if (
req.user.role.slug === GLOBAL_ADMIN_ROLE.slug &&
user.role.slug === GLOBAL_OWNER_ROLE.slug
) {
throw new ForbiddenError('Admin cannot reset password of global owner'); throw new ForbiddenError('Admin cannot reset password of global owner');
} }
@@ -186,7 +195,10 @@ export class UsersController {
const { transferId } = req.query; const { transferId } = req.query;
const userToDelete = await this.userRepository.findOneBy({ id: idToDelete }); const userToDelete = await this.userRepository.findOne({
where: { id: idToDelete },
relations: ['role'],
});
if (!userToDelete) { if (!userToDelete) {
throw new NotFoundError( throw new NotFoundError(
@@ -194,7 +206,7 @@ export class UsersController {
); );
} }
if (userToDelete.role === 'global:owner') { if (userToDelete.role.slug === GLOBAL_OWNER_ROLE.slug) {
throw new ForbiddenError('Instance owner cannot be deleted.'); throw new ForbiddenError('Instance owner cannot be deleted.');
} }
@@ -302,16 +314,25 @@ export class UsersController {
const { NO_ADMIN_ON_OWNER, NO_USER, NO_OWNER_ON_OWNER } = const { NO_ADMIN_ON_OWNER, NO_USER, NO_OWNER_ON_OWNER } =
UsersController.ERROR_MESSAGES.CHANGE_ROLE; UsersController.ERROR_MESSAGES.CHANGE_ROLE;
const targetUser = await this.userRepository.findOneBy({ id }); const targetUser = await this.userRepository.findOne({
where: { id },
relations: ['role'],
});
if (targetUser === null) { if (targetUser === null) {
throw new NotFoundError(NO_USER); throw new NotFoundError(NO_USER);
} }
if (req.user.role === 'global:admin' && targetUser.role === 'global:owner') { if (
req.user.role.slug === GLOBAL_ADMIN_ROLE.slug &&
targetUser.role.slug === GLOBAL_OWNER_ROLE.slug
) {
throw new ForbiddenError(NO_ADMIN_ON_OWNER); throw new ForbiddenError(NO_ADMIN_ON_OWNER);
} }
if (req.user.role === 'global:owner' && targetUser.role === 'global:owner') { if (
req.user.role.slug === GLOBAL_OWNER_ROLE.slug &&
targetUser.role.slug === GLOBAL_OWNER_ROLE.slug
) {
throw new ForbiddenError(NO_OWNER_ON_OWNER); throw new ForbiddenError(NO_OWNER_ON_OWNER);
} }

View File

@@ -1,5 +1,5 @@
import type { SourceControlledFile } from '@n8n/api-types'; import type { SourceControlledFile } from '@n8n/api-types';
import { User } from '@n8n/db'; import { GLOBAL_ADMIN_ROLE, User } from '@n8n/db';
import type { import type {
SharedCredentials, SharedCredentials,
SharedWorkflow, SharedWorkflow,
@@ -23,7 +23,7 @@ import { SourceControlContext } from '../types/source-control-context';
describe('SourceControlExportService', () => { describe('SourceControlExportService', () => {
const globalAdminContext = new SourceControlContext( const globalAdminContext = new SourceControlContext(
Object.assign(new User(), { Object.assign(new User(), {
role: 'global:admin', role: GLOBAL_ADMIN_ROLE,
}), }),
); );

View File

@@ -5,6 +5,8 @@ import {
type ProjectRepository, type ProjectRepository,
User, User,
WorkflowEntity, WorkflowEntity,
GLOBAL_ADMIN_ROLE,
GLOBAL_MEMBER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import * as fastGlob from 'fast-glob'; import * as fastGlob from 'fast-glob';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -20,13 +22,13 @@ jest.mock('fast-glob');
const globalAdminContext = new SourceControlContext( const globalAdminContext = new SourceControlContext(
Object.assign(new User(), { Object.assign(new User(), {
role: 'global:admin', role: GLOBAL_ADMIN_ROLE,
}), }),
); );
const globalMemberContext = new SourceControlContext( const globalMemberContext = new SourceControlContext(
Object.assign(new User(), { Object.assign(new User(), {
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
}), }),
); );

View File

@@ -1,12 +1,14 @@
import type { SourceControlledFile } from '@n8n/api-types'; import type { SourceControlledFile } from '@n8n/api-types';
import type { import {
Variables, type Variables,
FolderWithWorkflowAndSubFolderCount, type FolderWithWorkflowAndSubFolderCount,
TagEntity, type TagEntity,
User, type User,
FolderRepository, type FolderRepository,
TagRepository, type TagRepository,
WorkflowEntity, type WorkflowEntity,
GLOBAL_MEMBER_ROLE,
GLOBAL_ADMIN_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -149,8 +151,9 @@ describe('SourceControlService', () => {
describe('getStatus', () => { describe('getStatus', () => {
it('ensure updatedAt field for last deleted tag', async () => { it('ensure updatedAt field for last deleted tag', async () => {
// ARRANGE // ARRANGE
const user = mock<User>(); const user = mock<User>({
user.role = 'global:admin'; role: GLOBAL_ADMIN_ROLE,
});
sourceControlImportService.getRemoteVersionIdsFromFiles.mockResolvedValue([]); sourceControlImportService.getRemoteVersionIdsFromFiles.mockResolvedValue([]);
sourceControlImportService.getLocalVersionIdsFromDb.mockResolvedValue([]); sourceControlImportService.getLocalVersionIdsFromDb.mockResolvedValue([]);
@@ -204,8 +207,9 @@ describe('SourceControlService', () => {
it('ensure updatedAt field for last deleted folder', async () => { it('ensure updatedAt field for last deleted folder', async () => {
// ARRANGE // ARRANGE
const user = mock<User>(); const user = mock<User>({
user.role = 'global:admin'; role: GLOBAL_ADMIN_ROLE,
});
sourceControlImportService.getRemoteVersionIdsFromFiles.mockResolvedValue([]); sourceControlImportService.getRemoteVersionIdsFromFiles.mockResolvedValue([]);
sourceControlImportService.getLocalVersionIdsFromDb.mockResolvedValue([]); sourceControlImportService.getLocalVersionIdsFromDb.mockResolvedValue([]);
@@ -262,8 +266,9 @@ describe('SourceControlService', () => {
it('conflict depends on the value of `direction`', async () => { it('conflict depends on the value of `direction`', async () => {
// ARRANGE // ARRANGE
const user = mock<User>(); const user = mock<User>({
user.role = 'global:admin'; role: GLOBAL_ADMIN_ROLE,
});
// Define a credential that does only exist locally. // Define a credential that does only exist locally.
// Pulling this would delete it so it should be marked as a conflict. // Pulling this would delete it so it should be marked as a conflict.
@@ -368,8 +373,9 @@ describe('SourceControlService', () => {
it('should throw `ForbiddenError` if direction is pull and user is not allowed to globally pull', async () => { it('should throw `ForbiddenError` if direction is pull and user is not allowed to globally pull', async () => {
// ARRANGE // ARRANGE
const user = mock<User>(); const user = mock<User>({
user.role = 'global:member'; role: GLOBAL_MEMBER_ROLE,
});
// ACT // ACT
await expect( await expect(
@@ -387,7 +393,7 @@ describe('SourceControlService', () => {
'should return file content for $type', 'should return file content for $type',
async ({ type, id, content }) => { async ({ type, id, content }) => {
jest.spyOn(gitService, 'getFileContent').mockResolvedValue(content); jest.spyOn(gitService, 'getFileContent').mockResolvedValue(content);
const user = mock<User>({ id: 'user-id', role: 'global:admin' }); const user = mock<User>({ id: 'user-id', role: GLOBAL_ADMIN_ROLE });
const result = await sourceControlService.getRemoteFileEntity({ user, type, id }); const result = await sourceControlService.getRemoteFileEntity({ user, type, id });
@@ -398,7 +404,7 @@ describe('SourceControlService', () => {
it.each<SourceControlledFile['type']>(['folders', 'credential', 'tags', 'variables'])( it.each<SourceControlledFile['type']>(['folders', 'credential', 'tags', 'variables'])(
'should throw an error if the file type is not handled', 'should throw an error if the file type is not handled',
async (type) => { async (type) => {
const user = mock<User>({ id: 'user-id', role: 'global:admin' }); const user = mock<User>({ id: 'user-id', role: { slug: 'global:admin' } });
await expect( await expect(
sourceControlService.getRemoteFileEntity({ user, type, id: 'unknown' }), sourceControlService.getRemoteFileEntity({ user, type, id: 'unknown' }),
).rejects.toThrow(`Unsupported file type: ${type}`); ).rejects.toThrow(`Unsupported file type: ${type}`);
@@ -407,7 +413,7 @@ describe('SourceControlService', () => {
it('should fail if the git service fails to get the file content', async () => { it('should fail if the git service fails to get the file content', async () => {
jest.spyOn(gitService, 'getFileContent').mockRejectedValue(new Error('Git service error')); jest.spyOn(gitService, 'getFileContent').mockRejectedValue(new Error('Git service error'));
const user = mock<User>({ id: 'user-id', role: 'global:admin' }); const user = mock<User>({ id: 'user-id', role: { slug: 'global:admin' } });
await expect( await expect(
sourceControlService.getRemoteFileEntity({ user, type: 'workflow', id: '1234' }), sourceControlService.getRemoteFileEntity({ user, type: 'workflow', id: '1234' }),
@@ -417,7 +423,7 @@ describe('SourceControlService', () => {
it('should throw an error if the user does not have access to the project', async () => { it('should throw an error if the user does not have access to the project', async () => {
const user = mock<User>({ const user = mock<User>({
id: 'user-id', id: 'user-id',
role: 'global:member', role: { slug: 'global:member' },
}); });
jest jest
.spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext') .spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext')
@@ -429,7 +435,7 @@ describe('SourceControlService', () => {
}); });
it('should return content for an authorized workflow', async () => { it('should return content for an authorized workflow', async () => {
const user = mock<User>({ id: 'user-id', role: 'global:member' }); const user = mock<User>({ id: 'user-id', role: { slug: 'global:member' } });
jest jest
.spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext') .spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext')
.mockResolvedValue([{ id: '1234' } as WorkflowEntity]); .mockResolvedValue([{ id: '1234' } as WorkflowEntity]);

View File

@@ -1,4 +1,4 @@
import type { IWorkflowDb } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, type IWorkflowDb } from '@n8n/db';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { InstanceSettings } from 'n8n-core'; import type { InstanceSettings } from 'n8n-core';
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow'; import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
@@ -27,7 +27,7 @@ describe('LogStreamingEventRelay', () => {
email: 'john@n8n.io', email: 'john@n8n.io',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'owner', role: { slug: 'owner' },
}, },
workflow: mock<IWorkflowBase>({ workflow: mock<IWorkflowBase>({
id: 'wf123', id: 'wf123',
@@ -61,7 +61,7 @@ describe('LogStreamingEventRelay', () => {
email: 'jane@n8n.io', email: 'jane@n8n.io',
firstName: 'Jane', firstName: 'Jane',
lastName: 'Smith', lastName: 'Smith',
role: 'user', role: { slug: 'user' },
}, },
workflowId: 'wf789', workflowId: 'wf789',
publicApi: false, publicApi: false,
@@ -89,7 +89,7 @@ describe('LogStreamingEventRelay', () => {
email: 'jane@n8n.io', email: 'jane@n8n.io',
firstName: 'Jane', firstName: 'Jane',
lastName: 'Smith', lastName: 'Smith',
role: 'user', role: { slug: 'user' },
}, },
workflowId: 'wf789', workflowId: 'wf789',
publicApi: false, publicApi: false,
@@ -117,7 +117,7 @@ describe('LogStreamingEventRelay', () => {
email: 'jane@n8n.io', email: 'jane@n8n.io',
firstName: 'Jane', firstName: 'Jane',
lastName: 'Smith', lastName: 'Smith',
role: 'user', role: { slug: 'user' },
}, },
workflowId: 'wf789', workflowId: 'wf789',
publicApi: false, publicApi: false,
@@ -145,7 +145,7 @@ describe('LogStreamingEventRelay', () => {
email: 'alex@n8n.io', email: 'alex@n8n.io',
firstName: 'Alex', firstName: 'Alex',
lastName: 'Johnson', lastName: 'Johnson',
role: 'editor', role: { slug: 'editor' },
}, },
workflow: mock<IWorkflowDb>({ id: 'wf101', name: 'Updated Workflow' }), workflow: mock<IWorkflowDb>({ id: 'wf101', name: 'Updated Workflow' }),
publicApi: false, publicApi: false,
@@ -347,7 +347,7 @@ describe('LogStreamingEventRelay', () => {
email: 'updated@example.com', email: 'updated@example.com',
firstName: 'Updated', firstName: 'Updated',
lastName: 'User', lastName: 'User',
role: 'global:member', role: { slug: 'global:member' },
}, },
fieldsChanged: ['firstName', 'lastName', 'password'], fieldsChanged: ['firstName', 'lastName', 'password'],
}; };
@@ -374,7 +374,7 @@ describe('LogStreamingEventRelay', () => {
email: 'john@n8n.io', email: 'john@n8n.io',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'some-role', role: { slug: 'some-role' },
}, },
targetUserOldStatus: 'active', targetUserOldStatus: 'active',
publicApi: false, publicApi: false,
@@ -404,7 +404,7 @@ describe('LogStreamingEventRelay', () => {
email: 'inviter@example.com', email: 'inviter@example.com',
firstName: 'Inviter', firstName: 'Inviter',
lastName: 'User', lastName: 'User',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
targetUserId: ['newUser123'], targetUserId: ['newUser123'],
publicApi: false, publicApi: false,
@@ -434,7 +434,7 @@ describe('LogStreamingEventRelay', () => {
email: 'reinviter@example.com', email: 'reinviter@example.com',
firstName: 'Reinviter', firstName: 'Reinviter',
lastName: 'User', lastName: 'User',
role: 'global:admin', role: { slug: 'global:admin' },
}, },
targetUserId: ['existingUser456'], targetUserId: ['existingUser456'],
}; };
@@ -461,7 +461,7 @@ describe('LogStreamingEventRelay', () => {
email: 'newuser@example.com', email: 'newuser@example.com',
firstName: 'New', firstName: 'New',
lastName: 'User', lastName: 'User',
role: 'global:member', role: { slug: 'global:member' },
}, },
userType: 'email', userType: 'email',
wasDisabledLdapUser: false, wasDisabledLdapUser: false,
@@ -488,7 +488,7 @@ describe('LogStreamingEventRelay', () => {
email: 'loggedin@example.com', email: 'loggedin@example.com',
firstName: 'Logged', firstName: 'Logged',
lastName: 'In', lastName: 'In',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
authenticationMethod: 'email', authenticationMethod: 'email',
}; };
@@ -517,7 +517,7 @@ describe('LogStreamingEventRelay', () => {
email: 'user101@example.com', email: 'user101@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:member', role: { slug: 'global:member' },
}, },
}; };
@@ -542,14 +542,14 @@ describe('LogStreamingEventRelay', () => {
email: 'john@n8n.io', email: 'john@n8n.io',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'some-role', role: { slug: 'some-role' },
}, },
invitee: { invitee: {
id: '456', id: '456',
email: 'jane@n8n.io', email: 'jane@n8n.io',
firstName: 'Jane', firstName: 'Jane',
lastName: 'Doe', lastName: 'Doe',
role: 'some-other-role', role: { slug: 'some-other-role' },
}, },
}; };
@@ -583,7 +583,7 @@ describe('LogStreamingEventRelay', () => {
email: 'resetuser@example.com', email: 'resetuser@example.com',
firstName: 'Reset', firstName: 'Reset',
lastName: 'User', lastName: 'User',
role: 'global:member', role: { slug: 'global:member' },
}, },
}; };
@@ -708,7 +708,7 @@ describe('LogStreamingEventRelay', () => {
email: 'sharer@example.com', email: 'sharer@example.com',
firstName: 'Alice', firstName: 'Alice',
lastName: 'Sharer', lastName: 'Sharer',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialId: 'cred789', credentialId: 'cred789',
credentialType: 'githubApi', credentialType: 'githubApi',
@@ -743,7 +743,7 @@ describe('LogStreamingEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'Test', firstName: 'Test',
lastName: 'User', lastName: 'User',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialType: 'githubApi', credentialType: 'githubApi',
credentialId: 'cred456', credentialId: 'cred456',
@@ -778,7 +778,7 @@ describe('LogStreamingEventRelay', () => {
email: 'creduser@example.com', email: 'creduser@example.com',
firstName: 'Cred', firstName: 'Cred',
lastName: 'User', lastName: 'User',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialId: 'cred789', credentialId: 'cred789',
credentialType: 'githubApi', credentialType: 'githubApi',
@@ -807,7 +807,7 @@ describe('LogStreamingEventRelay', () => {
email: 'updatecred@example.com', email: 'updatecred@example.com',
firstName: 'Update', firstName: 'Update',
lastName: 'Cred', lastName: 'Cred',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialId: 'cred101', credentialId: 'cred101',
credentialType: 'slackApi', credentialType: 'slackApi',
@@ -859,7 +859,7 @@ describe('LogStreamingEventRelay', () => {
email: 'packageupdater@example.com', email: 'packageupdater@example.com',
firstName: 'Package', firstName: 'Package',
lastName: 'Updater', lastName: 'Updater',
role: 'global:admin', role: { slug: 'global:admin' },
}, },
packageName: 'n8n-nodes-awesome-package', packageName: 'n8n-nodes-awesome-package',
packageVersionCurrent: '1.0.0', packageVersionCurrent: '1.0.0',
@@ -896,7 +896,7 @@ describe('LogStreamingEventRelay', () => {
email: 'admin@example.com', email: 'admin@example.com',
firstName: 'Admin', firstName: 'Admin',
lastName: 'User', lastName: 'User',
role: 'global:admin', role: { slug: 'global:admin' },
}, },
inputString: 'n8n-nodes-custom-package', inputString: 'n8n-nodes-custom-package',
packageName: 'n8n-nodes-custom-package', packageName: 'n8n-nodes-custom-package',
@@ -935,7 +935,7 @@ describe('LogStreamingEventRelay', () => {
email: 'packagedeleter@example.com', email: 'packagedeleter@example.com',
firstName: 'Package', firstName: 'Package',
lastName: 'Deleter', lastName: 'Deleter',
role: 'global:admin', role: { slug: 'global:admin' },
}, },
packageName: 'n8n-nodes-awesome-package', packageName: 'n8n-nodes-awesome-package',
packageVersion: '1.0.0', packageVersion: '1.0.0',
@@ -972,7 +972,7 @@ describe('LogStreamingEventRelay', () => {
email: 'recipient@example.com', email: 'recipient@example.com',
firstName: 'Failed', firstName: 'Failed',
lastName: 'Recipient', lastName: 'Recipient',
role: 'global:member', role: { slug: 'global:member' },
}, },
messageType: 'New user invite', messageType: 'New user invite',
publicApi: false, publicApi: false,
@@ -1002,7 +1002,7 @@ describe('LogStreamingEventRelay', () => {
email: 'apiuser@example.com', email: 'apiuser@example.com',
firstName: 'API', firstName: 'API',
lastName: 'User', lastName: 'User',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
publicApi: true, publicApi: true,
}; };
@@ -1028,7 +1028,7 @@ describe('LogStreamingEventRelay', () => {
email: 'apiuser@example.com', email: 'apiuser@example.com',
firstName: 'API', firstName: 'API',
lastName: 'User', lastName: 'User',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
publicApi: true, publicApi: true,
}; };

View File

@@ -1,14 +1,15 @@
import type { NodeTypes } from '@/node-types'; import type { NodeTypes } from '@/node-types';
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import type { GlobalConfig } from '@n8n/config'; import type { GlobalConfig } from '@n8n/config';
import type { import {
CredentialsEntity, type CredentialsEntity,
WorkflowEntity, type WorkflowEntity,
IWorkflowDb, type IWorkflowDb,
CredentialsRepository, type CredentialsRepository,
ProjectRelationRepository, type ProjectRelationRepository,
SharedWorkflowRepository, type SharedWorkflowRepository,
WorkflowRepository, type WorkflowRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { type BinaryDataConfig, InstanceSettings } from 'n8n-core'; import { type BinaryDataConfig, InstanceSettings } from 'n8n-core';
@@ -381,7 +382,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
publicApi: true, publicApi: true,
}; };
@@ -401,7 +402,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
publicApi: true, publicApi: true,
}; };
@@ -423,7 +424,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
inputString: 'n8n-nodes-package', inputString: 'n8n-nodes-package',
packageName: 'n8n-nodes-package', packageName: 'n8n-nodes-package',
@@ -456,7 +457,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
packageName: 'n8n-nodes-package', packageName: 'n8n-nodes-package',
packageVersionCurrent: '1.0.0', packageVersionCurrent: '1.0.0',
@@ -486,7 +487,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
packageName: 'n8n-nodes-package', packageName: 'n8n-nodes-package',
packageVersion: '1.0.0', packageVersion: '1.0.0',
@@ -516,7 +517,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialType: 'github', credentialType: 'github',
credentialId: 'cred123', credentialId: 'cred123',
@@ -543,7 +544,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialType: 'github', credentialType: 'github',
credentialId: 'cred123', credentialId: 'cred123',
@@ -571,7 +572,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialId: 'cred123', credentialId: 'cred123',
credentialType: 'github', credentialType: 'github',
@@ -593,7 +594,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
credentialId: 'cred123', credentialId: 'cred123',
credentialType: 'github', credentialType: 'github',
@@ -689,7 +690,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
workflow: mock<IWorkflowBase>({ id: 'workflow123', name: 'Test Workflow', nodes: [] }), workflow: mock<IWorkflowBase>({ id: 'workflow123', name: 'Test Workflow', nodes: [] }),
publicApi: false, publicApi: false,
@@ -718,7 +719,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
workflowId: 'workflow123', workflowId: 'workflow123',
publicApi: false, publicApi: false,
@@ -740,7 +741,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
workflowId: 'workflow123', workflowId: 'workflow123',
publicApi: false, publicApi: false,
@@ -762,7 +763,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
workflowId: 'workflow123', workflowId: 'workflow123',
publicApi: false, publicApi: false,
@@ -808,7 +809,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
workflow: mock<IWorkflowDb>({ id: 'workflow123', name: 'Test Workflow', nodes: [] }), workflow: mock<IWorkflowDb>({ id: 'workflow123', name: 'Test Workflow', nodes: [] }),
publicApi: false, publicApi: false,
@@ -856,7 +857,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
fieldsChanged: ['firstName', 'lastName'], fieldsChanged: ['firstName', 'lastName'],
}; };
@@ -876,7 +877,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
publicApi: false, publicApi: false,
targetUserOldStatus: 'active', targetUserOldStatus: 'active',
@@ -904,7 +905,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
targetUserId: ['user456'], targetUserId: ['user456'],
publicApi: false, publicApi: false,
@@ -930,7 +931,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
userType: 'email', userType: 'email',
wasDisabledLdapUser: false, wasDisabledLdapUser: false,
@@ -1148,7 +1149,7 @@ describe('TelemetryEventRelay', () => {
email: 'user@example.com', email: 'user@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
role: 'global:owner', role: { slug: GLOBAL_OWNER_ROLE.slug },
}, },
messageType: 'New user invite', messageType: 'New user invite',
publicApi: false, publicApi: false,

View File

@@ -1,6 +1,5 @@
import type { AuthenticationMethod, ProjectRelation } from '@n8n/api-types'; import type { AuthenticationMethod, ProjectRelation } from '@n8n/api-types';
import type { AuthProviderType, User, IWorkflowDb } from '@n8n/db'; import type { AuthProviderType, User, IWorkflowDb } from '@n8n/db';
import type { GlobalRole } from '@n8n/permissions';
import type { import type {
IPersonalizationSurveyAnswersV4, IPersonalizationSurveyAnswersV4,
IRun, IRun,
@@ -17,7 +16,9 @@ export type UserLike = {
email?: string; email?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
role: string; role: {
slug: string;
};
}; };
export type RelayEventMap = { export type RelayEventMap = {
@@ -368,14 +369,14 @@ export type RelayEventMap = {
'team-project-updated': { 'team-project-updated': {
userId: string; userId: string;
role: GlobalRole; role: string;
members: ProjectRelation[]; members: ProjectRelation[];
projectId: string; projectId: string;
}; };
'team-project-deleted': { 'team-project-deleted': {
userId: string; userId: string;
role: GlobalRole; role: string;
projectId: string; projectId: string;
removalType: 'transfer' | 'delete'; removalType: 'transfer' | 'delete';
targetProjectId?: string; targetProjectId?: string;
@@ -383,7 +384,7 @@ export type RelayEventMap = {
'team-project-created': { 'team-project-created': {
userId: string; userId: string;
role: GlobalRole; role: string;
}; };
// #endregion // #endregion

View File

@@ -1,4 +1,9 @@
import type { Project, User, SharedCredentialsRepository } from '@n8n/db'; import {
type Project,
type User,
type SharedCredentialsRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { INode } from 'n8n-workflow'; import type { INode } from 'n8n-workflow';
@@ -93,7 +98,7 @@ describe('CredentialsPermissionChecker', () => {
}); });
it('should skip credential checks if the home project owner has global scope', async () => { it('should skip credential checks if the home project owner has global scope', async () => {
const projectOwner = mock<User>({ role: 'global:owner' }); const projectOwner = mock<User>({ role: GLOBAL_OWNER_ROLE });
ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner); ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner);
await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow(); await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow();

View File

@@ -7,6 +7,7 @@ import {
AuthIdentityRepository, AuthIdentityRepository,
AuthProviderSyncHistoryRepository, AuthProviderSyncHistoryRepository,
UserRepository, UserRepository,
GLOBAL_MEMBER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { validate } from 'jsonschema'; import { validate } from 'jsonschema';
@@ -91,6 +92,22 @@ export const getAuthIdentityByLdapId = async (
}); });
}; };
/**
* Retrieve user by LDAP ID from database
* @param idAttributeValue - LDAP ID value
*/
export const getUserByLdapId = async (idAttributeValue: string) => {
return await Container.get(UserRepository).findOne({
relations: { role: true },
where: {
authIdentities: {
providerId: idAttributeValue,
providerType: 'ldap',
},
},
});
};
export const getUserByEmail = async (email: string): Promise<User | null> => { export const getUserByEmail = async (email: string): Promise<User | null> => {
return await Container.get(UserRepository).findOne({ return await Container.get(UserRepository).findOne({
where: { email }, where: { email },
@@ -150,7 +167,7 @@ export const mapLdapUserToDbUser = (
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig); const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
Object.assign(user, data); Object.assign(user, data);
if (toCreate) { if (toCreate) {
user.role = 'global:member'; user.role = GLOBAL_MEMBER_ROLE;
user.password = randomString(8); user.password = randomString(8);
user.disabled = false; user.disabled = false;
} else { } else {
@@ -270,7 +287,7 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => { export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => {
const { user } = await Container.get(UserRepository).createUserWithProject({ const { user } = await Container.get(UserRepository).createUserWithProject({
password: randomString(8), password: randomString(8),
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
...data, ...data,
}); });
await createLdapAuthIdentity(user, ldapId); await createLdapAuthIdentity(user, ldapId);

View File

@@ -37,6 +37,7 @@ import {
resolveEntryBinaryAttributes, resolveEntryBinaryAttributes,
saveLdapSynchronization, saveLdapSynchronization,
validateLdapConfigurationSchema, validateLdapConfigurationSchema,
getUserByLdapId,
} from '@/ldap.ee/helpers.ee'; } from '@/ldap.ee/helpers.ee';
import { import {
getCurrentAuthenticationMethod, getCurrentAuthenticationMethod,
@@ -504,6 +505,6 @@ export class LdapService {
} }
// Retrieve the user again as user's data might have been updated // Retrieve the user again as user's data might have been updated
return (await getAuthIdentityByLdapId(ldapId))?.user; return (await getUserByLdapId(ldapId)) ?? undefined;
} }
} }

View File

@@ -122,7 +122,10 @@ export class MfaService {
} }
async enableMfa(userId: string) { async enableMfa(userId: string) {
const user = await this.userRepository.findOneByOrFail({ id: userId }); const user = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['role'],
});
user.mfaEnabled = true; user.mfaEnabled = true;
return await this.userRepository.save(user); return await this.userRepository.save(user);
} }

View File

@@ -1,5 +1,11 @@
import { createTeamProject, testDb, testModules } from '@n8n/backend-test-utils'; import { createTeamProject, testDb, testModules } from '@n8n/backend-test-utils';
import { ProjectRelationRepository, type Project, type User } from '@n8n/db'; import {
GLOBAL_MEMBER_ROLE,
GLOBAL_OWNER_ROLE,
ProjectRelationRepository,
type Project,
type User,
} from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import type { EntityManager } from '@n8n/typeorm'; import type { EntityManager } from '@n8n/typeorm';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -41,7 +47,7 @@ describe('dataStoreAggregate', () => {
beforeEach(async () => { beforeEach(async () => {
project1 = await createTeamProject(); project1 = await createTeamProject();
project2 = await createTeamProject(); project2 = await createTeamProject();
user = await createUser({ role: 'global:owner' }); user = await createUser({ role: GLOBAL_OWNER_ROLE });
}); });
afterEach(async () => { afterEach(async () => {
@@ -108,7 +114,7 @@ describe('dataStoreAggregate', () => {
it('should return an empty array if user has no access to any project', async () => { it('should return an empty array if user has no access to any project', async () => {
// ARRANGE // ARRANGE
const currentUser = await createUser({ role: 'global:member' }); const currentUser = await createUser({ role: GLOBAL_MEMBER_ROLE });
await dataStoreService.createDataStore(project1.id, { await dataStoreService.createDataStore(project1.id, {
name: 'store1', name: 'store1',

View File

@@ -1,4 +1,5 @@
import { import {
GLOBAL_MEMBER_ROLE,
ProjectRepository, ProjectRepository,
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
@@ -62,7 +63,7 @@ describe('userHasScopes', () => {
findByWorkflowMock.mockResolvedValueOnce([]); findByWorkflowMock.mockResolvedValueOnce([]);
findByCredentialMock.mockResolvedValueOnce([]); findByCredentialMock.mockResolvedValueOnce([]);
const user = { id: 'userId', scopes: [], role: 'global:member' } as unknown as User; const user = { id: 'userId', scopes: [], role: GLOBAL_MEMBER_ROLE } as unknown as User;
const scopes = ['workflow:read', 'credential:read'] as Scope[]; const scopes = ['workflow:read', 'credential:read'] as Scope[];
const params: { credentialId?: string; workflowId?: string; projectId?: string } = { const params: { credentialId?: string; workflowId?: string; projectId?: string } = {
@@ -140,7 +141,7 @@ describe('userHasScopes', () => {
const user = { const user = {
id: 'userId', id: 'userId',
scopes: userScopes, scopes: userScopes,
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
} as unknown as User; } as unknown as User;
const scopes = [scope] as Scope[]; const scopes = [scope] as Scope[];
const params: { credentialId?: string; workflowId?: string; projectId?: string } = { const params: { credentialId?: string; workflowId?: string; projectId?: string } = {

View File

@@ -72,7 +72,7 @@ export = {
const { id: credentialId } = req.params; const { id: credentialId } = req.params;
let credential: CredentialsEntity | undefined; let credential: CredentialsEntity | undefined;
if (!['global:owner', 'global:admin'].includes(req.user.role)) { if (!['global:owner', 'global:admin'].includes(req.user.role.slug)) {
const shared = await getSharedCredentials(req.user.id, credentialId); const shared = await getSharedCredentials(req.user.id, credentialId);
if (shared?.role === 'credential:owner') { if (shared?.role === 'credential:owner') {

View File

@@ -157,7 +157,7 @@ export = {
...(name !== undefined && { name: Like('%' + name.trim() + '%') }), ...(name !== undefined && { name: Like('%' + name.trim() + '%') }),
}; };
if (['global:owner', 'global:admin'].includes(req.user.role)) { if (['global:owner', 'global:admin'].includes(req.user.role.slug)) {
if (tags) { if (tags) {
const workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags( const workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags(
parseTagNames(tags), parseTagNames(tags),

View File

@@ -44,7 +44,7 @@ export async function getSharedWorkflow(
): Promise<SharedWorkflow | null> { ): Promise<SharedWorkflow | null> {
return await Container.get(SharedWorkflowRepository).findOne({ return await Container.get(SharedWorkflowRepository).findOne({
where: { where: {
...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }), ...(!['global:owner', 'global:admin'].includes(user.role.slug) && { userId: user.id }),
...(workflowId && { workflowId }), ...(workflowId && { workflowId }),
}, },
relations: [ relations: [

View File

@@ -8,7 +8,7 @@ import type {
ListQueryDb, ListQueryDb,
WorkflowHistory, WorkflowHistory,
} from '@n8n/db'; } from '@n8n/db';
import type { AssignableGlobalRole, GlobalRole, ProjectRole, Scope } from '@n8n/permissions'; import type { AssignableGlobalRole, ProjectRole, Scope } from '@n8n/permissions';
import type { import type {
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
INodeCredentialTestRequest, INodeCredentialTestRequest,
@@ -268,9 +268,7 @@ export declare namespace ActiveWorkflowRequest {
// ---------------------------------- // ----------------------------------
export declare namespace ProjectRequest { export declare namespace ProjectRequest {
type GetMyProjectsResponse = Array< type GetMyProjectsResponse = Array<Project & { role: string; scopes?: Scope[] }>;
Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }
>;
type ProjectRelationResponse = { type ProjectRelationResponse = {
id: string; id: string;

View File

@@ -1,4 +1,4 @@
import { WorkflowEntity } from '@n8n/db'; import { GLOBAL_ADMIN_ROLE, GLOBAL_MEMBER_ROLE, WorkflowEntity } from '@n8n/db';
import type { User, SharedWorkflowRepository, WorkflowRepository } from '@n8n/db'; import type { User, SharedWorkflowRepository, WorkflowRepository } from '@n8n/db';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -41,7 +41,7 @@ describe('ActiveWorkflowsService', () => {
}); });
it('should return all workflow ids when user has full access', async () => { it('should return all workflow ids when user has full access', async () => {
user.role = 'global:admin'; user.role = GLOBAL_ADMIN_ROLE;
const ids = await service.getAllActiveIdsFor(user); const ids = await service.getAllActiveIdsFor(user);
expect(ids).toEqual(['2', '3', '4']); expect(ids).toEqual(['2', '3', '4']);
@@ -49,7 +49,7 @@ describe('ActiveWorkflowsService', () => {
}); });
it('should filter out workflow ids that the user does not have access to', async () => { it('should filter out workflow ids that the user does not have access to', async () => {
user.role = 'global:member'; user.role = GLOBAL_MEMBER_ROLE;
sharedWorkflowRepository.getSharedWorkflowIds.mockResolvedValue(['3']); sharedWorkflowRepository.getSharedWorkflowIds.mockResolvedValue(['3']);
const ids = await service.getAllActiveIdsFor(user); const ids = await service.getAllActiveIdsFor(user);

View File

@@ -1,4 +1,4 @@
import { SharedCredentials } from '@n8n/db'; import { GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, SharedCredentials } from '@n8n/db';
import type { CredentialsEntity, User } from '@n8n/db'; import type { CredentialsEntity, User } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { In } from '@n8n/typeorm'; import { In } from '@n8n/typeorm';
@@ -16,10 +16,10 @@ describe('CredentialsFinderService', () => {
const sharedCredential = mock<SharedCredentials>(); const sharedCredential = mock<SharedCredentials>();
sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId }); sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId });
const owner = mock<User>({ const owner = mock<User>({
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
const member = mock<User>({ const member = mock<User>({
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
id: 'test', id: 'test',
}); });

View File

@@ -9,6 +9,7 @@ import {
ProjectRelationRepository, ProjectRelationRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
UserRepository, UserRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@@ -218,7 +219,7 @@ describe('OwnershipService', () => {
await ownershipService.getInstanceOwner(); await ownershipService.getInstanceOwner();
expect(userRepository.findOneOrFail).toHaveBeenCalledWith({ expect(userRepository.findOneOrFail).toHaveBeenCalledWith({
where: { role: 'global:owner' }, where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
}); });
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { testDb } from '@n8n/backend-test-utils'; import { testDb } from '@n8n/backend-test-utils';
import type { AuthenticatedRequest } from '@n8n/db'; import type { AuthenticatedRequest } from '@n8n/db';
import { ApiKeyRepository, UserRepository } from '@n8n/db'; import { ApiKeyRepository, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, UserRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { getOwnerOnlyApiKeyScopes, type ApiKeyScope } from '@n8n/permissions'; import { getOwnerOnlyApiKeyScopes, type ApiKeyScope } from '@n8n/permissions';
import type { Response, NextFunction } from 'express'; import type { Response, NextFunction } from 'express';
@@ -427,7 +427,9 @@ describe('PublicApiKeyService', () => {
// Act // Act
const result = publicApiKeyService.apiKeyHasValidScopesForRole( const result = publicApiKeyService.apiKeyHasValidScopesForRole(
'global:owner', {
role: GLOBAL_OWNER_ROLE,
},
ownerOnlyScopes, ownerOnlyScopes,
); );
@@ -443,7 +445,9 @@ describe('PublicApiKeyService', () => {
// Act // Act
const result = publicApiKeyService.apiKeyHasValidScopesForRole( const result = publicApiKeyService.apiKeyHasValidScopesForRole(
'global:member', {
role: GLOBAL_MEMBER_ROLE,
},
ownerOnlyScopes, ownerOnlyScopes,
); );

View File

@@ -1,6 +1,6 @@
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { User, UserRepository } from '@n8n/db'; import { GLOBAL_MEMBER_ROLE, User, UserRepository } from '@n8n/db';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@@ -23,6 +23,7 @@ describe('UserService', () => {
const commonMockUser = Object.assign(new User(), { const commonMockUser = Object.assign(new User(), {
id: uuid(), id: uuid(),
password: 'passwordHash', password: 'passwordHash',
role: GLOBAL_MEMBER_ROLE,
}); });
describe('toPublic', () => { describe('toPublic', () => {
@@ -35,6 +36,7 @@ describe('UserService', () => {
mfaRecoveryCodes: ['test'], mfaRecoveryCodes: ['test'],
updatedAt: new Date(), updatedAt: new Date(),
authIdentities: [], authIdentities: [],
role: GLOBAL_MEMBER_ROLE,
}); });
type MaybeSensitiveProperties = Partial< type MaybeSensitiveProperties = Partial<
@@ -55,13 +57,17 @@ describe('UserService', () => {
const scoped = await userService.toPublic(commonMockUser, { withScopes: true }); const scoped = await userService.toPublic(commonMockUser, { withScopes: true });
const unscoped = await userService.toPublic(commonMockUser); const unscoped = await userService.toPublic(commonMockUser);
expect(scoped.globalScopes).toEqual([]); expect(scoped.globalScopes).toEqual(GLOBAL_MEMBER_ROLE.scopes.map((s) => s.slug));
expect(unscoped.globalScopes).toBeUndefined(); expect(unscoped.globalScopes).toBeUndefined();
}); });
it('should add invite URL if requested', async () => { it('should add invite URL if requested', async () => {
const firstUser = Object.assign(new User(), { id: uuid() }); const firstUser = Object.assign(new User(), { id: uuid(), role: GLOBAL_MEMBER_ROLE });
const secondUser = Object.assign(new User(), { id: uuid(), isPending: true }); const secondUser = Object.assign(new User(), {
id: uuid(),
role: GLOBAL_MEMBER_ROLE,
isPending: true,
});
const withoutUrl = await userService.toPublic(secondUser); const withoutUrl = await userService.toPublic(secondUser);
const withUrl = await userService.toPublic(secondUser, { const withUrl = await userService.toPublic(secondUser, {

View File

@@ -17,7 +17,7 @@ export class AccessService {
/** Whether a user has read access to a workflow based on their project and scope. */ /** Whether a user has read access to a workflow based on their project and scope. */
async hasReadAccess(userId: User['id'], workflowId: Workflow['id']) { async hasReadAccess(userId: User['id'], workflowId: Workflow['id']) {
const user = await this.userRepository.findOneBy({ id: userId }); const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['role'] });
if (!user) return false; if (!user) return false;

View File

@@ -1,5 +1,6 @@
import type { Project, User, ListQueryDb } from '@n8n/db'; import type { Project, User, ListQueryDb } from '@n8n/db';
import { import {
GLOBAL_OWNER_ROLE,
ProjectRelationRepository, ProjectRelationRepository,
ProjectRepository, ProjectRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
@@ -106,7 +107,7 @@ export class OwnershipService {
async getInstanceOwner() { async getInstanceOwner() {
return await this.userRepository.findOneOrFail({ return await this.userRepository.findOneOrFail({
where: { role: 'global:owner' }, where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
}); });
} }
} }

View File

@@ -2,7 +2,7 @@ import type { CreateApiKeyRequestDto, UnixTimestamp, UpdateApiKeyRequestDto } fr
import type { AuthenticatedRequest, User } from '@n8n/db'; import type { AuthenticatedRequest, User } from '@n8n/db';
import { ApiKey, ApiKeyRepository, UserRepository } from '@n8n/db'; import { ApiKey, ApiKeyRepository, UserRepository } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { GlobalRole, ApiKeyScope } from '@n8n/permissions'; import type { ApiKeyScope, AuthPrincipal } from '@n8n/permissions';
import { getApiKeyScopesForRole, getOwnerOnlyApiKeyScopes } from '@n8n/permissions'; import { getApiKeyScopesForRole, getOwnerOnlyApiKeyScopes } from '@n8n/permissions';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import type { EntityManager } from '@n8n/typeorm'; import type { EntityManager } from '@n8n/typeorm';
@@ -79,12 +79,14 @@ export class PublicApiKeyService {
} }
private async getUserForApiKey(apiKey: string) { private async getUserForApiKey(apiKey: string) {
return await this.userRepository return await this.userRepository.findOne({
.createQueryBuilder('user') where: {
.innerJoin(ApiKey, 'apiKey', 'apiKey.userId = user.id') apiKeys: {
.where('apiKey.apiKey = :apiKey', { apiKey }) apiKey,
.select('user') },
.getOne(); },
relations: ['role'],
});
} }
/** /**
@@ -161,7 +163,7 @@ export class PublicApiKeyService {
return decoded?.exp ?? null; return decoded?.exp ?? null;
}; };
apiKeyHasValidScopesForRole(role: GlobalRole, apiKeyScopes: ApiKeyScope[]) { apiKeyHasValidScopesForRole(role: AuthPrincipal, apiKeyScopes: ApiKeyScope[]) {
const scopesForRole = getApiKeyScopesForRole(role); const scopesForRole = getApiKeyScopesForRole(role);
return apiKeyScopes.every((scope) => scopesForRole.includes(scope)); return apiKeyScopes.every((scope) => scopesForRole.includes(scope));
} }

View File

@@ -9,7 +9,7 @@ import type {
} from '@n8n/db'; } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { AllRoleTypes, Scope } from '@n8n/permissions'; import type { AllRoleTypes, Scope } from '@n8n/permissions';
import { ALL_ROLES, combineScopes, getRoleScopes } from '@n8n/permissions'; import { ALL_ROLES, combineScopes, getAuthPrincipalScopes, getRoleScopes } from '@n8n/permissions';
import { UnexpectedError } from 'n8n-workflow'; import { UnexpectedError } from 'n8n-workflow';
import { License } from '@/license'; import { License } from '@/license';
@@ -89,7 +89,7 @@ export class RoleService {
shared: SharedCredentials[] | SharedWorkflow[], shared: SharedCredentials[] | SharedWorkflow[],
userProjectRelations: ProjectRelation[], userProjectRelations: ProjectRelation[],
): Scope[] { ): Scope[] {
const globalScopes = getRoleScopes(user.role, [type]); const globalScopes = getAuthPrincipalScopes(user, [type]);
const scopesSet: Set<Scope> = new Set(globalScopes); const scopesSet: Set<Scope> = new Set(globalScopes);
for (const sharedEntity of shared) { for (const sharedEntity of shared) {
const pr = userProjectRelations.find( const pr = userProjectRelations.find(

View File

@@ -64,14 +64,16 @@ export class UserService {
mfaAuthenticated?: boolean; mfaAuthenticated?: boolean;
}, },
) { ) {
const { password, updatedAt, authIdentities, mfaRecoveryCodes, mfaSecret, ...rest } = user; const { password, updatedAt, authIdentities, mfaRecoveryCodes, mfaSecret, role, ...rest } =
user;
const providerType = authIdentities?.[0]?.providerType; const providerType = authIdentities?.[0]?.providerType;
let publicUser: PublicUser = { let publicUser: PublicUser = {
...rest, ...rest,
role: role.slug,
signInType: providerType ?? 'email', signInType: providerType ?? 'email',
isOwner: user.role === 'global:owner', isOwner: user.role.slug === 'global:owner',
}; };
if (options?.withInviteUrl && !options?.inviterId) { if (options?.withInviteUrl && !options?.inviterId) {
@@ -214,7 +216,12 @@ export class UserService {
await Promise.all( await Promise.all(
toCreateUsers.map(async ({ email, role }) => { toCreateUsers.map(async ({ email, role }) => {
const { user: savedUser } = await this.userRepository.createUserWithProject( const { user: savedUser } = await this.userRepository.createUserWithProject(
{ email, role }, {
email,
role: {
slug: role,
},
},
transactionManager, transactionManager,
); );
createdUsers.set(email, savedUser.id); createdUsers.set(email, savedUser.id);
@@ -240,11 +247,11 @@ export class UserService {
async changeUserRole(user: User, targetUser: User, newRole: RoleChangeRequestDto) { async changeUserRole(user: User, targetUser: User, newRole: RoleChangeRequestDto) {
return await this.userRepository.manager.transaction(async (trx) => { return await this.userRepository.manager.transaction(async (trx) => {
await trx.update(User, { id: targetUser.id }, { role: newRole.newRoleName }); await trx.update(User, { id: targetUser.id }, { role: { slug: newRole.newRoleName } });
const adminDowngradedToMember = const adminDowngradedToMember =
user.role === 'global:owner' && user.role.slug === 'global:owner' &&
targetUser.role === 'global:admin' && targetUser.role.slug === 'global:admin' &&
newRole.newRoleName === 'global:member'; newRole.newRoleName === 'global:member';
if (adminDowngradedToMember) { if (adminDowngradedToMember) {

View File

@@ -5,6 +5,7 @@ import {
AuthIdentity, AuthIdentity,
AuthIdentityRepository, AuthIdentityRepository,
isValidEmail, isValidEmail,
GLOBAL_MEMBER_ROLE,
SettingsRepository, SettingsRepository,
type User, type User,
UserRepository, UserRepository,
@@ -111,14 +112,21 @@ export class OidcService {
const openidUser = await this.authIdentityRepository.findOne({ const openidUser = await this.authIdentityRepository.findOne({
where: { providerId: claims.sub, providerType: 'oidc' }, where: { providerId: claims.sub, providerType: 'oidc' },
relations: ['user'], relations: {
user: {
role: true,
},
},
}); });
if (openidUser) { if (openidUser) {
return openidUser.user; return openidUser.user;
} }
const foundUser = await this.userRepository.findOneBy({ email: userInfo.email }); const foundUser = await this.userRepository.findOne({
where: { email: userInfo.email },
relations: ['authIdentities', 'role'],
});
if (foundUser) { if (foundUser) {
this.logger.debug( this.logger.debug(
@@ -143,7 +151,7 @@ export class OidcService {
lastName: userInfo.family_name, lastName: userInfo.family_name,
email: userInfo.email, email: userInfo.email,
authIdentities: [], authIdentities: [],
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
password: 'no password set', password: 'no password set',
}, },
trx, trx,

View File

@@ -1,4 +1,4 @@
import type { User } from '@n8n/db'; import { GLOBAL_MEMBER_ROLE, type User } from '@n8n/db';
import { type Request, type Response } from 'express'; import { type Request, type Response } from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -21,7 +21,7 @@ const user = mock<User>({
lastName: 'User', lastName: 'User',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
}); });
describe('OidcController', () => { describe('OidcController', () => {

View File

@@ -29,6 +29,7 @@ describe('sso/saml/samlHelpers', () => {
userPrincipalName: 'Huh?', userPrincipalName: 'Huh?',
}; };
userRepository.findOne.mockImplementationOnce(async (user) => user as User);
userRepository.save.mockImplementationOnce(async (user) => user as User); userRepository.save.mockImplementationOnce(async (user) => user as User);
// //

View File

@@ -1,4 +1,4 @@
import type { User } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
import { type Response } from 'express'; import { type Response } from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
@@ -30,7 +30,7 @@ const user = mock<User>({
id: '123', id: '123',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
const attributes: SamlUserAttributes = { const attributes: SamlUserAttributes = {

View File

@@ -79,7 +79,7 @@ export async function createUserFromSamlAttributes(attributes: SamlUserAttribute
email: attributes.email.toLowerCase(), email: attributes.email.toLowerCase(),
firstName: attributes.firstName, firstName: attributes.firstName,
lastName: attributes.lastName, lastName: attributes.lastName,
role: 'global:member', role: { slug: 'global:member' },
// generates a password that is not used or known to the user // generates a password that is not used or known to the user
password: await Container.get(PasswordUtility).hash(randomPassword), password: await Container.get(PasswordUtility).hash(randomPassword),
}, },
@@ -118,8 +118,14 @@ export async function updateUserFromSamlAttributes(
user.firstName = attributes.firstName; user.firstName = attributes.firstName;
user.lastName = attributes.lastName; user.lastName = attributes.lastName;
const resultUser = await Container.get(UserRepository).save(user, { transaction: false }); const resultUser = await Container.get(UserRepository).save(user, { transaction: false });
if (!resultUser) throw new AuthError('Could not create User'); if (!resultUser) throw new AuthError('Could not update User');
return resultUser; const userWithRole = await Container.get(UserRepository).findOne({
where: { id: resultUser.id },
relations: ['role'],
transaction: false,
});
if (!userWithRole) throw new AuthError('Failed to fetch user!');
return userWithRole;
} }
type GetMappedSamlReturn = { type GetMappedSamlReturn = {

View File

@@ -204,7 +204,7 @@ export class SamlService {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { email: lowerCasedEmail }, where: { email: lowerCasedEmail },
relations: ['authIdentities'], relations: ['authIdentities', 'role'],
}); });
if (user) { if (user) {
// Login path for existing users that are fully set up and that have a SAML authIdentity set up // Login path for existing users that are fully set up and that have a SAML authIdentity set up

View File

@@ -564,7 +564,7 @@ export class WorkflowsController {
@Post('/with-node-types') @Post('/with-node-types')
async getWorkflowsWithNodesIncluded(req: AuthenticatedRequest, res: express.Response) { async getWorkflowsWithNodesIncluded(req: AuthenticatedRequest, res: express.Response) {
try { try {
const hasPermission = req.user.role === ROLE.Owner || req.user.role === ROLE.Admin; const hasPermission = req.user.role.slug === ROLE.Owner || req.user.role.slug === ROLE.Admin;
if (!hasPermission) { if (!hasPermission) {
res.json({ data: [], count: 0 }); res.json({ data: [], count: 0 });

View File

@@ -2,7 +2,7 @@ import type { ApiKeyWithRawValue } from '@n8n/api-types';
import { testDb, randomValidPassword, mockInstance } from '@n8n/backend-test-utils'; import { testDb, randomValidPassword, mockInstance } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import type { User } from '@n8n/db'; import type { User } from '@n8n/db';
import { ApiKeyRepository } from '@n8n/db'; import { ApiKeyRepository, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { import {
getApiKeyScopesForRole, getApiKeyScopesForRole,
@@ -59,7 +59,7 @@ describe('Owner shell', () => {
let ownerShell: User; let ownerShell: User;
beforeEach(async () => { beforeEach(async () => {
ownerShell = await createUserShell('global:owner'); ownerShell = await createUserShell(GLOBAL_OWNER_ROLE);
}); });
test('POST /api-keys should create an api key with no expiration', async () => { test('POST /api-keys should create an api key with no expiration', async () => {
@@ -304,9 +304,9 @@ describe('Owner shell', () => {
const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[]; const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[];
const scopesForRole = getApiKeyScopesForRole(ownerShell.role); const scopesForRole = getApiKeyScopesForRole(ownerShell);
expect(scopes).toEqual(scopesForRole); expect(scopes.sort()).toEqual(scopesForRole.sort());
}); });
}); });
@@ -317,7 +317,7 @@ describe('Member', () => {
beforeEach(async () => { beforeEach(async () => {
member = await createUser({ member = await createUser({
password: memberPassword, password: memberPassword,
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
}); });
await utils.setInstanceOwnerSetUp(true); await utils.setInstanceOwnerSetUp(true);
}); });
@@ -328,6 +328,7 @@ describe('Member', () => {
.post('/api-keys') .post('/api-keys')
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] }); .send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
console.log(newApiKeyResponse.body);
expect(newApiKeyResponse.statusCode).toBe(200); expect(newApiKeyResponse.statusCode).toBe(200);
expect(newApiKeyResponse.body.data.apiKey).toBeDefined(); expect(newApiKeyResponse.body.data.apiKey).toBeDefined();
expect(newApiKeyResponse.body.data.apiKey).not.toBeNull(); expect(newApiKeyResponse.body.data.apiKey).not.toBeNull();
@@ -492,8 +493,8 @@ describe('Member', () => {
const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[]; const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[];
const scopesForRole = getApiKeyScopesForRole(member.role); const scopesForRole = getApiKeyScopesForRole(member);
expect(scopes).toEqual(scopesForRole); expect(scopes.sort()).toEqual(scopesForRole.sort());
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { randomValidPassword, testDb } from '@n8n/backend-test-utils'; import { randomValidPassword, testDb } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db'; import type { User } from '@n8n/db';
import { UserRepository } from '@n8n/db'; import { GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, UserRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import validator from 'validator'; import validator from 'validator';
@@ -36,7 +36,7 @@ describe('POST /login', () => {
beforeEach(async () => { beforeEach(async () => {
owner = await createUser({ owner = await createUser({
password: ownerPassword, password: ownerPassword,
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
}); });
@@ -140,7 +140,7 @@ describe('POST /login', () => {
license.setQuota('quota:users', 0); license.setQuota('quota:users', 0);
const ownerUser = await createUser({ const ownerUser = await createUser({
password: randomValidPassword(), password: randomValidPassword(),
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
const response = await testServer.authAgentFor(ownerUser).get('/login'); const response = await testServer.authAgentFor(ownerUser).get('/login');
@@ -182,7 +182,7 @@ describe('GET /login', () => {
}); });
test('should return logged-in owner shell', async () => { test('should return logged-in owner shell', async () => {
const ownerShell = await createUserShell('global:owner'); const ownerShell = await createUserShell(GLOBAL_OWNER_ROLE);
const response = await testServer.authAgentFor(ownerShell).get('/login'); const response = await testServer.authAgentFor(ownerShell).get('/login');
@@ -217,7 +217,7 @@ describe('GET /login', () => {
}); });
test('should return logged-in member shell', async () => { test('should return logged-in member shell', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const response = await testServer.authAgentFor(memberShell).get('/login'); const response = await testServer.authAgentFor(memberShell).get('/login');
@@ -252,7 +252,7 @@ describe('GET /login', () => {
}); });
test('should return logged-in owner', async () => { test('should return logged-in owner', async () => {
const owner = await createUser({ role: 'global:owner' }); const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
const response = await testServer.authAgentFor(owner).get('/login'); const response = await testServer.authAgentFor(owner).get('/login');
@@ -287,7 +287,7 @@ describe('GET /login', () => {
}); });
test('should return logged-in member', async () => { test('should return logged-in member', async () => {
const member = await createUser({ role: 'global:member' }); const member = await createUser({ role: { slug: 'global:member' } });
const response = await testServer.authAgentFor(member).get('/login'); const response = await testServer.authAgentFor(member).get('/login');
@@ -326,13 +326,13 @@ describe('GET /resolve-signup-token', () => {
beforeEach(async () => { beforeEach(async () => {
owner = await createUser({ owner = await createUser({
password: ownerPassword, password: ownerPassword,
role: 'global:owner', role: GLOBAL_OWNER_ROLE,
}); });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
}); });
test('should validate invite token', async () => { test('should validate invite token', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const response = await authOwnerAgent const response = await authOwnerAgent
.get('/resolve-signup-token') .get('/resolve-signup-token')
@@ -352,7 +352,7 @@ describe('GET /resolve-signup-token', () => {
test('should return 403 if user quota reached', async () => { test('should return 403 if user quota reached', async () => {
license.setQuota('quota:users', 0); license.setQuota('quota:users', 0);
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const response = await authOwnerAgent const response = await authOwnerAgent
.get('/resolve-signup-token') .get('/resolve-signup-token')
@@ -363,7 +363,7 @@ describe('GET /resolve-signup-token', () => {
}); });
test('should fail with invalid inputs', async () => { test('should fail with invalid inputs', async () => {
const { id: inviteeId } = await createUser({ role: 'global:member' }); const { id: inviteeId } = await createUser({ role: { slug: 'global:member' } });
const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id }); const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id });
@@ -396,7 +396,7 @@ describe('GET /resolve-signup-token', () => {
describe('POST /logout', () => { describe('POST /logout', () => {
test('should log user out', async () => { test('should log user out', async () => {
const owner = await createUser({ role: 'global:owner' }); const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
const ownerAgent = testServer.authAgentFor(owner); const ownerAgent = testServer.authAgentFor(owner);
// @ts-expect-error `accessInfo` types are incorrect // @ts-expect-error `accessInfo` types are incorrect
const cookie = ownerAgent.jar.getCookie(AUTH_COOKIE_NAME, { path: '/' }); const cookie = ownerAgent.jar.getCookie(AUTH_COOKIE_NAME, { path: '/' });

View File

@@ -40,7 +40,7 @@ describe('Auth Middleware', () => {
describe('Routes requiring Authorization', () => { describe('Routes requiring Authorization', () => {
let authMemberAgent: SuperAgentTest; let authMemberAgent: SuperAgentTest;
beforeAll(async () => { beforeAll(async () => {
const member = await createUser({ role: 'global:member' }); const member = await createUser({ role: { slug: 'global:member' } });
authMemberAgent = testServer.authAgentFor(member); authMemberAgent = testServer.authAgentFor(member);
}); });

View File

@@ -61,7 +61,7 @@ describe('--deleteWorkflowsAndCredentials', () => {
// //
// ARRANGE // ARRANGE
// //
const member = await createLdapUser({ role: 'global:member' }, uuid()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const memberProject = await getPersonalProject(member); const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member); const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), { const credential = await saveCredential(randomCredentialPayload(), {
@@ -166,7 +166,7 @@ describe('--userId', () => {
// //
// ARRANGE // ARRANGE
// //
const member = await createLdapUser({ role: 'global:member' }, uuid()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
await expect(command.run([`--userId=${member.id}`])).rejects.toThrowError( await expect(command.run([`--userId=${member.id}`])).rejects.toThrowError(
`Can't migrate workflows and credentials to the user with the ID ${member.id}. That user was created via LDAP and will be deleted as well.`, `Can't migrate workflows and credentials to the user with the ID ${member.id}. That user was created via LDAP and will be deleted as well.`,
@@ -177,7 +177,7 @@ describe('--userId', () => {
// //
// ARRANGE // ARRANGE
// //
const member = await createLdapUser({ role: 'global:member' }, uuid()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const memberProject = await getPersonalProject(member); const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member); const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), { const credential = await saveCredential(randomCredentialPayload(), {
@@ -242,7 +242,7 @@ describe('--projectId', () => {
// //
// ARRANGE // ARRANGE
// //
const member = await createLdapUser({ role: 'global:member' }, uuid()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const memberProject = await getPersonalProject(member); const memberProject = await getPersonalProject(member);
await expect(command.run([`--projectId=${memberProject.id}`])).rejects.toThrowError( await expect(command.run([`--projectId=${memberProject.id}`])).rejects.toThrowError(
@@ -254,7 +254,7 @@ describe('--projectId', () => {
// //
// ARRANGE // ARRANGE
// //
const member = await createLdapUser({ role: 'global:member' }, uuid()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const memberProject = await getPersonalProject(member); const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member); const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), { const credential = await saveCredential(randomCredentialPayload(), {
@@ -310,7 +310,7 @@ describe('--projectId', () => {
// //
// ARRANGE // ARRANGE
// //
const member = await createLdapUser({ role: 'global:member' }, uuid()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const memberProject = await getPersonalProject(member); const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member); const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), { const credential = await saveCredential(randomCredentialPayload(), {

View File

@@ -12,6 +12,7 @@ import {
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
UserRepository, UserRepository,
GLOBAL_OWNER_ROLE,
} from '@n8n/db'; } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
@@ -35,7 +36,7 @@ test('user-management:reset should reset DB to default user state', async () =>
// //
// ARRANGE // ARRANGE
// //
const owner = await createUser({ role: 'global:owner' }); const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
const ownerProject = await getPersonalProject(owner); const ownerProject = await getPersonalProject(owner);
// should be deleted // should be deleted
@@ -70,7 +71,7 @@ test('user-management:reset should reset DB to default user state', async () =>
// check if the owner account was reset: // check if the owner account was reset:
await expect( await expect(
Container.get(UserRepository).findOneBy({ role: 'global:owner' }), Container.get(UserRepository).findOneBy({ role: { slug: GLOBAL_OWNER_ROLE.slug } }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
email: null, email: null,
firstName: null, firstName: null,
@@ -80,7 +81,9 @@ test('user-management:reset should reset DB to default user state', async () =>
}); });
// all members were deleted: // all members were deleted:
const members = await Container.get(UserRepository).findOneBy({ role: 'global:member' }); const members = await Container.get(UserRepository).findOneBy({
role: { slug: 'global:member' },
});
expect(members).toBeNull(); expect(members).toBeNull();
// all workflows are owned by the owner: // all workflows are owned by the owner:

View File

@@ -6,7 +6,12 @@ import {
randomValidPassword, randomValidPassword,
} from '@n8n/backend-test-utils'; } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db'; import type { User } from '@n8n/db';
import { ProjectRelationRepository, UserRepository } from '@n8n/db'; import {
GLOBAL_ADMIN_ROLE,
GLOBAL_MEMBER_ROLE,
ProjectRelationRepository,
UserRepository,
} from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Not } from '@n8n/typeorm'; import { Not } from '@n8n/typeorm';
@@ -52,7 +57,7 @@ describe('InvitationController', () => {
describe('POST /invitations/:id/accept', () => { describe('POST /invitations/:id/accept', () => {
test('should fill out a member shell', async () => { test('should fill out a member shell', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const memberProps = { const memberProps = {
inviterId: instanceOwner.id, inviterId: instanceOwner.id,
@@ -83,7 +88,7 @@ describe('InvitationController', () => {
}); });
test('should fill out an admin shell', async () => { test('should fill out an admin shell', async () => {
const adminShell = await createUserShell('global:admin'); const adminShell = await createUserShell(GLOBAL_ADMIN_ROLE);
const memberProps = { const memberProps = {
inviterId: instanceOwner.id, inviterId: instanceOwner.id,
@@ -116,7 +121,7 @@ describe('InvitationController', () => {
test('should fail with invalid payloads', async () => { test('should fail with invalid payloads', async () => {
const memberShell = await userRepository.save({ const memberShell = await userRepository.save({
email: randomEmail(), email: randomEmail(),
role: 'global:member', role: { slug: 'global:member' },
}); });
const invalidPaylods = [ const invalidPaylods = [
@@ -374,7 +379,7 @@ describe('InvitationController', () => {
mailer.invite.mockResolvedValue({ emailSent: true }); mailer.invite.mockResolvedValue({ emailSent: true });
const member = await createMember(); const member = await createMember();
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const newUserEmail = randomEmail(); const newUserEmail = randomEmail();
const existingUserEmails = [member.email]; const existingUserEmails = [member.email];

View File

@@ -9,7 +9,7 @@ import {
mockInstance, mockInstance,
} from '@n8n/backend-test-utils'; } from '@n8n/backend-test-utils';
import type { Project, User, ListQueryDb } from '@n8n/db'; import type { Project, User, ListQueryDb } from '@n8n/db';
import { ProjectRepository, SharedCredentialsRepository } from '@n8n/db'; import { GLOBAL_MEMBER_ROLE, ProjectRepository, SharedCredentialsRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import type { ProjectRole } from '@n8n/permissions'; import type { ProjectRole } from '@n8n/permissions';
import { In } from '@n8n/typeorm'; import { In } from '@n8n/typeorm';
@@ -68,10 +68,10 @@ beforeEach(async () => {
admin = await createAdmin(); admin = await createAdmin();
ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
member = await createUser({ role: 'global:member' }); member = await createUser({ role: { slug: 'global:member' } });
memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(member.id); memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(member.id);
anotherMember = await createUser({ role: 'global:member' }); anotherMember = await createUser({ role: { slug: 'global:member' } });
anotherMemberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail( anotherMemberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
anotherMember.id, anotherMember.id,
); );
@@ -110,7 +110,7 @@ describe('POST /credentials', () => {
describe('GET /credentials', () => { describe('GET /credentials', () => {
test('should return all creds for owner', async () => { test('should return all creds for owner', async () => {
const [member1, member2, member3] = await createManyUsers(3, { const [member1, member2, member3] = await createManyUsers(3, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail( const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
member1.id, member1.id,
@@ -183,7 +183,7 @@ describe('GET /credentials', () => {
test('should return only relevant creds for member', async () => { test('should return only relevant creds for member', async () => {
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail( const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
member1.id, member1.id,
@@ -579,7 +579,7 @@ describe('GET /credentials/:id', () => {
test('should retrieve non-owned cred for owner', async () => { test('should retrieve non-owned cred for owner', async () => {
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail( const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
member1.id, member1.id,
@@ -626,7 +626,7 @@ describe('GET /credentials/:id', () => {
test('should retrieve owned cred for member', async () => { test('should retrieve owned cred for member', async () => {
const [member1, member2, member3] = await createManyUsers(3, { const [member1, member2, member3] = await createManyUsers(3, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail( const member1PersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
member1.id, member1.id,
@@ -745,7 +745,7 @@ describe('PUT /credentials/:id/share', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2, member3, member4, member5] = await createManyUsers(5, { const [member1, member2, member3, member4, member5] = await createManyUsers(5, {
role: 'global:member', role: { slug: 'global:member' },
}); });
// TODO: write helper for getting multiple personal projects by user id // TODO: write helper for getting multiple personal projects by user id
const shareWithProjectIds = ( const shareWithProjectIds = (
@@ -793,7 +793,7 @@ describe('PUT /credentials/:id/share', () => {
test('should share the credential with the provided userIds', async () => { test('should share the credential with the provided userIds', async () => {
const [member1, member2, member3] = await createManyUsers(3, { const [member1, member2, member3] = await createManyUsers(3, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const projectIds = ( const projectIds = (
await Promise.all([ await Promise.all([
@@ -876,7 +876,7 @@ describe('PUT /credentials/:id/share', () => {
test('should respond 403 for non-owned credentials for non-shared members sharing', async () => { test('should respond 403 for non-owned credentials for non-shared members sharing', async () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member });
const tempUser = await createUser({ role: 'global:member' }); const tempUser = await createUser({ role: { slug: 'global:member' } });
const tempUserPersonalProject = await projectRepository.getPersonalProjectForUserOrFail( const tempUserPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
tempUser.id, tempUser.id,
); );
@@ -910,7 +910,7 @@ describe('PUT /credentials/:id/share', () => {
}); });
test('should not ignore pending sharee', async () => { test('should not ignore pending sharee', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const memberShellPersonalProject = await projectRepository.getPersonalProjectForUserOrFail( const memberShellPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(
memberShell.id, memberShell.id,
); );
@@ -1019,7 +1019,7 @@ describe('PUT /credentials/:id/share', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
await shareCredentialWithUsers(savedCredential, [member1, member2]); await shareCredentialWithUsers(savedCredential, [member1, member2]);
@@ -1045,7 +1045,7 @@ describe('PUT /credentials/:id/share', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
await shareCredentialWithUsers(savedCredential, [member1, member2]); await shareCredentialWithUsers(savedCredential, [member1, member2]);

View File

@@ -103,7 +103,7 @@ describe('GET /credentials', () => {
test('should return only own creds for member', async () => { test('should return only own creds for member', async () => {
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const [savedCredential1] = await Promise.all([ const [savedCredential1] = await Promise.all([
@@ -125,7 +125,7 @@ describe('GET /credentials', () => {
test('should return scopes when ?includeScopes=true', async () => { test('should return scopes when ?includeScopes=true', async () => {
const [member1, member2] = await createManyUsers(2, { const [member1, member2] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const teamProject = await createTeamProject(undefined, member1); const teamProject = await createTeamProject(undefined, member1);
@@ -239,7 +239,7 @@ describe('GET /credentials', () => {
test('should return data when ?includeData=true', async () => { test('should return data when ?includeData=true', async () => {
// ARRANGE // ARRANGE
const [actor, otherMember] = await createManyUsers(2, { const [actor, otherMember] = await createManyUsers(2, {
role: 'global:member', role: { slug: 'global:member' },
}); });
const teamProjectViewer = await createTeamProject(undefined); const teamProjectViewer = await createTeamProject(undefined);

View File

@@ -1,6 +1,6 @@
import type { SourceControlledFile } from '@n8n/api-types'; import type { SourceControlledFile } from '@n8n/api-types';
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee'; import { SourceControlPreferencesService } from '@/environments.ee/source-control/source-control-preferences.service.ee';
@@ -24,7 +24,7 @@ const testServer = utils.setupTestServer({
let sourceControlPreferencesService: SourceControlPreferencesService; let sourceControlPreferencesService: SourceControlPreferencesService;
beforeAll(async () => { beforeAll(async () => {
owner = await createUser({ role: 'global:owner' }); owner = await createUser({ role: GLOBAL_OWNER_ROLE });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
sourceControlPreferencesService = Container.get(SourceControlPreferencesService); sourceControlPreferencesService = Container.get(SourceControlPreferencesService);

View File

@@ -4,6 +4,9 @@ import {
CredentialsEntity, CredentialsEntity,
type Folder, type Folder,
FolderRepository, FolderRepository,
GLOBAL_ADMIN_ROLE,
GLOBAL_MEMBER_ROLE,
GLOBAL_OWNER_ROLE,
Project, Project,
type TagEntity, type TagEntity,
TagRepository, TagRepository,
@@ -217,10 +220,10 @@ describe('SourceControlService', () => {
*/ */
[globalAdmin, globalOwner, globalMember, projectAdmin] = await Promise.all([ [globalAdmin, globalOwner, globalMember, projectAdmin] = await Promise.all([
await createUser({ role: 'global:admin' }), await createUser({ role: GLOBAL_ADMIN_ROLE }),
await createUser({ role: 'global:owner' }), await createUser({ role: GLOBAL_OWNER_ROLE }),
await createUser({ role: 'global:member' }), await createUser({ role: GLOBAL_MEMBER_ROLE }),
await createUser({ role: 'global:member' }), await createUser({ role: GLOBAL_MEMBER_ROLE }),
]); ]);
[projectA, projectB] = await Promise.all([ [projectA, projectB] = await Promise.all([

View File

@@ -1,6 +1,11 @@
import { createWorkflow, testDb } from '@n8n/backend-test-utils'; import { createWorkflow, testDb } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db'; import type { User } from '@n8n/db';
import { ProjectRepository, TestRunRepository } from '@n8n/db'; import {
GLOBAL_MEMBER_ROLE,
GLOBAL_OWNER_ROLE,
ProjectRepository,
TestRunRepository,
} from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mockInstance } from 'n8n-core/test/utils'; import { mockInstance } from 'n8n-core/test/utils';
import type { IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowBase } from 'n8n-workflow';
@@ -25,7 +30,7 @@ const testServer = utils.setupTestServer({
}); });
beforeAll(async () => { beforeAll(async () => {
ownerShell = await createUserShell('global:owner'); ownerShell = await createUserShell(GLOBAL_OWNER_ROLE);
authOwnerAgent = testServer.authAgentFor(ownerShell); authOwnerAgent = testServer.authAgentFor(ownerShell);
}); });
@@ -113,7 +118,7 @@ describe('GET /workflows/:workflowId/test-runs', () => {
}); });
test('should retrieve list of test runs for a shared workflow', async () => { test('should retrieve list of test runs for a shared workflow', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const memberAgent = testServer.authAgentFor(memberShell); const memberAgent = testServer.authAgentFor(memberShell);
const memberPersonalProject = await Container.get( const memberPersonalProject = await Container.get(
ProjectRepository, ProjectRepository,
@@ -171,7 +176,7 @@ describe('GET /workflows/:workflowId/test-runs/:id', () => {
}); });
test('should retrieve test run of a shared workflow', async () => { test('should retrieve test run of a shared workflow', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const memberAgent = testServer.authAgentFor(memberShell); const memberAgent = testServer.authAgentFor(memberShell);
const memberPersonalProject = await Container.get( const memberPersonalProject = await Container.get(
ProjectRepository, ProjectRepository,
@@ -345,7 +350,7 @@ describe('GET /workflows/:workflowId/test-runs/:id/test-cases', () => {
}); });
test('should return test cases for a shared workflow', async () => { test('should return test cases for a shared workflow', async () => {
const memberShell = await createUserShell('global:member'); const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE);
const memberAgent = testServer.authAgentFor(memberShell); const memberAgent = testServer.authAgentFor(memberShell);
const memberPersonalProject = await Container.get( const memberPersonalProject = await Container.get(
ProjectRepository, ProjectRepository,

View File

@@ -1,5 +1,5 @@
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import axios from 'axios'; import axios from 'axios';
import type { import type {
@@ -89,7 +89,7 @@ const testServer = utils.setupTestServer({
}); });
beforeAll(async () => { beforeAll(async () => {
owner = await createUser({ role: 'global:owner' }); owner = await createUser({ role: GLOBAL_OWNER_ROLE });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
mockedSyslog.createClient.mockImplementation(() => new syslog.Client()); mockedSyslog.createClient.mockImplementation(() => new syslog.Client());

View File

@@ -1,5 +1,5 @@
import { mockInstance } from '@n8n/backend-test-utils'; import { mockInstance } from '@n8n/backend-test-utils';
import type { User } from '@n8n/db'; import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { ExecutionRecoveryService } from '@/executions/execution-recovery.service'; import { ExecutionRecoveryService } from '@/executions/execution-recovery.service';
@@ -25,7 +25,7 @@ const testServer = utils.setupTestServer({
}); });
beforeAll(async () => { beforeAll(async () => {
owner = await createUser({ role: 'global:owner' }); owner = await createUser({ role: GLOBAL_OWNER_ROLE });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
}); });

View File

@@ -1793,7 +1793,7 @@ describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
test('should not transfer folder if license does not allow it', async () => { test('should not transfer folder if license does not allow it', async () => {
testServer.license.disable('feat:folders'); testServer.license.disable('feat:folders');
const admin = await createUser({ role: 'global:admin' }); const admin = await createUser({ role: { slug: 'global:admin' } });
const sourceProject = await createTeamProject('source project', admin); const sourceProject = await createTeamProject('source project', admin);
const destinationProject = await createTeamProject('destination project', member); const destinationProject = await createTeamProject('destination project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' }); const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
@@ -1996,7 +1996,7 @@ describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
test('owner transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => { test('owner transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => {
// ARRANGE // ARRANGE
const admin = await createUser({ role: 'global:admin' }); const admin = await createUser({ role: { slug: 'global:admin' } });
const sourceProject = await createTeamProject('source project', admin); const sourceProject = await createTeamProject('source project', admin);
const destinationProject = await createTeamProject('destination project', member); const destinationProject = await createTeamProject('destination project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' }); const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
@@ -2078,7 +2078,7 @@ describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
test('admin transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => { test('admin transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => {
// ARRANGE // ARRANGE
const admin = await createUser({ role: 'global:admin' }); const admin = await createUser({ role: { slug: 'global:admin' } });
const sourceProject = await createTeamProject('source project', owner); const sourceProject = await createTeamProject('source project', owner);
const destinationProject = await createTeamProject('destination project', owner); const destinationProject = await createTeamProject('destination project', owner);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' }); const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });

View File

@@ -8,6 +8,7 @@ import { createCompactedInsightsEvent } from '@/modules/insights/database/entiti
import { createUser } from '../shared/db/users'; import { createUser } from '../shared/db/users';
import type { SuperAgentTest } from '../shared/types'; import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils'; import * as utils from '../shared/utils';
import { GLOBAL_ADMIN_ROLE, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE } from '@n8n/db';
mockInstance(Telemetry); mockInstance(Telemetry);
@@ -20,9 +21,9 @@ const testServer = utils.setupTestServer({
}); });
beforeAll(async () => { beforeAll(async () => {
const owner = await createUser({ role: 'global:owner' }); const owner = await createUser({ role: GLOBAL_OWNER_ROLE });
const admin = await createUser({ role: 'global:admin' }); const admin = await createUser({ role: GLOBAL_ADMIN_ROLE });
const member = await createUser({ role: 'global:member' }); const member = await createUser({ role: GLOBAL_MEMBER_ROLE });
agents.owner = testServer.authAgentFor(owner); agents.owner = testServer.authAgentFor(owner);
agents.admin = testServer.authAgentFor(admin); agents.admin = testServer.authAgentFor(admin);
agents.member = testServer.authAgentFor(member); agents.member = testServer.authAgentFor(member);

View File

@@ -7,7 +7,12 @@ import {
} from '@n8n/backend-test-utils'; } from '@n8n/backend-test-utils';
import { LDAP_DEFAULT_CONFIGURATION } from '@n8n/constants'; import { LDAP_DEFAULT_CONFIGURATION } from '@n8n/constants';
import type { User } from '@n8n/db'; import type { User } from '@n8n/db';
import { AuthProviderSyncHistoryRepository, UserRepository } from '@n8n/db'; import {
AuthProviderSyncHistoryRepository,
GLOBAL_MEMBER_ROLE,
GLOBAL_OWNER_ROLE,
UserRepository,
} from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Not } from '@n8n/typeorm'; import { Not } from '@n8n/typeorm';
import type { Entry as LdapUser } from 'ldapts'; import type { Entry as LdapUser } from 'ldapts';
@@ -37,7 +42,7 @@ const testServer = utils.setupTestServer({
}); });
beforeAll(async () => { beforeAll(async () => {
owner = await createUser({ role: 'global:owner' }); owner = await createUser({ role: GLOBAL_OWNER_ROLE });
authOwnerAgent = testServer.authAgentFor(owner); authOwnerAgent = testServer.authAgentFor(owner);
defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt( defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
@@ -65,7 +70,7 @@ beforeEach(async () => {
}); });
test('Member role should not be able to access ldap routes', async () => { test('Member role should not be able to access ldap routes', async () => {
const member = await createUser({ role: 'global:member' }); const member = await createUser({ role: { slug: 'global:member' } });
const authAgent = testServer.authAgentFor(member); const authAgent = testServer.authAgentFor(member);
await authAgent.get('/ldap/config').expect(403); await authAgent.get('/ldap/config').expect(403);
await authAgent.put('/ldap/config').expect(403); await authAgent.put('/ldap/config').expect(403);
@@ -137,7 +142,7 @@ describe('PUT /ldap/config', () => {
const ldapConfig = await createLdapConfig(); const ldapConfig = await createLdapConfig();
Container.get(LdapService).setConfig(ldapConfig); Container.get(LdapService).setConfig(ldapConfig);
const member = await createLdapUser({ role: 'global:member' }, uniqueId()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
const configuration = ldapConfig; const configuration = ldapConfig;
@@ -250,7 +255,7 @@ describe('POST /ldap/sync', () => {
const ldapUserId = uniqueId(); const ldapUserId = uniqueId();
const member = await createLdapUser( const member = await createLdapUser(
{ role: 'global:member', email: ldapUserEmail }, { role: { slug: 'global:member' }, email: ldapUserEmail },
ldapUserId, ldapUserId,
); );
@@ -279,7 +284,7 @@ describe('POST /ldap/sync', () => {
const ldapUserId = uniqueId(); const ldapUserId = uniqueId();
const member = await createLdapUser( const member = await createLdapUser(
{ role: 'global:member', email: ldapUserEmail }, { role: { slug: 'global:member' }, email: ldapUserEmail },
ldapUserId, ldapUserId,
); );
@@ -364,7 +369,7 @@ describe('POST /ldap/sync', () => {
await createLdapUser( await createLdapUser(
{ {
role: 'global:member', role: { slug: 'global:member' },
email: ldapUser.mail, email: ldapUser.mail,
firstName: ldapUser.givenName, firstName: ldapUser.givenName,
lastName: randomName(), lastName: randomName(),
@@ -397,7 +402,7 @@ describe('POST /ldap/sync', () => {
await createLdapUser( await createLdapUser(
{ {
role: 'global:member', role: { slug: 'global:member' },
email: ldapUser.mail, email: ldapUser.mail,
firstName: ldapUser.givenName, firstName: ldapUser.givenName,
lastName: ldapUser.sn, lastName: ldapUser.sn,
@@ -426,7 +431,7 @@ describe('POST /ldap/sync', () => {
}); });
test('should remove user instance access once the user is disabled during synchronization', async () => { test('should remove user instance access once the user is disabled during synchronization', async () => {
const member = await createLdapUser({ role: 'global:member' }, uniqueId()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]); jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]);
@@ -485,7 +490,7 @@ describe('POST /ldap/sync', () => {
// Create user with valid email first // Create user with valid email first
await createLdapUser( await createLdapUser(
{ {
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
email: originalEmail, email: originalEmail,
firstName: randomName(), firstName: randomName(),
lastName: randomName(), lastName: randomName(),
@@ -603,7 +608,7 @@ describe('POST /login', () => {
await createLdapUser( await createLdapUser(
{ {
role: 'global:member', role: { slug: 'global:member' },
email: ldapUser.mail, email: ldapUser.mail,
firstName: 'firstname', firstName: 'firstname',
lastName: 'lastname', lastName: 'lastname',
@@ -637,7 +642,7 @@ describe('POST /login', () => {
}; };
await createUser({ await createUser({
role: 'global:member', role: GLOBAL_MEMBER_ROLE,
email: ldapUser.mail, email: ldapUser.mail,
firstName: ldapUser.givenName, firstName: ldapUser.givenName,
lastName: 'lastname', lastName: 'lastname',
@@ -652,7 +657,7 @@ describe('Instance owner should able to delete LDAP users', () => {
const ldapConfig = await createLdapConfig(); const ldapConfig = await createLdapConfig();
Container.get(LdapService).setConfig(ldapConfig); Container.get(LdapService).setConfig(ldapConfig);
const member = await createLdapUser({ role: 'global:member' }, uniqueId()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
await authOwnerAgent.post(`/users/${member.id}`); await authOwnerAgent.post(`/users/${member.id}`);
}); });
@@ -661,7 +666,7 @@ describe('Instance owner should able to delete LDAP users', () => {
const ldapConfig = await createLdapConfig(); const ldapConfig = await createLdapConfig();
Container.get(LdapService).setConfig(ldapConfig); Container.get(LdapService).setConfig(ldapConfig);
const member = await createLdapUser({ role: 'global:member' }, uniqueId()); const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
// delete the LDAP member and transfer its workflows/credentials to instance owner // delete the LDAP member and transfer its workflows/credentials to instance owner
await authOwnerAgent.post(`/users/${member.id}?transferId=${owner.id}`); await authOwnerAgent.post(`/users/${member.id}?transferId=${owner.id}`);

Some files were not shown because too many files have changed in this diff Show More