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

This commit is contained in:
Andreas Fitzek
2025-08-25 14:29:28 +02:00
committed by GitHub
parent b62c957c71
commit a21a03d4b0
117 changed files with 410 additions and 875 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,13 +22,11 @@ export const RESOURCES = {
user: ['resetPassword', 'changeRole', 'enforceMfa', ...DEFAULT_OPERATIONS] as const,
variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', 'activate', 'deactivate', ...DEFAULT_OPERATIONS] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
folder: [...DEFAULT_OPERATIONS, 'move'] as const,
insights: ['list'] as const,
oidc: ['manage'] as const,
dataStore: [...DEFAULT_OPERATIONS, 'readRow', 'writeRow', 'listProject'] as const,
execution: ['delete', 'read', 'list', 'get'] as const,
workflowTags: ['update', 'list'] as const,
} as const;
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 { rolesWithScope } from './utilities/roles-with-scope.ee';
export { getGlobalScopes } from './utilities/get-global-scopes.ee';
export { getRoleScopes, getAuthPrincipalScopes } from './utilities/get-role-scopes.ee';
export { getRoleScopes } from './utilities/get-role-scopes.ee';
export { getResourcePermissions } from './utilities/get-resource-permissions.ee';
export type { PermissionsRecord } from './utilities/get-resource-permissions.ee';
export * from './public-api-permissions.ee';

View File

@@ -1,4 +1,4 @@
import { isApiKeyScope, type ApiKeyScope, type AuthPrincipal, type GlobalRole } from './types.ee';
import type { ApiKeyScope, GlobalRole } from './types.ee';
export const OWNER_API_KEY_SCOPES: ApiKeyScope[] = [
'user:read',
@@ -65,46 +65,14 @@ export const MEMBER_API_KEY_SCOPES: ApiKeyScope[] = [
'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[]> = {
'global:owner': OWNER_API_KEY_SCOPES,
'global:admin': ADMIN_API_KEY_SCOPES,
'global:member': MEMBER_API_KEY_SCOPES,
};
export const getApiKeyScopesForRole = (user: AuthPrincipal) => {
return [
...new Set(
user.role.scopes
.map((scope) => scope.slug)
.concat(API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT)
.filter(isApiKeyScope),
),
];
export const getApiKeyScopesForRole = (role: GlobalRole) => {
return MAP_ROLE_SCOPES[role];
};
export const getOwnerOnlyApiKeyScopes = () => {

View File

@@ -1,5 +1,5 @@
import { API_KEY_RESOURCES, RESOURCES } from './constants.ee';
import type { ApiKeyScope, Scope, ScopeInformation } from './types.ee';
import { RESOURCES } from './constants.ee';
import type { Scope, ScopeInformation } from './types.ee';
function buildResourceScopes() {
const resourceScopes = Object.entries(RESOURCES).flatMap(([resource, operations]) => [
@@ -11,19 +11,8 @@ function buildResourceScopes() {
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();
// 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>> = {
'annotationTag:create': {
displayName: 'Create Annotation Tag',

View File

@@ -10,7 +10,6 @@ import type {
teamRoleSchema,
workflowSharingRoleSchema,
} from './schemas.ee';
import { ALL_API_KEY_SCOPES } from './scope-information';
export type ScopeInformation = {
displayName: string;
@@ -77,21 +76,12 @@ export type AllRolesMap = {
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.
* @property role - The global role this principal has
*/
export type AuthPrincipal = {
role: DbRole;
role: GlobalRole;
};
// #region Public API
@@ -111,9 +101,4 @@ type AllApiKeyScopesObject = {
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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
import type { AllRoleTypes, AuthPrincipal, Resource, Scope } from '../types.ee';
import type { AllRoleTypes, Resource, Scope } from '../types.ee';
export const COMBINED_ROLE_MAP = Object.fromEntries(
Object.values(ALL_ROLE_MAPS).flatMap((o: Record<string, Scope[]>) => Object.entries(o)),
@@ -10,8 +10,6 @@ export const COMBINED_ROLE_MAP = Object.fromEntries(
* @param role - The role to look up
* @param filters - Optional resources to filter scopes by
* @returns Array of matching scopes
*
* @deprecated Use the 'getRoleScopes' from the AuthRolesService instead.
*/
export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[] {
let scopes = COMBINED_ROLE_MAP[role];
@@ -20,22 +18,3 @@ export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[]
}
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 type { AuthPrincipal, Scope, ScopeOptions } from '../types.ee';
import { getAuthPrincipalScopes } from './get-role-scopes.ee';
/**
* Checks if an auth-principal has specified global scope(s).
@@ -12,6 +12,6 @@ export const hasGlobalScope = (
scope: Scope | Scope[],
scopeOptions?: ScopeOptions,
): boolean => {
const global = getAuthPrincipalScopes(principal);
const global = getGlobalScopes(principal);
return hasScope(scope, { global }, undefined, scopeOptions);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -132,10 +132,7 @@ export class MFAController {
await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode);
}
const updatedUser = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['role'],
});
const updatedUser = await this.userRepository.findOneByOrFail({ id: userId });
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 { Time } from '@n8n/constants';
import type { CredentialsEntity, User } from '@n8n/db';
import { CredentialsRepository, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { CredentialsRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import Csrf from 'csrf';
import type { Response } from 'express';
@@ -44,7 +44,7 @@ describe('OAuth1CredentialController', () => {
id: '123',
password: 'password',
authIdentities: [],
role: GLOBAL_OWNER_ROLE,
role: 'global:owner',
});
const credential = mock<CredentialsEntity>({
id: '1',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import {
AuthIdentityRepository,
AuthProviderSyncHistoryRepository,
UserRepository,
GLOBAL_MEMBER_ROLE,
} from '@n8n/db';
import { Container } from '@n8n/di';
import { validate } from 'jsonschema';
@@ -92,22 +91,6 @@ 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> => {
return await Container.get(UserRepository).findOne({
where: { email },
@@ -167,7 +150,7 @@ export const mapLdapUserToDbUser = (
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
Object.assign(user, data);
if (toCreate) {
user.role = GLOBAL_MEMBER_ROLE;
user.role = 'global:member';
user.password = randomString(8);
user.disabled = false;
} else {
@@ -287,7 +270,7 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => {
const { user } = await Container.get(UserRepository).createUserWithProject({
password: randomString(8),
role: GLOBAL_MEMBER_ROLE,
role: 'global:member',
...data,
});
await createLdapAuthIdentity(user, ldapId);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { GLOBAL_ADMIN_ROLE, GLOBAL_MEMBER_ROLE, WorkflowEntity } from '@n8n/db';
import { WorkflowEntity } from '@n8n/db';
import type { User, SharedWorkflowRepository, WorkflowRepository } from '@n8n/db';
import { mock } from 'jest-mock-extended';
@@ -41,7 +41,7 @@ describe('ActiveWorkflowsService', () => {
});
it('should return all workflow ids when user has full access', async () => {
user.role = GLOBAL_ADMIN_ROLE;
user.role = 'global:admin';
const ids = await service.getAllActiveIdsFor(user);
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 () => {
user.role = GLOBAL_MEMBER_ROLE;
user.role = 'global:member';
sharedWorkflowRepository.getSharedWorkflowIds.mockResolvedValue(['3']);
const ids = await service.getAllActiveIdsFor(user);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -204,7 +204,7 @@ export class SamlService {
const user = await this.userRepository.findOne({
where: { email: lowerCasedEmail },
relations: ['authIdentities', 'role'],
relations: ['authIdentities'],
});
if (user) {
// 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')
async getWorkflowsWithNodesIncluded(req: AuthenticatedRequest, res: express.Response) {
try {
const hasPermission = req.user.role.slug === ROLE.Owner || req.user.role.slug === ROLE.Admin;
const hasPermission = req.user.role === ROLE.Owner || req.user.role === ROLE.Admin;
if (!hasPermission) {
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 { GlobalConfig } from '@n8n/config';
import type { User } from '@n8n/db';
import { ApiKeyRepository, GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { ApiKeyRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import {
getApiKeyScopesForRole,
@@ -59,7 +59,7 @@ describe('Owner shell', () => {
let ownerShell: User;
beforeEach(async () => {
ownerShell = await createUserShell(GLOBAL_OWNER_ROLE);
ownerShell = await createUserShell('global:owner');
});
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 scopesForRole = getApiKeyScopesForRole(ownerShell);
const scopesForRole = getApiKeyScopesForRole(ownerShell.role);
expect(scopes.sort()).toEqual(scopesForRole.sort());
expect(scopes).toEqual(scopesForRole);
});
});
@@ -317,7 +317,7 @@ describe('Member', () => {
beforeEach(async () => {
member = await createUser({
password: memberPassword,
role: GLOBAL_MEMBER_ROLE,
role: 'global:member',
});
await utils.setInstanceOwnerSetUp(true);
});
@@ -328,7 +328,6 @@ describe('Member', () => {
.post('/api-keys')
.send({ label: 'My API Key', expiresAt: null, scopes: ['workflow:create'] });
console.log(newApiKeyResponse.body);
expect(newApiKeyResponse.statusCode).toBe(200);
expect(newApiKeyResponse.body.data.apiKey).toBeDefined();
expect(newApiKeyResponse.body.data.apiKey).not.toBeNull();
@@ -493,8 +492,8 @@ describe('Member', () => {
const scopes = apiKeyScopesResponse.body.data as ApiKeyScope[];
const scopesForRole = getApiKeyScopesForRole(member);
const scopesForRole = getApiKeyScopesForRole(member.role);
expect(scopes.sort()).toEqual(scopesForRole.sort());
expect(scopes).toEqual(scopesForRole);
});
});

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ describe('--deleteWorkflowsAndCredentials', () => {
//
// ARRANGE
//
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const member = await createLdapUser({ role: 'global:member' }, uuid());
const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), {
@@ -166,7 +166,7 @@ describe('--userId', () => {
//
// ARRANGE
//
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const member = await createLdapUser({ role: 'global:member' }, uuid());
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.`,
@@ -177,7 +177,7 @@ describe('--userId', () => {
//
// ARRANGE
//
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const member = await createLdapUser({ role: 'global:member' }, uuid());
const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), {
@@ -242,7 +242,7 @@ describe('--projectId', () => {
//
// ARRANGE
//
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const member = await createLdapUser({ role: 'global:member' }, uuid());
const memberProject = await getPersonalProject(member);
await expect(command.run([`--projectId=${memberProject.id}`])).rejects.toThrowError(
@@ -254,7 +254,7 @@ describe('--projectId', () => {
//
// ARRANGE
//
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const member = await createLdapUser({ role: 'global:member' }, uuid());
const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), {
@@ -310,7 +310,7 @@ describe('--projectId', () => {
//
// ARRANGE
//
const member = await createLdapUser({ role: { slug: 'global:member' } }, uuid());
const member = await createLdapUser({ role: 'global:member' }, uuid());
const memberProject = await getPersonalProject(member);
const workflow = await createWorkflow({}, member);
const credential = await saveCredential(randomCredentialPayload(), {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { mockInstance } from '@n8n/backend-test-utils';
import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
import type { User } from '@n8n/db';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { ExecutionRecoveryService } from '@/executions/execution-recovery.service';
@@ -25,7 +25,7 @@ const testServer = utils.setupTestServer({
});
beforeAll(async () => {
owner = await createUser({ role: GLOBAL_OWNER_ROLE });
owner = await createUser({ role: 'global: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 () => {
testServer.license.disable('feat:folders');
const admin = await createUser({ role: { slug: 'global:admin' } });
const admin = await createUser({ role: 'global:admin' });
const sourceProject = await createTeamProject('source project', admin);
const destinationProject = await createTeamProject('destination project', member);
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 () => {
// ARRANGE
const admin = await createUser({ role: { slug: 'global:admin' } });
const admin = await createUser({ role: 'global:admin' });
const sourceProject = await createTeamProject('source project', admin);
const destinationProject = await createTeamProject('destination project', member);
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 () => {
// ARRANGE
const admin = await createUser({ role: { slug: 'global:admin' } });
const admin = await createUser({ role: 'global:admin' });
const sourceProject = await createTeamProject('source project', owner);
const destinationProject = await createTeamProject('destination project', owner);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });

View File

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

View File

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