mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { GLOBAL_SCOPE_MAP, type GlobalRole } from '@n8n/permissions';
|
||||
|
||||
import type { Role } from 'entities';
|
||||
|
||||
export function buildInRoleToRoleObject(role: GlobalRole): Role {
|
||||
return {
|
||||
slug: role,
|
||||
displayName: role,
|
||||
scopes: GLOBAL_SCOPE_MAP[role].map((scope) => {
|
||||
return {
|
||||
slug: scope,
|
||||
displayName: scope,
|
||||
description: null,
|
||||
};
|
||||
}),
|
||||
systemRole: true,
|
||||
roleType: 'global',
|
||||
description: `Built-in global role with ${role} permissions.`,
|
||||
};
|
||||
}
|
||||
|
||||
export const GLOBAL_OWNER_ROLE = buildInRoleToRoleObject('global:owner');
|
||||
export const GLOBAL_ADMIN_ROLE = buildInRoleToRoleObject('global:admin');
|
||||
export const GLOBAL_MEMBER_ROLE = buildInRoleToRoleObject('global:member');
|
||||
|
||||
export const GLOBAL_ROLES: Record<GlobalRole, Role> = {
|
||||
'global:owner': GLOBAL_OWNER_ROLE,
|
||||
'global:admin': GLOBAL_ADMIN_ROLE,
|
||||
'global:member': GLOBAL_MEMBER_ROLE,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -16,11 +16,7 @@ export class ProjectRelationRepository extends Repository<ProjectRelation> {
|
||||
projectId: In(projectIds),
|
||||
role: 'project:personalOwner',
|
||||
},
|
||||
relations: {
|
||||
user: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
relations: { user: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,20 +55,19 @@ export class UserRepository extends Repository<User> {
|
||||
email,
|
||||
password: Not(IsNull()),
|
||||
},
|
||||
relations: ['authIdentities', 'role'],
|
||||
relations: ['authIdentities'],
|
||||
});
|
||||
}
|
||||
|
||||
/** Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }` */
|
||||
async countUsersByRole() {
|
||||
const escapedRoleSlug = this.manager.connection.driver.escape('roleSlug');
|
||||
const rows = (await this.createQueryBuilder()
|
||||
.select([escapedRoleSlug, `COUNT(${escapedRoleSlug}) as count`])
|
||||
.groupBy(escapedRoleSlug)
|
||||
.execute()) as Array<{ roleSlug: string; count: string }>;
|
||||
.select(['role', 'COUNT(role) as count'])
|
||||
.groupBy('role')
|
||||
.execute()) as Array<{ role: string; count: string }>;
|
||||
return rows.reduce(
|
||||
(acc, row) => {
|
||||
acc[row.roleSlug] = parseInt(row.count, 10);
|
||||
acc[row.role] = parseInt(row.count, 10);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
@@ -92,25 +91,20 @@ export class UserRepository extends Repository<User> {
|
||||
const createInner = async (entityManager: EntityManager) => {
|
||||
const newUser = entityManager.create(User, user);
|
||||
const savedUser = await entityManager.save<User>(newUser);
|
||||
const userWithRole = await entityManager.findOne(User, {
|
||||
where: { id: savedUser.id },
|
||||
relations: ['role'],
|
||||
});
|
||||
if (!userWithRole) throw new Error('Failed to create user!');
|
||||
const savedProject = await entityManager.save<Project>(
|
||||
entityManager.create(Project, {
|
||||
type: 'personal',
|
||||
name: userWithRole.createPersonalProjectName(),
|
||||
name: savedUser.createPersonalProjectName(),
|
||||
}),
|
||||
);
|
||||
await entityManager.save<ProjectRelation>(
|
||||
entityManager.create(ProjectRelation, {
|
||||
projectId: savedProject.id,
|
||||
userId: userWithRole.id,
|
||||
userId: savedUser.id,
|
||||
role: 'project:personalOwner',
|
||||
}),
|
||||
);
|
||||
return { user: userWithRole, project: savedProject };
|
||||
return { user: savedUser, project: savedProject };
|
||||
};
|
||||
if (transactionManager) {
|
||||
return await createInner(transactionManager);
|
||||
@@ -133,7 +127,6 @@ export class UserRepository extends Repository<User> {
|
||||
project: { sharedWorkflows: { workflowId, role: 'workflow:owner' } },
|
||||
},
|
||||
},
|
||||
relations: ['role'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,7 +143,6 @@ export class UserRepository extends Repository<User> {
|
||||
projectId,
|
||||
},
|
||||
},
|
||||
relations: ['role'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,8 +286,6 @@ export class UserRepository extends Repository<User> {
|
||||
this.applyUserListExpand(queryBuilder, expand);
|
||||
this.applyUserListPagination(queryBuilder, take, skip);
|
||||
this.applyUserListSort(queryBuilder, sortBy);
|
||||
queryBuilder.leftJoinAndSelect('user.role', 'role');
|
||||
queryBuilder.leftJoinAndSelect('role.scopes', 'scopes');
|
||||
|
||||
return queryBuilder;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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:*",
|
||||
"*",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isApiKeyScope, type ApiKeyScope, type AuthPrincipal, type GlobalRole } from './types.ee';
|
||||
import type { ApiKeyScope, GlobalRole } from './types.ee';
|
||||
|
||||
export const OWNER_API_KEY_SCOPES: ApiKeyScope[] = [
|
||||
'user:read',
|
||||
@@ -65,46 +65,14 @@ export const MEMBER_API_KEY_SCOPES: ApiKeyScope[] = [
|
||||
'credential:delete',
|
||||
];
|
||||
|
||||
/**
|
||||
* This is a bit of a mess, because we are handing out scopes in API keys that are only
|
||||
* valid for the personal project, which is enforced in the public API, because the workflows,
|
||||
* execution endpoints are limited to the personal project.
|
||||
* This is a temporary solution until we have a better way to handle personal projects and API key scopes!
|
||||
*/
|
||||
export const API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT: ApiKeyScope[] = [
|
||||
'workflowTags:update',
|
||||
'workflowTags:list',
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:move',
|
||||
'workflow:activate',
|
||||
'workflow:deactivate',
|
||||
'execution:delete',
|
||||
'execution:read',
|
||||
'execution:list',
|
||||
'credential:create',
|
||||
'credential:move',
|
||||
'credential:delete',
|
||||
];
|
||||
|
||||
const MAP_ROLE_SCOPES: Record<GlobalRole, ApiKeyScope[]> = {
|
||||
'global:owner': OWNER_API_KEY_SCOPES,
|
||||
'global:admin': ADMIN_API_KEY_SCOPES,
|
||||
'global:member': MEMBER_API_KEY_SCOPES,
|
||||
};
|
||||
|
||||
export const getApiKeyScopesForRole = (user: AuthPrincipal) => {
|
||||
return [
|
||||
...new Set(
|
||||
user.role.scopes
|
||||
.map((scope) => scope.slug)
|
||||
.concat(API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT)
|
||||
.filter(isApiKeyScope),
|
||||
),
|
||||
];
|
||||
export const getApiKeyScopesForRole = (role: GlobalRole) => {
|
||||
return MAP_ROLE_SCOPES[role];
|
||||
};
|
||||
|
||||
export const getOwnerOnlyApiKeyScopes = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { API_KEY_RESOURCES, RESOURCES } from './constants.ee';
|
||||
import type { ApiKeyScope, Scope, ScopeInformation } from './types.ee';
|
||||
import { RESOURCES } from './constants.ee';
|
||||
import type { Scope, ScopeInformation } from './types.ee';
|
||||
|
||||
function buildResourceScopes() {
|
||||
const resourceScopes = Object.entries(RESOURCES).flatMap(([resource, operations]) => [
|
||||
@@ -11,19 +11,8 @@ function buildResourceScopes() {
|
||||
return resourceScopes;
|
||||
}
|
||||
|
||||
function buildApiKeyScopes() {
|
||||
const apiKeyScopes = Object.entries(API_KEY_RESOURCES).flatMap(([resource, operations]) => [
|
||||
...operations.map((op) => `${resource}:${op}` as const),
|
||||
]) as ApiKeyScope[];
|
||||
|
||||
return new Set(apiKeyScopes);
|
||||
}
|
||||
|
||||
export const ALL_SCOPES = buildResourceScopes();
|
||||
|
||||
// Keep the type of Scope[] to ensure that ApiKeyScopes are a subset of Scopes!
|
||||
export const ALL_API_KEY_SCOPES = buildApiKeyScopes();
|
||||
|
||||
export const scopeInformation: Partial<Record<Scope, ScopeInformation>> = {
|
||||
'annotationTag:create': {
|
||||
displayName: 'Create Annotation Tag',
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
teamRoleSchema,
|
||||
workflowSharingRoleSchema,
|
||||
} from './schemas.ee';
|
||||
import { ALL_API_KEY_SCOPES } from './scope-information';
|
||||
|
||||
export type ScopeInformation = {
|
||||
displayName: string;
|
||||
@@ -77,21 +76,12 @@ export type AllRolesMap = {
|
||||
workflow: Array<RoleObject<WorkflowSharingRole>>;
|
||||
};
|
||||
|
||||
export type DbScope = {
|
||||
slug: Scope;
|
||||
};
|
||||
|
||||
export type DbRole = {
|
||||
slug: string;
|
||||
scopes: DbScope[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an authenticated entity in the system that can have specific permissions via a role.
|
||||
* @property role - The global role this principal has
|
||||
*/
|
||||
export type AuthPrincipal = {
|
||||
role: DbRole;
|
||||
role: GlobalRole;
|
||||
};
|
||||
|
||||
// #region Public API
|
||||
@@ -111,9 +101,4 @@ type AllApiKeyScopesObject = {
|
||||
|
||||
export type ApiKeyScope = AllApiKeyScopesObject[PublicApiKeyResources];
|
||||
|
||||
export function isApiKeyScope(scope: Scope): scope is ApiKeyScope {
|
||||
// We are casting with as for runtime type checking
|
||||
return ALL_API_KEY_SCOPES.has(scope as ApiKeyScope);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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] ?? [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
|
||||
import type { AllRoleTypes, AuthPrincipal, Resource, Scope } from '../types.ee';
|
||||
import type { AllRoleTypes, Resource, Scope } from '../types.ee';
|
||||
|
||||
export const COMBINED_ROLE_MAP = Object.fromEntries(
|
||||
Object.values(ALL_ROLE_MAPS).flatMap((o: Record<string, Scope[]>) => Object.entries(o)),
|
||||
@@ -10,8 +10,6 @@ export const COMBINED_ROLE_MAP = Object.fromEntries(
|
||||
* @param role - The role to look up
|
||||
* @param filters - Optional resources to filter scopes by
|
||||
* @returns Array of matching scopes
|
||||
*
|
||||
* @deprecated Use the 'getRoleScopes' from the AuthRolesService instead.
|
||||
*/
|
||||
export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[] {
|
||||
let scopes = COMBINED_ROLE_MAP[role];
|
||||
@@ -20,22 +18,3 @@ export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[]
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets scopes for an auth principal, optionally filtered by resource types.
|
||||
* @param user - The auth principal to search scopes for
|
||||
* @param filters - Optional resources to filter scopes by
|
||||
* @returns Array of matching scopes
|
||||
*/
|
||||
export function getAuthPrincipalScopes(user: AuthPrincipal, filters?: Resource[]): Scope[] {
|
||||
if (!user.role) {
|
||||
const e = new Error('AuthPrincipal does not have a role defined');
|
||||
console.error('AuthPrincipal does not have a role defined', e);
|
||||
throw e;
|
||||
}
|
||||
let scopes = user.role.scopes.map((s) => s.slug);
|
||||
if (filters) {
|
||||
scopes = scopes.filter((s) => filters.includes(s.split(':')[0] as Resource));
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const handleEmailLogin = async (
|
||||
): Promise<User | undefined> => {
|
||||
const user = await Container.get(UserRepository).findOne({
|
||||
where: { email },
|
||||
relations: ['authIdentities', 'role'],
|
||||
relations: ['authIdentities'],
|
||||
});
|
||||
|
||||
if (user?.password && (await Container.get(PasswordUtility).compare(password, user.password))) {
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
CredentialsEntity,
|
||||
Project,
|
||||
User,
|
||||
SharedCredentials,
|
||||
ProjectRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
} from '@n8n/db';
|
||||
import { CredentialsEntity, Project, User, SharedCredentials, ProjectRepository } from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
@@ -260,11 +253,7 @@ export class ImportCredentialsCommand extends BaseCommand<z.infer<typeof flagsSc
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
const owner = await this.transactionManager.findOneBy(User, {
|
||||
role: {
|
||||
slug: GLOBAL_OWNER_ROLE.slug,
|
||||
},
|
||||
});
|
||||
const owner = await this.transactionManager.findOneBy(User, { role: 'global:owner' });
|
||||
if (!owner) {
|
||||
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
SharedWorkflowRepository,
|
||||
WorkflowRepository,
|
||||
UserRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
} from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
@@ -218,9 +217,7 @@ export class ImportWorkflowsCommand extends BaseCommand<z.infer<typeof flagsSche
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
const owner = await Container.get(UserRepository).findOneBy({
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
});
|
||||
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
|
||||
if (!owner) {
|
||||
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { LDAP_FEATURE_NAME, LDAP_DEFAULT_CONFIGURATION } from '@n8n/constants';
|
||||
import {
|
||||
AuthIdentityRepository,
|
||||
AuthProviderSyncHistoryRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
ProjectRelationRepository,
|
||||
ProjectRepository,
|
||||
SettingsRepository,
|
||||
@@ -170,10 +169,7 @@ export class Reset extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
}
|
||||
|
||||
private async getOwner() {
|
||||
const owner = await Container.get(UserRepository).findOne({
|
||||
where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
|
||||
relations: ['role'],
|
||||
});
|
||||
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
|
||||
if (!owner) {
|
||||
throw new UserError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SharedCredentialsRepository,
|
||||
SharedWorkflowRepository,
|
||||
UserRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
} from '@n8n/db';
|
||||
import { Command } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
@@ -62,9 +61,7 @@ export class Reset extends BaseCommand {
|
||||
}
|
||||
|
||||
async getInstanceOwner(): Promise<User> {
|
||||
const owner = await Container.get(UserRepository).findOneBy({
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
});
|
||||
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
|
||||
|
||||
if (owner) return owner;
|
||||
|
||||
@@ -74,9 +71,7 @@ export class Reset extends BaseCommand {
|
||||
|
||||
await Container.get(UserRepository).save(user);
|
||||
|
||||
return await Container.get(UserRepository).findOneByOrFail({
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
});
|
||||
return await Container.get(UserRepository).findOneByOrFail({ role: 'global:owner' });
|
||||
}
|
||||
|
||||
async catch(error: Error): Promise<void> {
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('ApiKeysController', () => {
|
||||
id: '123',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
mfaEnabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('AuthController', () => {
|
||||
|
||||
const member = mock<User>({
|
||||
id: '123',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
mfaEnabled: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UserUpdateRequestDto } from '@n8n/api-types';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import type { AuthenticatedRequest, User, PublicUser } from '@n8n/db';
|
||||
import { GLOBAL_OWNER_ROLE, InvalidAuthTokenRepository, UserRepository } from '@n8n/db';
|
||||
import { InvalidAuthTokenRepository, UserRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Response } from 'express';
|
||||
import { mock, anyObject } from 'jest-mock-extended';
|
||||
@@ -38,7 +38,7 @@ describe('MeController', () => {
|
||||
email: 'valid@email.com',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
mfaEnabled: false,
|
||||
});
|
||||
const payload = new UserUpdateRequestDto({
|
||||
@@ -88,7 +88,7 @@ describe('MeController', () => {
|
||||
id: '123',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
mfaEnabled: false,
|
||||
});
|
||||
const req = mock<AuthenticatedRequest>({ user });
|
||||
@@ -115,7 +115,7 @@ describe('MeController', () => {
|
||||
email: 'valid@email.com',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
mfaEnabled: true,
|
||||
});
|
||||
const req = mock<AuthenticatedRequest>({ user, browserId });
|
||||
@@ -139,7 +139,7 @@ describe('MeController', () => {
|
||||
email: 'valid@email.com',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
mfaEnabled: true,
|
||||
});
|
||||
const req = mock<AuthenticatedRequest>({ user, browserId });
|
||||
@@ -165,7 +165,7 @@ describe('MeController', () => {
|
||||
email: 'valid@email.com',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
mfaEnabled: true,
|
||||
mfaSecret: 'secret',
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types';
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import {
|
||||
type AuthenticatedRequest,
|
||||
type User,
|
||||
type PublicUser,
|
||||
type SettingsRepository,
|
||||
type UserRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
import type {
|
||||
AuthenticatedRequest,
|
||||
User,
|
||||
PublicUser,
|
||||
SettingsRepository,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import type { Response } from 'express';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
@@ -66,7 +65,7 @@ describe('OwnerController', () => {
|
||||
it('should setup the instance owner successfully', async () => {
|
||||
const user = mock<User>({
|
||||
id: 'userId',
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
authIdentities: [],
|
||||
});
|
||||
const browserId = 'test-browser-id';
|
||||
@@ -86,8 +85,7 @@ describe('OwnerController', () => {
|
||||
const result = await controller.setupOwner(req, res, payload);
|
||||
|
||||
expect(userRepository.findOneOrFail).toHaveBeenCalledWith({
|
||||
where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
|
||||
relations: ['role'],
|
||||
where: { role: 'global:owner' },
|
||||
});
|
||||
expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false });
|
||||
expect(authService.issueCookie).toHaveBeenCalledWith(res, user, false, browserId);
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('UsersController', () => {
|
||||
const request = mock<AuthenticatedRequest>({
|
||||
user: { id: '123' },
|
||||
});
|
||||
userRepository.findOne.mockResolvedValue(mock<User>({ id: '456' }));
|
||||
userRepository.findOneBy.mockResolvedValue(mock<User>({ id: '456' }));
|
||||
projectService.getUserOwnedOrAdminProjects.mockResolvedValue([]);
|
||||
|
||||
await controller.changeGlobalRole(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { Time } from '@n8n/constants';
|
||||
import type { CredentialsEntity, User } from '@n8n/db';
|
||||
import { CredentialsRepository, GLOBAL_OWNER_ROLE } from '@n8n/db';
|
||||
import { CredentialsRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import Csrf from 'csrf';
|
||||
import type { Response } from 'express';
|
||||
@@ -44,7 +44,7 @@ describe('OAuth1CredentialController', () => {
|
||||
id: '123',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
});
|
||||
const credential = mock<CredentialsEntity>({
|
||||
id: '1',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import { Time } from '@n8n/constants';
|
||||
import type { CredentialsEntity, User } from '@n8n/db';
|
||||
import { CredentialsRepository, GLOBAL_OWNER_ROLE } from '@n8n/db';
|
||||
import { CredentialsRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import Csrf from 'csrf';
|
||||
import { type Response } from 'express';
|
||||
@@ -46,7 +46,7 @@ describe('OAuth2CredentialController', () => {
|
||||
id: '123',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
});
|
||||
const credential = mock<CredentialsEntity>({
|
||||
id: '1',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { SourceControlledFile } from '@n8n/api-types';
|
||||
import {
|
||||
type Variables,
|
||||
type FolderWithWorkflowAndSubFolderCount,
|
||||
type TagEntity,
|
||||
type User,
|
||||
type FolderRepository,
|
||||
type TagRepository,
|
||||
type WorkflowEntity,
|
||||
GLOBAL_MEMBER_ROLE,
|
||||
GLOBAL_ADMIN_ROLE,
|
||||
import type {
|
||||
Variables,
|
||||
FolderWithWorkflowAndSubFolderCount,
|
||||
TagEntity,
|
||||
User,
|
||||
FolderRepository,
|
||||
TagRepository,
|
||||
WorkflowEntity,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
@@ -151,9 +149,8 @@ describe('SourceControlService', () => {
|
||||
describe('getStatus', () => {
|
||||
it('ensure updatedAt field for last deleted tag', async () => {
|
||||
// ARRANGE
|
||||
const user = mock<User>({
|
||||
role: GLOBAL_ADMIN_ROLE,
|
||||
});
|
||||
const user = mock<User>();
|
||||
user.role = 'global:admin';
|
||||
|
||||
sourceControlImportService.getRemoteVersionIdsFromFiles.mockResolvedValue([]);
|
||||
sourceControlImportService.getLocalVersionIdsFromDb.mockResolvedValue([]);
|
||||
@@ -207,9 +204,8 @@ describe('SourceControlService', () => {
|
||||
|
||||
it('ensure updatedAt field for last deleted folder', async () => {
|
||||
// ARRANGE
|
||||
const user = mock<User>({
|
||||
role: GLOBAL_ADMIN_ROLE,
|
||||
});
|
||||
const user = mock<User>();
|
||||
user.role = 'global:admin';
|
||||
|
||||
sourceControlImportService.getRemoteVersionIdsFromFiles.mockResolvedValue([]);
|
||||
sourceControlImportService.getLocalVersionIdsFromDb.mockResolvedValue([]);
|
||||
@@ -266,9 +262,8 @@ describe('SourceControlService', () => {
|
||||
|
||||
it('conflict depends on the value of `direction`', async () => {
|
||||
// ARRANGE
|
||||
const user = mock<User>({
|
||||
role: GLOBAL_ADMIN_ROLE,
|
||||
});
|
||||
const user = mock<User>();
|
||||
user.role = 'global:admin';
|
||||
|
||||
// Define a credential that does only exist locally.
|
||||
// Pulling this would delete it so it should be marked as a conflict.
|
||||
@@ -373,9 +368,8 @@ describe('SourceControlService', () => {
|
||||
|
||||
it('should throw `ForbiddenError` if direction is pull and user is not allowed to globally pull', async () => {
|
||||
// ARRANGE
|
||||
const user = mock<User>({
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
});
|
||||
const user = mock<User>();
|
||||
user.role = 'global:member';
|
||||
|
||||
// ACT
|
||||
await expect(
|
||||
@@ -393,7 +387,7 @@ describe('SourceControlService', () => {
|
||||
'should return file content for $type',
|
||||
async ({ type, id, content }) => {
|
||||
jest.spyOn(gitService, 'getFileContent').mockResolvedValue(content);
|
||||
const user = mock<User>({ id: 'user-id', role: GLOBAL_ADMIN_ROLE });
|
||||
const user = mock<User>({ id: 'user-id', role: 'global:admin' });
|
||||
|
||||
const result = await sourceControlService.getRemoteFileEntity({ user, type, id });
|
||||
|
||||
@@ -404,7 +398,7 @@ describe('SourceControlService', () => {
|
||||
it.each<SourceControlledFile['type']>(['folders', 'credential', 'tags', 'variables'])(
|
||||
'should throw an error if the file type is not handled',
|
||||
async (type) => {
|
||||
const user = mock<User>({ id: 'user-id', role: { slug: 'global:admin' } });
|
||||
const user = mock<User>({ id: 'user-id', role: 'global:admin' });
|
||||
await expect(
|
||||
sourceControlService.getRemoteFileEntity({ user, type, id: 'unknown' }),
|
||||
).rejects.toThrow(`Unsupported file type: ${type}`);
|
||||
@@ -413,7 +407,7 @@ describe('SourceControlService', () => {
|
||||
|
||||
it('should fail if the git service fails to get the file content', async () => {
|
||||
jest.spyOn(gitService, 'getFileContent').mockRejectedValue(new Error('Git service error'));
|
||||
const user = mock<User>({ id: 'user-id', role: { slug: 'global:admin' } });
|
||||
const user = mock<User>({ id: 'user-id', role: 'global:admin' });
|
||||
|
||||
await expect(
|
||||
sourceControlService.getRemoteFileEntity({ user, type: 'workflow', id: '1234' }),
|
||||
@@ -423,7 +417,7 @@ describe('SourceControlService', () => {
|
||||
it('should throw an error if the user does not have access to the project', async () => {
|
||||
const user = mock<User>({
|
||||
id: 'user-id',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
});
|
||||
jest
|
||||
.spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext')
|
||||
@@ -435,7 +429,7 @@ describe('SourceControlService', () => {
|
||||
});
|
||||
|
||||
it('should return content for an authorized workflow', async () => {
|
||||
const user = mock<User>({ id: 'user-id', role: { slug: 'global:member' } });
|
||||
const user = mock<User>({ id: 'user-id', role: 'global:member' });
|
||||
jest
|
||||
.spyOn(sourceControlScopedService, 'getWorkflowsInAdminProjectsFromContext')
|
||||
.mockResolvedValue([{ id: '1234' } as WorkflowEntity]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GLOBAL_OWNER_ROLE, type IWorkflowDb } from '@n8n/db';
|
||||
import type { IWorkflowDb } from '@n8n/db';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { InstanceSettings } from 'n8n-core';
|
||||
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
|
||||
@@ -27,7 +27,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'john@n8n.io',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: 'owner' },
|
||||
role: 'owner',
|
||||
},
|
||||
workflow: mock<IWorkflowBase>({
|
||||
id: 'wf123',
|
||||
@@ -61,7 +61,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'jane@n8n.io',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
role: { slug: 'user' },
|
||||
role: 'user',
|
||||
},
|
||||
workflowId: 'wf789',
|
||||
publicApi: false,
|
||||
@@ -89,7 +89,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'jane@n8n.io',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
role: { slug: 'user' },
|
||||
role: 'user',
|
||||
},
|
||||
workflowId: 'wf789',
|
||||
publicApi: false,
|
||||
@@ -117,7 +117,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'jane@n8n.io',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
role: { slug: 'user' },
|
||||
role: 'user',
|
||||
},
|
||||
workflowId: 'wf789',
|
||||
publicApi: false,
|
||||
@@ -145,7 +145,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'alex@n8n.io',
|
||||
firstName: 'Alex',
|
||||
lastName: 'Johnson',
|
||||
role: { slug: 'editor' },
|
||||
role: 'editor',
|
||||
},
|
||||
workflow: mock<IWorkflowDb>({ id: 'wf101', name: 'Updated Workflow' }),
|
||||
publicApi: false,
|
||||
@@ -347,7 +347,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'updated@example.com',
|
||||
firstName: 'Updated',
|
||||
lastName: 'User',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
},
|
||||
fieldsChanged: ['firstName', 'lastName', 'password'],
|
||||
};
|
||||
@@ -374,7 +374,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'john@n8n.io',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: 'some-role' },
|
||||
role: 'some-role',
|
||||
},
|
||||
targetUserOldStatus: 'active',
|
||||
publicApi: false,
|
||||
@@ -404,7 +404,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'inviter@example.com',
|
||||
firstName: 'Inviter',
|
||||
lastName: 'User',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
targetUserId: ['newUser123'],
|
||||
publicApi: false,
|
||||
@@ -434,7 +434,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'reinviter@example.com',
|
||||
firstName: 'Reinviter',
|
||||
lastName: 'User',
|
||||
role: { slug: 'global:admin' },
|
||||
role: 'global:admin',
|
||||
},
|
||||
targetUserId: ['existingUser456'],
|
||||
};
|
||||
@@ -461,7 +461,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'newuser@example.com',
|
||||
firstName: 'New',
|
||||
lastName: 'User',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
},
|
||||
userType: 'email',
|
||||
wasDisabledLdapUser: false,
|
||||
@@ -488,7 +488,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'loggedin@example.com',
|
||||
firstName: 'Logged',
|
||||
lastName: 'In',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
authenticationMethod: 'email',
|
||||
};
|
||||
@@ -517,7 +517,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'user101@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -542,14 +542,14 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'john@n8n.io',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: 'some-role' },
|
||||
role: 'some-role',
|
||||
},
|
||||
invitee: {
|
||||
id: '456',
|
||||
email: 'jane@n8n.io',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
role: { slug: 'some-other-role' },
|
||||
role: 'some-other-role',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -583,7 +583,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'resetuser@example.com',
|
||||
firstName: 'Reset',
|
||||
lastName: 'User',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -708,7 +708,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'sharer@example.com',
|
||||
firstName: 'Alice',
|
||||
lastName: 'Sharer',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialId: 'cred789',
|
||||
credentialType: 'githubApi',
|
||||
@@ -743,7 +743,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialType: 'githubApi',
|
||||
credentialId: 'cred456',
|
||||
@@ -778,7 +778,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'creduser@example.com',
|
||||
firstName: 'Cred',
|
||||
lastName: 'User',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialId: 'cred789',
|
||||
credentialType: 'githubApi',
|
||||
@@ -807,7 +807,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'updatecred@example.com',
|
||||
firstName: 'Update',
|
||||
lastName: 'Cred',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialId: 'cred101',
|
||||
credentialType: 'slackApi',
|
||||
@@ -859,7 +859,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'packageupdater@example.com',
|
||||
firstName: 'Package',
|
||||
lastName: 'Updater',
|
||||
role: { slug: 'global:admin' },
|
||||
role: 'global:admin',
|
||||
},
|
||||
packageName: 'n8n-nodes-awesome-package',
|
||||
packageVersionCurrent: '1.0.0',
|
||||
@@ -896,7 +896,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'admin@example.com',
|
||||
firstName: 'Admin',
|
||||
lastName: 'User',
|
||||
role: { slug: 'global:admin' },
|
||||
role: 'global:admin',
|
||||
},
|
||||
inputString: 'n8n-nodes-custom-package',
|
||||
packageName: 'n8n-nodes-custom-package',
|
||||
@@ -935,7 +935,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'packagedeleter@example.com',
|
||||
firstName: 'Package',
|
||||
lastName: 'Deleter',
|
||||
role: { slug: 'global:admin' },
|
||||
role: 'global:admin',
|
||||
},
|
||||
packageName: 'n8n-nodes-awesome-package',
|
||||
packageVersion: '1.0.0',
|
||||
@@ -972,7 +972,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'recipient@example.com',
|
||||
firstName: 'Failed',
|
||||
lastName: 'Recipient',
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
},
|
||||
messageType: 'New user invite',
|
||||
publicApi: false,
|
||||
@@ -1002,7 +1002,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'apiuser@example.com',
|
||||
firstName: 'API',
|
||||
lastName: 'User',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
publicApi: true,
|
||||
};
|
||||
@@ -1028,7 +1028,7 @@ describe('LogStreamingEventRelay', () => {
|
||||
email: 'apiuser@example.com',
|
||||
firstName: 'API',
|
||||
lastName: 'User',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
publicApi: true,
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { NodeTypes } from '@/node-types';
|
||||
import { mockInstance } from '@n8n/backend-test-utils';
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
import {
|
||||
type CredentialsEntity,
|
||||
type WorkflowEntity,
|
||||
type IWorkflowDb,
|
||||
type CredentialsRepository,
|
||||
type ProjectRelationRepository,
|
||||
type SharedWorkflowRepository,
|
||||
type WorkflowRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
import type {
|
||||
CredentialsEntity,
|
||||
WorkflowEntity,
|
||||
IWorkflowDb,
|
||||
CredentialsRepository,
|
||||
ProjectRelationRepository,
|
||||
SharedWorkflowRepository,
|
||||
WorkflowRepository,
|
||||
} from '@n8n/db';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { type BinaryDataConfig, InstanceSettings } from 'n8n-core';
|
||||
@@ -382,7 +381,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
publicApi: true,
|
||||
};
|
||||
@@ -402,7 +401,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
publicApi: true,
|
||||
};
|
||||
@@ -424,7 +423,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
inputString: 'n8n-nodes-package',
|
||||
packageName: 'n8n-nodes-package',
|
||||
@@ -457,7 +456,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
packageName: 'n8n-nodes-package',
|
||||
packageVersionCurrent: '1.0.0',
|
||||
@@ -487,7 +486,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
packageName: 'n8n-nodes-package',
|
||||
packageVersion: '1.0.0',
|
||||
@@ -517,7 +516,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialType: 'github',
|
||||
credentialId: 'cred123',
|
||||
@@ -544,7 +543,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialType: 'github',
|
||||
credentialId: 'cred123',
|
||||
@@ -572,7 +571,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialId: 'cred123',
|
||||
credentialType: 'github',
|
||||
@@ -594,7 +593,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
credentialId: 'cred123',
|
||||
credentialType: 'github',
|
||||
@@ -690,7 +689,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
workflow: mock<IWorkflowBase>({ id: 'workflow123', name: 'Test Workflow', nodes: [] }),
|
||||
publicApi: false,
|
||||
@@ -719,7 +718,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
workflowId: 'workflow123',
|
||||
publicApi: false,
|
||||
@@ -741,7 +740,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
workflowId: 'workflow123',
|
||||
publicApi: false,
|
||||
@@ -763,7 +762,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
workflowId: 'workflow123',
|
||||
publicApi: false,
|
||||
@@ -809,7 +808,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
workflow: mock<IWorkflowDb>({ id: 'workflow123', name: 'Test Workflow', nodes: [] }),
|
||||
publicApi: false,
|
||||
@@ -857,7 +856,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
fieldsChanged: ['firstName', 'lastName'],
|
||||
};
|
||||
@@ -877,7 +876,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
publicApi: false,
|
||||
targetUserOldStatus: 'active',
|
||||
@@ -905,7 +904,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
targetUserId: ['user456'],
|
||||
publicApi: false,
|
||||
@@ -931,7 +930,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
userType: 'email',
|
||||
wasDisabledLdapUser: false,
|
||||
@@ -1149,7 +1148,7 @@ describe('TelemetryEventRelay', () => {
|
||||
email: 'user@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: { slug: GLOBAL_OWNER_ROLE.slug },
|
||||
role: 'global:owner',
|
||||
},
|
||||
messageType: 'New user invite',
|
||||
publicApi: false,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
type Project,
|
||||
type User,
|
||||
type SharedCredentialsRepository,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
} from '@n8n/db';
|
||||
import type { Project, User, SharedCredentialsRepository } from '@n8n/db';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
|
||||
@@ -98,7 +93,7 @@ describe('CredentialsPermissionChecker', () => {
|
||||
});
|
||||
|
||||
it('should skip credential checks if the home project owner has global scope', async () => {
|
||||
const projectOwner = mock<User>({ role: GLOBAL_OWNER_ROLE });
|
||||
const projectOwner = mock<User>({ role: 'global:owner' });
|
||||
ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner);
|
||||
|
||||
await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow();
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
AuthIdentityRepository,
|
||||
AuthProviderSyncHistoryRepository,
|
||||
UserRepository,
|
||||
GLOBAL_MEMBER_ROLE,
|
||||
} from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { validate } from 'jsonschema';
|
||||
@@ -92,22 +91,6 @@ export const getAuthIdentityByLdapId = async (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve user by LDAP ID from database
|
||||
* @param idAttributeValue - LDAP ID value
|
||||
*/
|
||||
export const getUserByLdapId = async (idAttributeValue: string) => {
|
||||
return await Container.get(UserRepository).findOne({
|
||||
relations: { role: true },
|
||||
where: {
|
||||
authIdentities: {
|
||||
providerId: idAttributeValue,
|
||||
providerType: 'ldap',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserByEmail = async (email: string): Promise<User | null> => {
|
||||
return await Container.get(UserRepository).findOne({
|
||||
where: { email },
|
||||
@@ -167,7 +150,7 @@ export const mapLdapUserToDbUser = (
|
||||
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
|
||||
Object.assign(user, data);
|
||||
if (toCreate) {
|
||||
user.role = GLOBAL_MEMBER_ROLE;
|
||||
user.role = 'global:member';
|
||||
user.password = randomString(8);
|
||||
user.disabled = false;
|
||||
} else {
|
||||
@@ -287,7 +270,7 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
|
||||
export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => {
|
||||
const { user } = await Container.get(UserRepository).createUserWithProject({
|
||||
password: randomString(8),
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
role: 'global:member',
|
||||
...data,
|
||||
});
|
||||
await createLdapAuthIdentity(user, ldapId);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function getSharedWorkflow(
|
||||
): Promise<SharedWorkflow | null> {
|
||||
return await Container.get(SharedWorkflowRepository).findOne({
|
||||
where: {
|
||||
...(!['global:owner', 'global:admin'].includes(user.role.slug) && { userId: user.id }),
|
||||
...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }),
|
||||
...(workflowId && { workflowId }),
|
||||
},
|
||||
relations: [
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ListQueryDb,
|
||||
WorkflowHistory,
|
||||
} from '@n8n/db';
|
||||
import type { AssignableGlobalRole, ProjectRole, Scope } from '@n8n/permissions';
|
||||
import type { AssignableGlobalRole, GlobalRole, ProjectRole, Scope } from '@n8n/permissions';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
INodeCredentialTestRequest,
|
||||
@@ -268,7 +268,9 @@ export declare namespace ActiveWorkflowRequest {
|
||||
// ----------------------------------
|
||||
|
||||
export declare namespace ProjectRequest {
|
||||
type GetMyProjectsResponse = Array<Project & { role: string; scopes?: Scope[] }>;
|
||||
type GetMyProjectsResponse = Array<
|
||||
Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }
|
||||
>;
|
||||
|
||||
type ProjectRelationResponse = {
|
||||
id: string;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GLOBAL_MEMBER_ROLE, GLOBAL_OWNER_ROLE, SharedCredentials } from '@n8n/db';
|
||||
import { SharedCredentials } from '@n8n/db';
|
||||
import type { CredentialsEntity, User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { In } from '@n8n/typeorm';
|
||||
@@ -16,10 +16,10 @@ describe('CredentialsFinderService', () => {
|
||||
const sharedCredential = mock<SharedCredentials>();
|
||||
sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId });
|
||||
const owner = mock<User>({
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
});
|
||||
const member = mock<User>({
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
role: 'global:member',
|
||||
id: 'test',
|
||||
});
|
||||
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { AllRoleTypes, Scope } from '@n8n/permissions';
|
||||
import { ALL_ROLES, combineScopes, getAuthPrincipalScopes, getRoleScopes } from '@n8n/permissions';
|
||||
import { ALL_ROLES, combineScopes, getRoleScopes } from '@n8n/permissions';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
import { License } from '@/license';
|
||||
@@ -89,7 +89,7 @@ export class RoleService {
|
||||
shared: SharedCredentials[] | SharedWorkflow[],
|
||||
userProjectRelations: ProjectRelation[],
|
||||
): Scope[] {
|
||||
const globalScopes = getAuthPrincipalScopes(user, [type]);
|
||||
const globalScopes = getRoleScopes(user.role, [type]);
|
||||
const scopesSet: Set<Scope> = new Set(globalScopes);
|
||||
for (const sharedEntity of shared) {
|
||||
const pr = userProjectRelations.find(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GLOBAL_MEMBER_ROLE, type User } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import { type Request, type Response } from 'express';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
@@ -21,7 +21,7 @@ const user = mock<User>({
|
||||
lastName: 'User',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
role: 'global:member',
|
||||
});
|
||||
|
||||
describe('OidcController', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
//
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GLOBAL_OWNER_ROLE, type User } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import { type Response } from 'express';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
@@ -30,7 +30,7 @@ const user = mock<User>({
|
||||
id: '123',
|
||||
password: 'password',
|
||||
authIdentities: [],
|
||||
role: GLOBAL_OWNER_ROLE,
|
||||
role: 'global:owner',
|
||||
});
|
||||
|
||||
const attributes: SamlUserAttributes = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: '/' });
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(), {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,12 +7,7 @@ import {
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { LDAP_DEFAULT_CONFIGURATION } from '@n8n/constants';
|
||||
import type { User } from '@n8n/db';
|
||||
import {
|
||||
AuthProviderSyncHistoryRepository,
|
||||
GLOBAL_MEMBER_ROLE,
|
||||
GLOBAL_OWNER_ROLE,
|
||||
UserRepository,
|
||||
} from '@n8n/db';
|
||||
import { AuthProviderSyncHistoryRepository, UserRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Not } from '@n8n/typeorm';
|
||||
import type { Entry as LdapUser } from 'ldapts';
|
||||
@@ -42,7 +37,7 @@ const testServer = utils.setupTestServer({
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await createUser({ role: GLOBAL_OWNER_ROLE });
|
||||
owner = await createUser({ role: 'global:owner' });
|
||||
authOwnerAgent = testServer.authAgentFor(owner);
|
||||
|
||||
defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
|
||||
@@ -70,7 +65,7 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
test('Member role should not be able to access ldap routes', async () => {
|
||||
const member = await createUser({ role: { slug: 'global:member' } });
|
||||
const member = await createUser({ role: 'global:member' });
|
||||
const authAgent = testServer.authAgentFor(member);
|
||||
await authAgent.get('/ldap/config').expect(403);
|
||||
await authAgent.put('/ldap/config').expect(403);
|
||||
@@ -142,7 +137,7 @@ describe('PUT /ldap/config', () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
|
||||
|
||||
const configuration = ldapConfig;
|
||||
|
||||
@@ -255,7 +250,7 @@ describe('POST /ldap/sync', () => {
|
||||
const ldapUserId = uniqueId();
|
||||
|
||||
const member = await createLdapUser(
|
||||
{ role: { slug: 'global:member' }, email: ldapUserEmail },
|
||||
{ role: 'global:member', email: ldapUserEmail },
|
||||
ldapUserId,
|
||||
);
|
||||
|
||||
@@ -284,7 +279,7 @@ describe('POST /ldap/sync', () => {
|
||||
const ldapUserId = uniqueId();
|
||||
|
||||
const member = await createLdapUser(
|
||||
{ role: { slug: 'global:member' }, email: ldapUserEmail },
|
||||
{ role: 'global:member', email: ldapUserEmail },
|
||||
ldapUserId,
|
||||
);
|
||||
|
||||
@@ -369,7 +364,7 @@ describe('POST /ldap/sync', () => {
|
||||
|
||||
await createLdapUser(
|
||||
{
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
email: ldapUser.mail,
|
||||
firstName: ldapUser.givenName,
|
||||
lastName: randomName(),
|
||||
@@ -402,7 +397,7 @@ describe('POST /ldap/sync', () => {
|
||||
|
||||
await createLdapUser(
|
||||
{
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
email: ldapUser.mail,
|
||||
firstName: ldapUser.givenName,
|
||||
lastName: ldapUser.sn,
|
||||
@@ -431,7 +426,7 @@ describe('POST /ldap/sync', () => {
|
||||
});
|
||||
|
||||
test('should remove user instance access once the user is disabled during synchronization', async () => {
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
|
||||
|
||||
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]);
|
||||
|
||||
@@ -490,7 +485,7 @@ describe('POST /ldap/sync', () => {
|
||||
// Create user with valid email first
|
||||
await createLdapUser(
|
||||
{
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
role: 'global:member',
|
||||
email: originalEmail,
|
||||
firstName: randomName(),
|
||||
lastName: randomName(),
|
||||
@@ -608,7 +603,7 @@ describe('POST /login', () => {
|
||||
|
||||
await createLdapUser(
|
||||
{
|
||||
role: { slug: 'global:member' },
|
||||
role: 'global:member',
|
||||
email: ldapUser.mail,
|
||||
firstName: 'firstname',
|
||||
lastName: 'lastname',
|
||||
@@ -642,7 +637,7 @@ describe('POST /login', () => {
|
||||
};
|
||||
|
||||
await createUser({
|
||||
role: GLOBAL_MEMBER_ROLE,
|
||||
role: 'global:member',
|
||||
email: ldapUser.mail,
|
||||
firstName: ldapUser.givenName,
|
||||
lastName: 'lastname',
|
||||
@@ -657,7 +652,7 @@ describe('Instance owner should able to delete LDAP users', () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
|
||||
|
||||
await authOwnerAgent.post(`/users/${member.id}`);
|
||||
});
|
||||
@@ -666,7 +661,7 @@ describe('Instance owner should able to delete LDAP users', () => {
|
||||
const ldapConfig = await createLdapConfig();
|
||||
Container.get(LdapService).setConfig(ldapConfig);
|
||||
|
||||
const member = await createLdapUser({ role: { slug: 'global:member' } }, uniqueId());
|
||||
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
|
||||
|
||||
// delete the LDAP member and transfer its workflows/credentials to instance owner
|
||||
await authOwnerAgent.post(`/users/${member.id}?transferId=${owner.id}`);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user