diff --git a/packages/@n8n/backend-test-utils/src/test-db.ts b/packages/@n8n/backend-test-utils/src/test-db.ts index 1493c32fe1..0f5798461a 100644 --- a/packages/@n8n/backend-test-utils/src/test-db.ts +++ b/packages/@n8n/backend-test-utils/src/test-db.ts @@ -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() { diff --git a/packages/@n8n/db/src/constants.ts b/packages/@n8n/db/src/constants.ts deleted file mode 100644 index 6520148711..0000000000 --- a/packages/@n8n/db/src/constants.ts +++ /dev/null @@ -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 = { - 'global:owner': GLOBAL_OWNER_ROLE, - 'global:admin': GLOBAL_ADMIN_ROLE, - 'global:member': GLOBAL_MEMBER_ROLE, -}; diff --git a/packages/@n8n/db/src/entities/types-db.ts b/packages/@n8n/db/src/entities/types-db.ts index ed51b3d292..7ac648655c 100644 --- a/packages/@n8n/db/src/entities/types-db.ts +++ b/packages/@n8n/db/src/entities/types-db.ts @@ -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; diff --git a/packages/@n8n/db/src/entities/user.ts b/packages/@n8n/db/src/entities/user.ts index f8c2fd4687..6b103d4a20 100644 --- a/packages/@n8n/db/src/entities/user.ts +++ b/packages/@n8n/db/src/entities/user.ts @@ -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() { diff --git a/packages/@n8n/db/src/index.ts b/packages/@n8n/db/src/index.ts index 4090928a6c..b2b1bd9621 100644 --- a/packages/@n8n/db/src/index.ts +++ b/packages/@n8n/db/src/index.ts @@ -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'; diff --git a/packages/@n8n/db/src/migrations/common/1742918400000-AddScopesColumnToApiKeys.ts b/packages/@n8n/db/src/migrations/common/1742918400000-AddScopesColumnToApiKeys.ts index 7455bbf338..94ed159710 100644 --- a/packages/@n8n/db/src/migrations/common/1742918400000-AddScopesColumnToApiKeys.ts +++ b/packages/@n8n/db/src/migrations/common/1742918400000-AddScopesColumnToApiKeys.ts @@ -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 }); } } diff --git a/packages/@n8n/db/src/migrations/common/1750252139170-RemoveOldRoleColumn.ts b/packages/@n8n/db/src/migrations/common/1750252139170-RemoveOldRoleColumn.ts deleted file mode 100644 index 1e9d9a5f9d..0000000000 --- a/packages/@n8n/db/src/migrations/common/1750252139170-RemoveOldRoleColumn.ts +++ /dev/null @@ -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})`, - ); - } -} diff --git a/packages/@n8n/db/src/migrations/mysqldb/index.ts b/packages/@n8n/db/src/migrations/mysqldb/index.ts index c711a8a2c5..cedde7a623 100644 --- a/packages/@n8n/db/src/migrations/mysqldb/index.ts +++ b/packages/@n8n/db/src/migrations/mysqldb/index.ts @@ -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, ]; diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts index 125612d7a2..d85271a13c 100644 --- a/packages/@n8n/db/src/migrations/postgresdb/index.ts +++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts @@ -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, ]; diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts index 8b90716bc6..f7a5d5e520 100644 --- a/packages/@n8n/db/src/migrations/sqlite/index.ts +++ b/packages/@n8n/db/src/migrations/sqlite/index.ts @@ -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 }; diff --git a/packages/@n8n/db/src/repositories/project-relation.repository.ts b/packages/@n8n/db/src/repositories/project-relation.repository.ts index 9ed236c0a0..27dc06931c 100644 --- a/packages/@n8n/db/src/repositories/project-relation.repository.ts +++ b/packages/@n8n/db/src/repositories/project-relation.repository.ts @@ -16,11 +16,7 @@ export class ProjectRelationRepository extends Repository { projectId: In(projectIds), role: 'project:personalOwner', }, - relations: { - user: { - role: true, - }, - }, + relations: { user: true }, }); } diff --git a/packages/@n8n/db/src/repositories/user.repository.ts b/packages/@n8n/db/src/repositories/user.repository.ts index 74f72d7314..328ff09b92 100644 --- a/packages/@n8n/db/src/repositories/user.repository.ts +++ b/packages/@n8n/db/src/repositories/user.repository.ts @@ -55,20 +55,19 @@ export class UserRepository extends Repository { 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, @@ -92,25 +91,20 @@ export class UserRepository extends Repository { const createInner = async (entityManager: EntityManager) => { const newUser = entityManager.create(User, user); const savedUser = await entityManager.save(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( entityManager.create(Project, { type: 'personal', - name: userWithRole.createPersonalProjectName(), + name: savedUser.createPersonalProjectName(), }), ); await entityManager.save( 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 { project: { sharedWorkflows: { workflowId, role: 'workflow:owner' } }, }, }, - relations: ['role'], }); } @@ -150,7 +143,6 @@ export class UserRepository extends Repository { projectId, }, }, - relations: ['role'], }); } @@ -294,8 +286,6 @@ export class UserRepository extends Repository { 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; } diff --git a/packages/@n8n/decorators/src/__tests__/redactable.test.ts b/packages/@n8n/decorators/src/__tests__/redactable.test.ts index 831af55d94..78331c6297 100644 --- a/packages/@n8n/decorators/src/__tests__/redactable.test.ts +++ b/packages/@n8n/decorators/src/__tests__/redactable.test.ts @@ -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', }, }; diff --git a/packages/@n8n/decorators/src/redactable.ts b/packages/@n8n/decorators/src/redactable.ts index 3f54253a03..9ef47cf1f2 100644 --- a/packages/@n8n/decorators/src/redactable.ts +++ b/packages/@n8n/decorators/src/redactable.ts @@ -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, }; } diff --git a/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap b/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap index 6e49bb7940..ff1374bb7f 100644 --- a/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap +++ b/packages/@n8n/permissions/src/__tests__/__snapshots__/scope-information.test.ts.snap @@ -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:*", "*", ] `; diff --git a/packages/@n8n/permissions/src/constants.ee.ts b/packages/@n8n/permissions/src/constants.ee.ts index 83db301ce4..c3dcac3b64 100644 --- a/packages/@n8n/permissions/src/constants.ee.ts +++ b/packages/@n8n/permissions/src/constants.ee.ts @@ -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 = { diff --git a/packages/@n8n/permissions/src/index.ts b/packages/@n8n/permissions/src/index.ts index fdcc7bb264..f8b8fb6d86 100644 --- a/packages/@n8n/permissions/src/index.ts +++ b/packages/@n8n/permissions/src/index.ts @@ -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'; diff --git a/packages/@n8n/permissions/src/public-api-permissions.ee.ts b/packages/@n8n/permissions/src/public-api-permissions.ee.ts index 47e5a454af..fd6c62c537 100644 --- a/packages/@n8n/permissions/src/public-api-permissions.ee.ts +++ b/packages/@n8n/permissions/src/public-api-permissions.ee.ts @@ -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 = { '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 = () => { diff --git a/packages/@n8n/permissions/src/scope-information.ts b/packages/@n8n/permissions/src/scope-information.ts index 95b729992f..fc457fd133 100644 --- a/packages/@n8n/permissions/src/scope-information.ts +++ b/packages/@n8n/permissions/src/scope-information.ts @@ -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> = { 'annotationTag:create': { displayName: 'Create Annotation Tag', diff --git a/packages/@n8n/permissions/src/types.ee.ts b/packages/@n8n/permissions/src/types.ee.ts index 34bc542c78..5e2ae1c022 100644 --- a/packages/@n8n/permissions/src/types.ee.ts +++ b/packages/@n8n/permissions/src/types.ee.ts @@ -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>; }; -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 diff --git a/packages/@n8n/permissions/src/utilities/__tests__/get-global-scopes.test.ts b/packages/@n8n/permissions/src/utilities/__tests__/get-global-scopes.test.ts index 979eaef45b..a56f1aa499 100644 --- a/packages/@n8n/permissions/src/utilities/__tests__/get-global-scopes.test.ts +++ b/packages/@n8n/permissions/src/utilities/__tests__/get-global-scopes.test.ts @@ -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([]); }); diff --git a/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts b/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts index aef291dfff..2df39ccd31 100644 --- a/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts +++ b/packages/@n8n/permissions/src/utilities/__tests__/get-resource-permissions.test.ts @@ -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); diff --git a/packages/@n8n/permissions/src/utilities/__tests__/has-global-scope.test.ts b/packages/@n8n/permissions/src/utilities/__tests__/has-global-scope.test.ts index d177053715..138e802641 100644 --- a/packages/@n8n/permissions/src/utilities/__tests__/has-global-scope.test.ts +++ b/packages/@n8n/permissions/src/utilities/__tests__/has-global-scope.test.ts @@ -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); }); }); diff --git a/packages/@n8n/permissions/src/utilities/__tests__/utils.ts b/packages/@n8n/permissions/src/utilities/__tests__/utils.ts deleted file mode 100644 index d2290e4291..0000000000 --- a/packages/@n8n/permissions/src/utilities/__tests__/utils.ts +++ /dev/null @@ -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, - }; - }) || [], - }, - }; -} diff --git a/packages/@n8n/permissions/src/utilities/get-global-scopes.ee.ts b/packages/@n8n/permissions/src/utilities/get-global-scopes.ee.ts index 18934a070a..aaa443fe34 100644 --- a/packages/@n8n/permissions/src/utilities/get-global-scopes.ee.ts +++ b/packages/@n8n/permissions/src/utilities/get-global-scopes.ee.ts @@ -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] ?? []; diff --git a/packages/@n8n/permissions/src/utilities/get-role-scopes.ee.ts b/packages/@n8n/permissions/src/utilities/get-role-scopes.ee.ts index aed73eb2cf..384bfaa43e 100644 --- a/packages/@n8n/permissions/src/utilities/get-role-scopes.ee.ts +++ b/packages/@n8n/permissions/src/utilities/get-role-scopes.ee.ts @@ -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) => 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; -} diff --git a/packages/@n8n/permissions/src/utilities/has-global-scope.ee.ts b/packages/@n8n/permissions/src/utilities/has-global-scope.ee.ts index 43fdf6460d..462a9ef97a 100644 --- a/packages/@n8n/permissions/src/utilities/has-global-scope.ee.ts +++ b/packages/@n8n/permissions/src/utilities/has-global-scope.ee.ts @@ -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); }; diff --git a/packages/cli/src/__tests__/workflow-runner.test.ts b/packages/cli/src/__tests__/workflow-runner.test.ts index 95810b806e..ff378cdac2 100644 --- a/packages/cli/src/__tests__/workflow-runner.test.ts +++ b/packages/cli/src/__tests__/workflow-runner.test.ts @@ -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); }); diff --git a/packages/cli/src/auth/auth.service.ts b/packages/cli/src/auth/auth.service.ts index 3a38f1dd1f..873bc0ce6e 100644 --- a/packages/cli/src/auth/auth.service.ts +++ b/packages/cli/src/auth/auth.service.ts @@ -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) { diff --git a/packages/cli/src/auth/methods/email.ts b/packages/cli/src/auth/methods/email.ts index 9c4216dfcc..81b8af35a2 100644 --- a/packages/cli/src/auth/methods/email.ts +++ b/packages/cli/src/auth/methods/email.ts @@ -13,7 +13,7 @@ export const handleEmailLogin = async ( ): Promise => { 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))) { diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 673bafe476..12c68110df 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -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> { } 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}`); } diff --git a/packages/cli/src/commands/user-management/reset.ts b/packages/cli/src/commands/user-management/reset.ts index cddd2027d8..69667fc565 100644 --- a/packages/cli/src/commands/user-management/reset.ts +++ b/packages/cli/src/commands/user-management/reset.ts @@ -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 { - 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 { diff --git a/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts b/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts index ae1044ab3f..e7e457a0f0 100644 --- a/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts @@ -129,7 +129,7 @@ describe('ApiKeysController', () => { id: '123', password: 'password', authIdentities: [], - role: { slug: 'global:member' }, + role: 'global:member', mfaEnabled: false, }); diff --git a/packages/cli/src/controllers/__tests__/auth.controller.test.ts b/packages/cli/src/controllers/__tests__/auth.controller.test.ts index 1478e6741b..18a2ecb696 100644 --- a/packages/cli/src/controllers/__tests__/auth.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/auth.controller.test.ts @@ -47,7 +47,7 @@ describe('AuthController', () => { const member = mock({ id: '123', - role: { slug: 'global:member' }, + role: 'global:member', mfaEnabled: false, }); diff --git a/packages/cli/src/controllers/__tests__/me.controller.test.ts b/packages/cli/src/controllers/__tests__/me.controller.test.ts index 95c56af9e7..47e05b1461 100644 --- a/packages/cli/src/controllers/__tests__/me.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/me.controller.test.ts @@ -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({ 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({ 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({ 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', }); diff --git a/packages/cli/src/controllers/__tests__/owner.controller.test.ts b/packages/cli/src/controllers/__tests__/owner.controller.test.ts index ebabd8cb69..0f8b982fe0 100644 --- a/packages/cli/src/controllers/__tests__/owner.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/owner.controller.test.ts @@ -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({ 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); diff --git a/packages/cli/src/controllers/__tests__/users.controller.test.ts b/packages/cli/src/controllers/__tests__/users.controller.test.ts index b17537baf8..65c16b7b80 100644 --- a/packages/cli/src/controllers/__tests__/users.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/users.controller.test.ts @@ -35,7 +35,7 @@ describe('UsersController', () => { const request = mock({ user: { id: '123' }, }); - userRepository.findOne.mockResolvedValue(mock({ id: '456' })); + userRepository.findOneBy.mockResolvedValue(mock({ id: '456' })); projectService.getUserOwnedOrAdminProjects.mockResolvedValue([]); await controller.changeGlobalRole( diff --git a/packages/cli/src/controllers/api-keys.controller.ts b/packages/cli/src/controllers/api-keys.controller.ts index 40016101d8..43fa163826 100644 --- a/packages/cli/src/controllers/api-keys.controller.ts +++ b/packages/cli/src/controllers/api-keys.controller.ts @@ -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; } } diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 0424e14e20..55fc67d3ad 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -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 { diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 250a4a6ca5..0ebe4cffb1 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -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', }), ); } diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 8dd5a7ed22..ffa84a2c99 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -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( diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 568204a1c1..2a86f4b02c 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -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 }); diff --git a/packages/cli/src/controllers/mfa.controller.ts b/packages/cli/src/controllers/mfa.controller.ts index 41790b8a70..8833a25ecc 100644 --- a/packages/cli/src/controllers/mfa.controller.ts +++ b/packages/cli/src/controllers/mfa.controller.ts @@ -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); } diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts index d87459b527..28750cfcc5 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts @@ -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({ id: '1', diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts index 73b9ce34df..76fe241232 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts @@ -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({ id: '1', diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 6192fed4cb..9b4feb5945 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -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; diff --git a/packages/cli/src/controllers/password-reset.controller.ts b/packages/cli/src/controllers/password-reset.controller.ts index 94164bb70b..706b5ebfa1 100644 --- a/packages/cli/src/controllers/password-reset.controller.ts +++ b/packages/cli/src/controllers/password-reset.controller.ts @@ -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 }, diff --git a/packages/cli/src/controllers/project.controller.ts b/packages/cli/src/controllers/project.controller.ts index 98725fc443..939369acc1 100644 --- a/packages/cli/src/controllers/project.controller.ts +++ b/packages/cli/src/controllers/project.controller.ts @@ -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, diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 0b8fe925dc..a101097474 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -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); } diff --git a/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts index 039674a0a2..2cec34f872 100644 --- a/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control-export.service.test.ts @@ -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', }), ); diff --git a/packages/cli/src/environments.ee/source-control/__tests__/source-control-import.service.ee.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control-import.service.ee.test.ts index b4c170fdc8..246cd45647 100644 --- a/packages/cli/src/environments.ee/source-control/__tests__/source-control-import.service.ee.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control-import.service.ee.test.ts @@ -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', }), ); diff --git a/packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts b/packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts index 6d7c8d4b27..1f001f6c61 100644 --- a/packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts +++ b/packages/cli/src/environments.ee/source-control/__tests__/source-control.service.test.ts @@ -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({ - role: GLOBAL_ADMIN_ROLE, - }); + const user = mock(); + 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({ - role: GLOBAL_ADMIN_ROLE, - }); + const user = mock(); + 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({ - role: GLOBAL_ADMIN_ROLE, - }); + const user = mock(); + 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({ - role: GLOBAL_MEMBER_ROLE, - }); + const user = mock(); + 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({ id: 'user-id', role: GLOBAL_ADMIN_ROLE }); + const user = mock({ id: 'user-id', role: 'global:admin' }); const result = await sourceControlService.getRemoteFileEntity({ user, type, id }); @@ -404,7 +398,7 @@ describe('SourceControlService', () => { it.each(['folders', 'credential', 'tags', 'variables'])( 'should throw an error if the file type is not handled', async (type) => { - const user = mock({ id: 'user-id', role: { slug: 'global:admin' } }); + const user = mock({ 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({ id: 'user-id', role: { slug: 'global:admin' } }); + const user = mock({ 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({ 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({ id: 'user-id', role: { slug: 'global:member' } }); + const user = mock({ id: 'user-id', role: 'global:member' }); jest .spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext') .mockResolvedValue([{ id: '1234' } as WorkflowEntity]); diff --git a/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts b/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts index 576b1fc24c..2cd2019018 100644 --- a/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/log-streaming-event-relay.test.ts @@ -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({ 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({ 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, }; diff --git a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts index b80c01d79a..078e463b72 100644 --- a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts @@ -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({ 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({ 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, diff --git a/packages/cli/src/events/maps/relay.event-map.ts b/packages/cli/src/events/maps/relay.event-map.ts index 0967c9314d..2d5fd0e2c7 100644 --- a/packages/cli/src/events/maps/relay.event-map.ts +++ b/packages/cli/src/events/maps/relay.event-map.ts @@ -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 diff --git a/packages/cli/src/executions/pre-execution-checks/__tests__/credentials-permission-checker.test.ts b/packages/cli/src/executions/pre-execution-checks/__tests__/credentials-permission-checker.test.ts index bb2f1b2a2f..7dc1685cb5 100644 --- a/packages/cli/src/executions/pre-execution-checks/__tests__/credentials-permission-checker.test.ts +++ b/packages/cli/src/executions/pre-execution-checks/__tests__/credentials-permission-checker.test.ts @@ -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({ role: GLOBAL_OWNER_ROLE }); + const projectOwner = mock({ role: 'global:owner' }); ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner); await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow(); diff --git a/packages/cli/src/ldap.ee/helpers.ee.ts b/packages/cli/src/ldap.ee/helpers.ee.ts index 074e49de97..a58b5ae03d 100644 --- a/packages/cli/src/ldap.ee/helpers.ee.ts +++ b/packages/cli/src/ldap.ee/helpers.ee.ts @@ -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 => { 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, ldapId: string) => { const { user } = await Container.get(UserRepository).createUserWithProject({ password: randomString(8), - role: GLOBAL_MEMBER_ROLE, + role: 'global:member', ...data, }); await createLdapAuthIdentity(user, ldapId); diff --git a/packages/cli/src/ldap.ee/ldap.service.ee.ts b/packages/cli/src/ldap.ee/ldap.service.ee.ts index c111b3f43f..1dbf050b94 100644 --- a/packages/cli/src/ldap.ee/ldap.service.ee.ts +++ b/packages/cli/src/ldap.ee/ldap.service.ee.ts @@ -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; } } diff --git a/packages/cli/src/mfa/mfa.service.ts b/packages/cli/src/mfa/mfa.service.ts index 60d2965720..3355442ad2 100644 --- a/packages/cli/src/mfa/mfa.service.ts +++ b/packages/cli/src/mfa/mfa.service.ts @@ -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); } diff --git a/packages/cli/src/modules/data-store/__tests__/data-store-aggregate.service.test.ts b/packages/cli/src/modules/data-store/__tests__/data-store-aggregate.service.test.ts index e64c1cc685..b2465216b9 100644 --- a/packages/cli/src/modules/data-store/__tests__/data-store-aggregate.service.test.ts +++ b/packages/cli/src/modules/data-store/__tests__/data-store-aggregate.service.test.ts @@ -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', diff --git a/packages/cli/src/permissions.ee/__tests__/check-access.test.ts b/packages/cli/src/permissions.ee/__tests__/check-access.test.ts index d60620a69d..bb518ab0f4 100644 --- a/packages/cli/src/permissions.ee/__tests__/check-access.test.ts +++ b/packages/cli/src/permissions.ee/__tests__/check-access.test.ts @@ -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 } = { diff --git a/packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts b/packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts index d933531024..2224f67c6d 100644 --- a/packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/credentials/credentials.handler.ts @@ -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') { diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index 02ef3c8825..8a85df8f4b 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -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), diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.service.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.service.ts index f396d3ef50..3fb3825847 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.service.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.service.ts @@ -44,7 +44,7 @@ export async function getSharedWorkflow( ): Promise { 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: [ diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 7d64f06f26..bfde747102 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -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; + type GetMyProjectsResponse = Array< + Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] } + >; type ProjectRelationResponse = { id: string; diff --git a/packages/cli/src/services/__tests__/active-workflows.service.test.ts b/packages/cli/src/services/__tests__/active-workflows.service.test.ts index 11a5957f93..f2984e6e28 100644 --- a/packages/cli/src/services/__tests__/active-workflows.service.test.ts +++ b/packages/cli/src/services/__tests__/active-workflows.service.test.ts @@ -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); diff --git a/packages/cli/src/services/__tests__/credentials-finder.service.test.ts b/packages/cli/src/services/__tests__/credentials-finder.service.test.ts index 18c5cb1e0c..3356e42a54 100644 --- a/packages/cli/src/services/__tests__/credentials-finder.service.test.ts +++ b/packages/cli/src/services/__tests__/credentials-finder.service.test.ts @@ -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(); sharedCredential.credentials = mock({ id: credentialsId }); const owner = mock({ - role: GLOBAL_OWNER_ROLE, + role: 'global:owner', }); const member = mock({ - role: GLOBAL_MEMBER_ROLE, + role: 'global:member', id: 'test', }); diff --git a/packages/cli/src/services/__tests__/ownership.service.test.ts b/packages/cli/src/services/__tests__/ownership.service.test.ts index 3105a7348b..2c307192b3 100644 --- a/packages/cli/src/services/__tests__/ownership.service.test.ts +++ b/packages/cli/src/services/__tests__/ownership.service.test.ts @@ -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' }, }); }); }); diff --git a/packages/cli/src/services/__tests__/public-api-key.service.test.ts b/packages/cli/src/services/__tests__/public-api-key.service.test.ts index 2ea748d894..519b69ffc2 100644 --- a/packages/cli/src/services/__tests__/public-api-key.service.test.ts +++ b/packages/cli/src/services/__tests__/public-api-key.service.test.ts @@ -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, ); diff --git a/packages/cli/src/services/__tests__/user.service.test.ts b/packages/cli/src/services/__tests__/user.service.test.ts index 5cf2025556..651fcb1c14 100644 --- a/packages/cli/src/services/__tests__/user.service.test.ts +++ b/packages/cli/src/services/__tests__/user.service.test.ts @@ -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, { diff --git a/packages/cli/src/services/access.service.ts b/packages/cli/src/services/access.service.ts index 4932e04ea8..c81ed37175 100644 --- a/packages/cli/src/services/access.service.ts +++ b/packages/cli/src/services/access.service.ts @@ -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; diff --git a/packages/cli/src/services/ownership.service.ts b/packages/cli/src/services/ownership.service.ts index ffcd3cd996..9f7ad7d458 100644 --- a/packages/cli/src/services/ownership.service.ts +++ b/packages/cli/src/services/ownership.service.ts @@ -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' }, }); } } diff --git a/packages/cli/src/services/public-api-key.service.ts b/packages/cli/src/services/public-api-key.service.ts index f5d7778597..58a80a5995 100644 --- a/packages/cli/src/services/public-api-key.service.ts +++ b/packages/cli/src/services/public-api-key.service.ts @@ -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)); } diff --git a/packages/cli/src/services/role.service.ts b/packages/cli/src/services/role.service.ts index f86aff0902..da1dd406d9 100644 --- a/packages/cli/src/services/role.service.ts +++ b/packages/cli/src/services/role.service.ts @@ -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 = new Set(globalScopes); for (const sharedEntity of shared) { const pr = userProjectRelations.find( diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 9b5c8d1210..8d2e9b5894 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -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) { diff --git a/packages/cli/src/sso.ee/oidc/oidc.service.ee.ts b/packages/cli/src/sso.ee/oidc/oidc.service.ee.ts index e716c52056..47100d0460 100644 --- a/packages/cli/src/sso.ee/oidc/oidc.service.ee.ts +++ b/packages/cli/src/sso.ee/oidc/oidc.service.ee.ts @@ -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, diff --git a/packages/cli/src/sso.ee/oidc/routes/__tests__/oidc.controller.ee.test.ts b/packages/cli/src/sso.ee/oidc/routes/__tests__/oidc.controller.ee.test.ts index cff5369540..93208f0db2 100644 --- a/packages/cli/src/sso.ee/oidc/routes/__tests__/oidc.controller.ee.test.ts +++ b/packages/cli/src/sso.ee/oidc/routes/__tests__/oidc.controller.ee.test.ts @@ -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({ lastName: 'User', password: 'password', authIdentities: [], - role: GLOBAL_MEMBER_ROLE, + role: 'global:member', }); describe('OidcController', () => { diff --git a/packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts b/packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts index 7ad9b2efb7..e9f9917e86 100644 --- a/packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts +++ b/packages/cli/src/sso.ee/saml/__tests__/saml-helpers.test.ts @@ -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); // diff --git a/packages/cli/src/sso.ee/saml/routes/__tests__/saml.controller.ee.test.ts b/packages/cli/src/sso.ee/saml/routes/__tests__/saml.controller.ee.test.ts index bbafc277a1..e88c2a5ce3 100644 --- a/packages/cli/src/sso.ee/saml/routes/__tests__/saml.controller.ee.test.ts +++ b/packages/cli/src/sso.ee/saml/routes/__tests__/saml.controller.ee.test.ts @@ -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({ id: '123', password: 'password', authIdentities: [], - role: GLOBAL_OWNER_ROLE, + role: 'global:owner', }); const attributes: SamlUserAttributes = { diff --git a/packages/cli/src/sso.ee/saml/saml-helpers.ts b/packages/cli/src/sso.ee/saml/saml-helpers.ts index 52079b1fb5..cebbac7ea4 100644 --- a/packages/cli/src/sso.ee/saml/saml-helpers.ts +++ b/packages/cli/src/sso.ee/saml/saml-helpers.ts @@ -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 = { diff --git a/packages/cli/src/sso.ee/saml/saml.service.ee.ts b/packages/cli/src/sso.ee/saml/saml.service.ee.ts index 327b8417ce..b96c3fe147 100644 --- a/packages/cli/src/sso.ee/saml/saml.service.ee.ts +++ b/packages/cli/src/sso.ee/saml/saml.service.ee.ts @@ -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 diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index a15044719e..618d03fc4c 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -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 }); diff --git a/packages/cli/test/integration/api-keys.api.test.ts b/packages/cli/test/integration/api-keys.api.test.ts index 573c78308b..8f6ff0e0c0 100644 --- a/packages/cli/test/integration/api-keys.api.test.ts +++ b/packages/cli/test/integration/api-keys.api.test.ts @@ -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); }); }); diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts index 3412daf080..5fcd891a50 100644 --- a/packages/cli/test/integration/auth.api.test.ts +++ b/packages/cli/test/integration/auth.api.test.ts @@ -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: '/' }); diff --git a/packages/cli/test/integration/auth.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index ec64df904b..d9f1dbd872 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -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); }); diff --git a/packages/cli/test/integration/commands/ldap/reset.test.ts b/packages/cli/test/integration/commands/ldap/reset.test.ts index 725f53a492..3be9485658 100644 --- a/packages/cli/test/integration/commands/ldap/reset.test.ts +++ b/packages/cli/test/integration/commands/ldap/reset.test.ts @@ -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(), { diff --git a/packages/cli/test/integration/commands/reset.cmd.test.ts b/packages/cli/test/integration/commands/reset.cmd.test.ts index e1262d5fe3..f5968db155 100644 --- a/packages/cli/test/integration/commands/reset.cmd.test.ts +++ b/packages/cli/test/integration/commands/reset.cmd.test.ts @@ -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: diff --git a/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts b/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts index 0101a847bb..ca00de5f99 100644 --- a/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts +++ b/packages/cli/test/integration/controllers/invitation/invitation.controller.integration.test.ts @@ -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]; diff --git a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts index 69d487489d..ceafb94eda 100644 --- a/packages/cli/test/integration/credentials/credentials.api.ee.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.ee.test.ts @@ -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]); diff --git a/packages/cli/test/integration/credentials/credentials.api.test.ts b/packages/cli/test/integration/credentials/credentials.api.test.ts index edbdc8e3e7..9a4cbb706f 100644 --- a/packages/cli/test/integration/credentials/credentials.api.test.ts +++ b/packages/cli/test/integration/credentials/credentials.api.test.ts @@ -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); diff --git a/packages/cli/test/integration/environments/source-control.api.test.ts b/packages/cli/test/integration/environments/source-control.api.test.ts index 91c39335d9..0fc559f552 100644 --- a/packages/cli/test/integration/environments/source-control.api.test.ts +++ b/packages/cli/test/integration/environments/source-control.api.test.ts @@ -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); diff --git a/packages/cli/test/integration/environments/source-control.service.test.ts b/packages/cli/test/integration/environments/source-control.service.test.ts index ebecf561e6..3491f57ea2 100644 --- a/packages/cli/test/integration/environments/source-control.service.test.ts +++ b/packages/cli/test/integration/environments/source-control.service.test.ts @@ -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([ diff --git a/packages/cli/test/integration/evaluation/test-runs.api.test.ts b/packages/cli/test/integration/evaluation/test-runs.api.test.ts index f08df23429..40916fd23a 100644 --- a/packages/cli/test/integration/evaluation/test-runs.api.test.ts +++ b/packages/cli/test/integration/evaluation/test-runs.api.test.ts @@ -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, diff --git a/packages/cli/test/integration/eventbus.ee.test.ts b/packages/cli/test/integration/eventbus.ee.test.ts index 122ada4760..1787176571 100644 --- a/packages/cli/test/integration/eventbus.ee.test.ts +++ b/packages/cli/test/integration/eventbus.ee.test.ts @@ -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()); diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index 48cc184489..86bfec4a39 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -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); }); diff --git a/packages/cli/test/integration/folder/folder.controller.test.ts b/packages/cli/test/integration/folder/folder.controller.test.ts index 653a0d3b8a..bb0c2563d2 100644 --- a/packages/cli/test/integration/folder/folder.controller.test.ts +++ b/packages/cli/test/integration/folder/folder.controller.test.ts @@ -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' }); diff --git a/packages/cli/test/integration/insights/insights.api.test.ts b/packages/cli/test/integration/insights/insights.api.test.ts index 5bbe6ff462..296d2ba3da 100644 --- a/packages/cli/test/integration/insights/insights.api.test.ts +++ b/packages/cli/test/integration/insights/insights.api.test.ts @@ -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); diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index 29e9473447..3a26f45a0d 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -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}`); diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index bbf9a8eb75..357ef5fed7 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -1,5 +1,5 @@ import { testDb } from '@n8n/backend-test-utils'; -import { GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, type User } from '@n8n/db'; +import type { User } from '@n8n/db'; import nock from 'nock'; import config from '@/config'; @@ -21,8 +21,8 @@ let authMemberAgent: SuperAgentTest; const testServer = utils.setupTestServer({ endpointGroups: ['license'] }); beforeAll(async () => { - owner = await createUserShell(GLOBAL_OWNER_ROLE); - member = await createUserShell(GLOBAL_MEMBER_ROLE); + owner = await createUserShell('global:owner'); + member = await createUserShell('global:member'); authOwnerAgent = testServer.authAgentFor(owner); authMemberAgent = testServer.authAgentFor(member); diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index 7b9ebc8a77..6fb7b19776 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -7,7 +7,7 @@ import { } from '@n8n/backend-test-utils'; import { GlobalConfig } from '@n8n/config'; import type { User } from '@n8n/db'; -import { GLOBAL_OWNER_ROLE, ProjectRepository, UserRepository } from '@n8n/db'; +import { ProjectRepository, UserRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow'; import validator from 'validator'; @@ -32,7 +32,7 @@ describe('Owner shell', () => { let authOwnerShellAgent: SuperAgentTest; beforeEach(async () => { - ownerShell = await createUserShell(GLOBAL_OWNER_ROLE); + ownerShell = await createUserShell('global:owner'); authOwnerShellAgent = testServer.authAgentFor(ownerShell); }); @@ -139,7 +139,7 @@ describe('Member', () => { beforeEach(async () => { member = await createUser({ password: memberPassword, - role: { slug: 'global:member' }, + role: 'global:member', }); authMemberAgent = testServer.authAgentFor(member); await utils.setInstanceOwnerSetUp(true); @@ -243,7 +243,7 @@ describe('Member', () => { describe('Owner', () => { test('PATCH /me should succeed with valid inputs', async () => { - const owner = await createUser({ role: GLOBAL_OWNER_ROLE }); + const owner = await createUser({ role: 'global:owner' }); const authOwnerAgent = testServer.authAgentFor(owner); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { diff --git a/packages/cli/test/integration/mfa/mfa.api.test.ts b/packages/cli/test/integration/mfa/mfa.api.test.ts index 65c45f798c..83e760d680 100644 --- a/packages/cli/test/integration/mfa/mfa.api.test.ts +++ b/packages/cli/test/integration/mfa/mfa.api.test.ts @@ -30,11 +30,6 @@ beforeEach(async () => { owner = await createOwner(); - owner = await Container.get(UserRepository).findOneOrFail({ - where: { id: owner.id }, - relations: ['role'], - }); - externalHooks.run.mockReset(); config.set('userManagement.disabled', false); diff --git a/packages/cli/test/integration/owner.api.test.ts b/packages/cli/test/integration/owner.api.test.ts index f1f096e625..2238c6e05f 100644 --- a/packages/cli/test/integration/owner.api.test.ts +++ b/packages/cli/test/integration/owner.api.test.ts @@ -6,7 +6,7 @@ import { testDb, } from '@n8n/backend-test-utils'; import type { User } from '@n8n/db'; -import { GLOBAL_OWNER_ROLE, UserRepository } from '@n8n/db'; +import { UserRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import validator from 'validator'; @@ -20,7 +20,7 @@ const testServer = utils.setupTestServer({ endpointGroups: ['owner'] }); let ownerShell: User; beforeEach(async () => { - ownerShell = await createUserShell(GLOBAL_OWNER_ROLE); + ownerShell = await createUserShell('global:owner'); config.set('userManagement.isInstanceOwnerSetUp', false); }); diff --git a/packages/cli/test/integration/password-reset.api.test.ts b/packages/cli/test/integration/password-reset.api.test.ts index 6194f21119..1a13eac312 100644 --- a/packages/cli/test/integration/password-reset.api.test.ts +++ b/packages/cli/test/integration/password-reset.api.test.ts @@ -7,7 +7,7 @@ import { mockInstance, } 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 { compare } from 'bcryptjs'; import { mock } from 'jest-mock-extended'; @@ -39,8 +39,8 @@ let authService: AuthService; beforeEach(async () => { await testDb.truncate(['User']); - owner = await createUser({ role: GLOBAL_OWNER_ROLE }); - member = await createUser({ role: GLOBAL_MEMBER_ROLE }); + owner = await createUser({ role: 'global:owner' }); + member = await createUser({ role: 'global:member' }); externalHooks.run.mockReset(); jest.replaceProperty(mailer, 'isEmailSetUp', true); authService = Container.get(AuthService); @@ -50,7 +50,7 @@ describe('POST /forgot-password', () => { test('should send password reset email', async () => { const member = await createUser({ email: 'test@test.com', - role: { slug: 'global:member' }, + role: 'global:member', }); await Promise.all( @@ -76,7 +76,7 @@ describe('POST /forgot-password', () => { await setCurrentAuthenticationMethod('saml'); const member = await createUser({ email: 'test@test.com', - role: { slug: 'global:member' }, + role: 'global:member', }); await testServer.authlessAgent diff --git a/packages/cli/test/integration/public-api/endpoints-with-scopes-enabled.test.ts b/packages/cli/test/integration/public-api/endpoints-with-scopes-enabled.test.ts index 026b704b94..673fdef04c 100644 --- a/packages/cli/test/integration/public-api/endpoints-with-scopes-enabled.test.ts +++ b/packages/cli/test/integration/public-api/endpoints-with-scopes-enabled.test.ts @@ -222,7 +222,7 @@ describe('Public API endpoints with feat:apiKeyScopes enabled', () => { expect(returnedUser.id).toBe(storedUser.id); expect(returnedUser.email).toBe(storedUser.email); expect(returnedUser.email).toBe(payloadUser.email); - expect(storedUser.role.slug).toBe(payloadUser.role); + expect(storedUser.role).toBe(payloadUser.role); }); test('should fail to create user when API key doesn\'t have "user:create" scope', async () => { @@ -267,7 +267,7 @@ describe('Public API endpoints with feat:apiKeyScopes enabled', () => { */ expect(response.status).toBe(204); const storedUser = await getUserById(member.id); - expect(storedUser.role.slug).toBe(payload.newRoleName); + expect(storedUser.role).toBe(payload.newRoleName); }); test('should fail to change role when API key doesn\'t have "user:changeRole" scope', async () => { diff --git a/packages/cli/test/integration/public-api/users.ee.test.ts b/packages/cli/test/integration/public-api/users.ee.test.ts index 9e60554260..ead728b7f5 100644 --- a/packages/cli/test/integration/public-api/users.ee.test.ts +++ b/packages/cli/test/integration/public-api/users.ee.test.ts @@ -4,7 +4,7 @@ import { testDb, mockInstance, } from '@n8n/backend-test-utils'; -import { GLOBAL_MEMBER_ROLE, type User } from '@n8n/db'; +import type { User } from '@n8n/db'; import { v4 as uuid } from 'uuid'; import validator from 'validator'; @@ -155,7 +155,7 @@ describe('With license unlimited quota:users', () => { test('should return a pending user', async () => { const owner = await createOwnerWithApiKey(); - const { id: memberId } = await createUserShell(GLOBAL_MEMBER_ROLE); + const { id: memberId } = await createUserShell('global:member'); const authOwnerAgent = testServer.publicApiAgentFor(owner); const response = await authOwnerAgent.get(`/users/${memberId}`).expect(200); diff --git a/packages/cli/test/integration/public-api/users.test.ts b/packages/cli/test/integration/public-api/users.test.ts index e42f192608..8c120bd2f2 100644 --- a/packages/cli/test/integration/public-api/users.test.ts +++ b/packages/cli/test/integration/public-api/users.test.ts @@ -95,7 +95,7 @@ describe('Users in Public API', () => { expect(returnedUser.id).toBe(storedUser.id); expect(returnedUser.email).toBe(storedUser.email); expect(returnedUser.email).toBe(payloadUser.email); - expect(storedUser.role.slug).toBe(payloadUser.role); + expect(storedUser.role).toBe(payloadUser.role); }); }); @@ -275,7 +275,7 @@ describe('Users in Public API', () => { */ expect(response.status).toBe(204); const storedUser = await getUserById(member.id); - expect(storedUser.role.slug).toBe(payload.newRoleName); + expect(storedUser.role).toBe(payload.newRoleName); }); }); }); diff --git a/packages/cli/test/integration/shared/db/users.ts b/packages/cli/test/integration/shared/db/users.ts index a3f684fb26..64aefcc929 100644 --- a/packages/cli/test/integration/shared/db/users.ts +++ b/packages/cli/test/integration/shared/db/users.ts @@ -1,23 +1,14 @@ import { randomEmail, randomName, randomValidPassword } from '@n8n/backend-test-utils'; -import { - AuthIdentity, - AuthIdentityRepository, - GLOBAL_ADMIN_ROLE, - GLOBAL_MEMBER_ROLE, - GLOBAL_OWNER_ROLE, - type Role, - UserRepository, -} from '@n8n/db'; +import { AuthIdentity, AuthIdentityRepository, UserRepository } from '@n8n/db'; import { type User } from '@n8n/db'; import { Container } from '@n8n/di'; -import type { ApiKeyScope } from '@n8n/permissions'; +import type { ApiKeyScope, GlobalRole } from '@n8n/permissions'; import { getApiKeyScopesForRole } from '@n8n/permissions'; import { hash } from 'bcryptjs'; import { MfaService } from '@/mfa/mfa.service'; import { TOTPService } from '@/mfa/totp.service'; import { PublicApiKeyService } from '@/services/public-api-key.service'; -import type { DeepPartial } from '@n8n/typeorm'; type ApiKeyOptions = { expiresAt?: number | null; @@ -41,26 +32,26 @@ async function handlePasswordSetup(password: string | null | undefined): Promise } /** Store a new user object, defaulting to a `member` */ -export async function newUser(attributes: DeepPartial = {}): Promise { +export async function newUser(attributes: Partial = {}): Promise { const { email, password, firstName, lastName, role, ...rest } = attributes; return Container.get(UserRepository).create({ email: email ?? randomEmail(), password: await handlePasswordSetup(password), firstName: firstName ?? randomName(), lastName: lastName ?? randomName(), - role: role ?? GLOBAL_MEMBER_ROLE, + role: role ?? 'global:member', ...rest, }); } /** Store a user object in the DB */ -export async function createUser(attributes: DeepPartial = {}): Promise { +export async function createUser(attributes: Partial = {}): Promise { const userInstance = await newUser(attributes); const { user } = await Container.get(UserRepository).createUserWithProject(userInstance); return user; } -export async function createLdapUser(attributes: DeepPartial, ldapId: string): Promise { +export async function createLdapUser(attributes: Partial, ldapId: string): Promise { const user = await createUser(attributes); await Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId, 'ldap')); return user; @@ -114,7 +105,7 @@ export const addApiKey = async ( return await Container.get(PublicApiKeyService).createPublicApiKeyForUser(user, { label: randomName(), expiresAt, - scopes: scopes.length ? scopes : getApiKeyScopesForRole(user), + scopes: scopes.length ? scopes : getApiKeyScopesForRole(user.role), }); }; @@ -143,21 +134,21 @@ export async function createAdminWithApiKey({ expiresAt = null, scopes = [] }: A } export async function createOwner() { - return await createUser({ role: GLOBAL_OWNER_ROLE }); + return await createUser({ role: 'global:owner' }); } export async function createMember() { - return await createUser({ role: GLOBAL_MEMBER_ROLE }); + return await createUser({ role: 'global:member' }); } export async function createAdmin() { - return await createUser({ role: GLOBAL_ADMIN_ROLE }); + return await createUser({ role: 'global:admin' }); } -export async function createUserShell(role: Role): Promise { - const shell: DeepPartial = { role }; +export async function createUserShell(role: GlobalRole): Promise { + const shell: Partial = { role }; - if (role.slug !== GLOBAL_OWNER_ROLE.slug) { + if (role !== 'global:owner') { shell.email = randomEmail(); } @@ -170,7 +161,7 @@ export async function createUserShell(role: Role): Promise { */ export async function createManyUsers( amount: number, - attributes: DeepPartial = {}, + attributes: Partial = {}, ): Promise { const result = await Promise.all( Array(amount) @@ -185,13 +176,13 @@ export async function createManyUsers( export const getAllUsers = async () => await Container.get(UserRepository).find({ - relations: ['authIdentities', 'role'], + relations: ['authIdentities'], }); export const getUserById = async (id: string) => await Container.get(UserRepository).findOneOrFail({ where: { id }, - relations: ['authIdentities', 'role'], + relations: ['authIdentities'], }); export const getLdapIdentities = async () => @@ -201,8 +192,5 @@ export const getLdapIdentities = async () => }); export async function getGlobalOwner() { - return await Container.get(UserRepository).findOneOrFail({ - where: { role: { slug: GLOBAL_OWNER_ROLE.slug } }, - relations: ['role'], - }); + return await Container.get(UserRepository).findOneByOrFail({ role: 'global:owner' }); } diff --git a/packages/cli/test/integration/tags.api.test.ts b/packages/cli/test/integration/tags.api.test.ts index d275dc85da..b74187baf0 100644 --- a/packages/cli/test/integration/tags.api.test.ts +++ b/packages/cli/test/integration/tags.api.test.ts @@ -1,5 +1,5 @@ import { testDb } from '@n8n/backend-test-utils'; -import { GLOBAL_OWNER_ROLE, TagRepository } from '@n8n/db'; +import { TagRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { createUserShell } from './shared/db/users'; @@ -11,7 +11,7 @@ let authOwnerAgent: SuperAgentTest; const testServer = utils.setupTestServer({ endpointGroups: ['tags'] }); beforeAll(async () => { - const ownerShell = await createUserShell(GLOBAL_OWNER_ROLE); + const ownerShell = await createUserShell('global:owner'); authOwnerAgent = testServer.authAgentFor(ownerShell); }); diff --git a/packages/cli/test/integration/user.repository.test.ts b/packages/cli/test/integration/user.repository.test.ts index f4328ac372..f9caa4fe01 100644 --- a/packages/cli/test/integration/user.repository.test.ts +++ b/packages/cli/test/integration/user.repository.test.ts @@ -44,7 +44,7 @@ describe('UserRepository', () => { test('should create personal project for a user', async () => { const { user, project } = await userRepository.createUserWithProject({ email: randomEmail(), - role: { slug: 'global:member' }, + role: 'global:member', }); const projectRelation = await Container.get(ProjectRelationRepository).findOneOrFail({ diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 4685f15fb1..49167b1bc3 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -12,9 +12,6 @@ import { import type { PublicUser, User } from '@n8n/db'; import { FolderRepository, - GLOBAL_ADMIN_ROLE, - GLOBAL_MEMBER_ROLE, - GLOBAL_OWNER_ROLE, ProjectRelationRepository, ProjectRepository, SharedCredentialsRepository, @@ -64,26 +61,26 @@ describe('GET /users', () => { userRepository = Container.get(UserRepository); owner = await createUser({ - role: GLOBAL_OWNER_ROLE, + role: 'global:owner', email: 'owner@n8n.io', firstName: 'OwnerFirstName', lastName: 'OwnerLastName', }); member1 = await createUser({ - role: GLOBAL_MEMBER_ROLE, + role: 'global:member', email: 'member1@n8n.io', firstName: 'Member1FirstName', lastName: 'Member1LastName', mfaEnabled: true, }); member2 = await createUser({ - role: GLOBAL_MEMBER_ROLE, + role: 'global:member', email: 'member2@n8n.io', firstName: 'Member2FirstName', lastName: 'Member2LastName', }); await createUser({ - role: GLOBAL_ADMIN_ROLE, + role: 'global:admin', email: 'admin@n8n.io', firstName: 'AdminFirstName', lastName: 'AdminLastName', @@ -578,7 +575,7 @@ describe('GET /users', () => { let pendingUser: User; beforeAll(async () => { pendingUser = await createUser({ - role: { slug: 'global:member' }, + role: 'global:member', email: 'pending@n8n.io', firstName: 'PendingFirstName', lastName: 'PendingLastName', @@ -726,14 +723,14 @@ describe('GET /users', () => { test('should sort by firstName and lastName combined', async () => { const user1 = await createUser({ - role: { slug: 'global:member' }, + role: 'global:member', email: 'memberz1@n8n.io', firstName: 'ZZZFirstName', lastName: 'ZZZLastName', }); const user2 = await createUser({ - role: { slug: 'global:member' }, + role: 'global:member', email: 'memberz2@n8n.io', firstName: 'ZZZFirstName', lastName: 'ZZYLastName', @@ -1426,7 +1423,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(otherAdmin.id); - expect(user.role.slug).toBe('global:member'); + expect(user.role).toBe('global:member'); // restore other admin @@ -1444,7 +1441,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.role.slug).toBe('global:member'); + expect(user.role).toBe('global:member'); // restore admin @@ -1462,7 +1459,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.role.slug).toBe('global:admin'); + expect(user.role).toBe('global:admin'); // restore member @@ -1511,7 +1508,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.role.slug).toBe('global:admin'); + expect(user.role).toBe('global:admin'); // restore member @@ -1529,7 +1526,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.role.slug).toBe('global:member'); + expect(user.role).toBe('global:member'); // restore admin diff --git a/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts b/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts index 1f2a6a4fe7..0739084250 100644 --- a/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow-sharing.service.test.ts @@ -1,6 +1,6 @@ import { LicenseState } from '@n8n/backend-common'; import { createWorkflow, shareWorkflowWithUsers, testDb } from '@n8n/backend-test-utils'; -import { GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, type User } from '@n8n/db'; +import type { User } from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; @@ -17,9 +17,9 @@ let projectService: ProjectService; beforeAll(async () => { await testDb.init(); - owner = await createUser({ role: GLOBAL_OWNER_ROLE }); - member = await createUser({ role: GLOBAL_MEMBER_ROLE }); - anotherMember = await createUser({ role: GLOBAL_MEMBER_ROLE }); + owner = await createUser({ role: 'global:owner' }); + member = await createUser({ role: 'global:member' }); + anotherMember = await createUser({ role: 'global:member' }); const licenseMock = mock(); licenseMock.isSharingLicensed.mockReturnValue(true); licenseMock.getMaxTeamProjects.mockReturnValue(-1); @@ -39,6 +39,7 @@ afterAll(async () => { describe('WorkflowSharingService', () => { describe('getSharedWorkflowIds', () => { it('should show all workflows to owners', async () => { + owner.role = 'global:owner'; const workflow1 = await createWorkflow({}, member); const workflow2 = await createWorkflow({}, anotherMember); const sharedWorkflowIds = await workflowSharingService.getSharedWorkflowIds(owner, { @@ -50,6 +51,7 @@ describe('WorkflowSharingService', () => { }); it('should show shared workflows to users', async () => { + member.role = 'global:member'; const workflow1 = await createWorkflow({}, anotherMember); const workflow2 = await createWorkflow({}, anotherMember); const workflow3 = await createWorkflow({}, anotherMember); diff --git a/packages/cli/test/integration/workflows/workflows.controller-with-active-workflow-manager.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller-with-active-workflow-manager.ee.test.ts index 964b4dce48..6180e13b33 100644 --- a/packages/cli/test/integration/workflows/workflows.controller-with-active-workflow-manager.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller-with-active-workflow-manager.ee.test.ts @@ -21,7 +21,7 @@ const testServer = utils.setupTestServer({ }); beforeAll(async () => { - member = await createUser({ role: { slug: 'global:member' } }); + member = await createUser({ role: 'global:member' }); await utils.initNodeTypes(); }); diff --git a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts index f59509e48d..8ed11d94de 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts @@ -16,7 +16,6 @@ import { WorkflowHistoryRepository, SharedWorkflowRepository, WorkflowRepository, - GLOBAL_MEMBER_ROLE, } from '@n8n/db'; import { Container } from '@n8n/di'; import type { ProjectRole } from '@n8n/permissions'; @@ -71,9 +70,9 @@ beforeAll(async () => { owner = await createOwner(); 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, ); @@ -160,7 +159,7 @@ describe('PUT /workflows/:workflowId/share', () => { test('should allow sharing with pending users', async () => { const workflow = await createWorkflow({}, owner); - const memberShell = await createUserShell(GLOBAL_MEMBER_ROLE); + const memberShell = await createUserShell('global:member'); const memberShellPersonalProject = await projectRepository.getPersonalProjectForUserOrFail( memberShell.id, ); @@ -273,7 +272,7 @@ describe('PUT /workflows/:workflowId/share', () => { test('should not allow sharing by another non-shared member', async () => { const workflow = await createWorkflow({}, member); - const tempUser = await createUser({ role: { slug: 'global:member' } }); + const tempUser = await createUser({ role: 'global:member' }); const tempUserPersonalProject = await projectRepository.getPersonalProjectForUserOrFail( tempUser.id, ); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 3a2fccec3d..486b31f95e 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -657,7 +657,7 @@ describe('GET /workflows', () => { test('should return workflows with scopes when ?includeScopes=true', async () => { const [member1, member2] = await createManyUsers(2, { - role: { slug: 'global:member' }, + role: 'global:member', }); const teamProject = await createTeamProject(undefined, member1); @@ -1474,7 +1474,7 @@ describe('GET /workflows?includeFolders=true', () => { test('should return workflows with scopes and folders when ?includeScopes=true', async () => { const [member1, member2] = await createManyUsers(2, { - role: { slug: 'global:member' }, + role: 'global:member', }); const teamProject = await createTeamProject(undefined, member1); @@ -2165,8 +2165,6 @@ describe('PATCH /workflows/:workflowId', () => { const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload); - console.log(response.body); - const { data: { id }, } = response.body; diff --git a/packages/frontend/editor-ui/src/stores/rbac.store.ts b/packages/frontend/editor-ui/src/stores/rbac.store.ts index 1fbb2fc4f4..a528ae2adc 100644 --- a/packages/frontend/editor-ui/src/stores/rbac.store.ts +++ b/packages/frontend/editor-ui/src/stores/rbac.store.ts @@ -38,8 +38,6 @@ export const useRBACStore = defineStore(STORES.RBAC, () => { folder: {}, insights: {}, dataStore: {}, - execution: {}, - workflowTags: {}, }); function addGlobalRole(role: Role) {