refactor(core): Remove roleId indirection (no-changelog) (#8413)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-01-24 13:38:57 +01:00 committed by GitHub
parent 1affebd85e
commit d6deceacde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 922 additions and 1684 deletions

View file

@ -84,7 +84,7 @@ export class ActiveWebhooks implements IWebhookManager {
const workflowData = await this.workflowRepository.findOne({
where: { id: webhook.workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
relations: ['shared', 'shared.user'],
});
if (workflowData === null) {

View file

@ -229,7 +229,7 @@ export class ActiveWorkflowRunner {
async clearWebhooks(workflowId: string) {
const workflowData = await this.workflowRepository.findOne({
where: { id: workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
relations: ['shared', 'shared.user'],
});
if (workflowData === null) {
@ -615,7 +615,7 @@ export class ActiveWorkflowRunner {
);
}
const sharing = dbWorkflow.shared.find((shared) => shared.role.name === 'owner');
const sharing = dbWorkflow.shared.find((shared) => shared.role === 'workflow:owner');
if (!sharing) {
throw new WorkflowActivationError(`Workflow ${dbWorkflow.display()} has no owner`);

View file

@ -786,15 +786,9 @@ export class CredentialsHelper extends ICredentialsHelper {
const credential = await this.sharedCredentialsRepository.findOne({
where: {
role: {
scope: 'credential',
name: 'owner',
},
role: 'credential:owner',
user: {
globalRole: {
scope: 'global',
name: 'owner',
},
role: 'global:owner',
},
credentials: {
id: nodeCredential.id,

View file

@ -97,6 +97,16 @@ export function getConnectionOptions(dbType: DatabaseType): ConnectionOptions {
}
}
export async function setSchema(conn: Connection) {
const schema = config.getEnv('database.postgresdb.schema');
const searchPath = ['public'];
if (schema !== 'public') {
await conn.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
searchPath.unshift(schema);
}
await conn.query(`SET search_path TO ${searchPath.join(',')};`);
}
export async function init(testConnectionOptions?: ConnectionOptions): Promise<void> {
if (connectionState.connected) return;
@ -130,13 +140,7 @@ export async function init(testConnectionOptions?: ConnectionOptions): Promise<v
await connection.initialize();
if (dbType === 'postgresdb') {
const schema = config.getEnv('database.postgresdb.schema');
const searchPath = ['public'];
if (schema !== 'public') {
await connection.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
searchPath.unshift(schema);
}
await connection.query(`SET search_path TO ${searchPath.join(',')};`);
await setSchema(connection);
}
connectionState.connected = true;

View file

@ -35,10 +35,9 @@ import type { ChildProcess } from 'child_process';
import type { DatabaseType } from '@db/types';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { Role } from '@db/entities/Role';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User';
import type { GlobalRole, User } from '@db/entities/User';
import type { CredentialsRepository } from '@db/repositories/credentials.repository';
import type { SettingsRepository } from '@db/repositories/settings.repository';
import type { UserRepository } from '@db/repositories/user.repository';
@ -681,7 +680,7 @@ export interface PublicUser {
createdAt: Date;
isPending: boolean;
hasRecoveryCodesLeft: boolean;
globalRole?: Role;
role?: GlobalRole;
globalScopes?: Scope[];
signInType: AuthProviderType;
disabled: boolean;

View file

@ -22,11 +22,11 @@ import { Telemetry } from '@/telemetry';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import { eventBus } from './eventbus';
import { EventsService } from '@/services/events.service';
import type { User } from '@db/entities/User';
import type { GlobalRole, User } from '@db/entities/User';
import { N8N_VERSION } from '@/constants';
import { NodeTypes } from './NodeTypes';
import { NodeTypes } from '@/NodeTypes';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
import { RoleService } from './services/role.service';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
import { InstanceSettings } from 'n8n-core';
@ -36,14 +36,14 @@ function userToPayload(user: User): {
_email: string;
_firstName: string;
_lastName: string;
globalRole?: string;
globalRole: GlobalRole;
} {
return {
userId: user.id,
_email: user.email,
_firstName: user.firstName,
_lastName: user.lastName,
globalRole: user.globalRole?.name,
globalRole: user.role,
};
}
@ -52,7 +52,7 @@ export class InternalHooks {
constructor(
private telemetry: Telemetry,
private nodeTypes: NodeTypes,
private roleService: RoleService,
private sharedWorkflowRepository: SharedWorkflowRepository,
eventsService: EventsService,
private readonly instanceSettings: InstanceSettings,
) {
@ -166,9 +166,9 @@ export class InternalHooks {
let userRole: 'owner' | 'sharee' | undefined = undefined;
if (user.id && workflow.id) {
const role = await this.roleService.findRoleByUserAndWorkflow(user.id, workflow.id);
const role = await this.sharedWorkflowRepository.findSharingRole(user.id, workflow.id);
if (role) {
userRole = role.name === 'owner' ? 'owner' : 'sharee';
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
}
}
@ -371,9 +371,9 @@ export class InternalHooks {
let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId) {
const role = await this.roleService.findRoleByUserAndWorkflow(userId, workflow.id);
const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
if (role) {
userRole = role.name === 'owner' ? 'owner' : 'sharee';
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
}
}

View file

@ -5,7 +5,6 @@ import { Container } from 'typedi';
import { validate } from 'jsonschema';
import * as Db from '@/Db';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import { User } from '@db/entities/User';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
@ -18,7 +17,6 @@ import {
} from './constants';
import type { ConnectionSecurity, LdapConfig } from './types';
import { License } from '@/License';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository';
import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
@ -47,13 +45,6 @@ export const randomPassword = (): string => {
return Math.random().toString(36).slice(-8);
};
/**
* Return the user role to be assigned to LDAP users
*/
export const getLdapUserRole = async (): Promise<Role> => {
return await Container.get(RoleService).findGlobalMemberRole();
};
/**
* Validate the structure of the LDAP configuration schema
*/
@ -102,7 +93,7 @@ export const getAuthIdentityByLdapId = async (
idAttributeValue: string,
): Promise<AuthIdentity | null> => {
return await Container.get(AuthIdentityRepository).findOne({
relations: ['user', 'user.globalRole'],
relations: ['user'],
where: {
providerId: idAttributeValue,
providerType: 'ldap',
@ -113,7 +104,6 @@ export const getAuthIdentityByLdapId = async (
export const getUserByEmail = async (email: string): Promise<User | null> => {
return await Container.get(UserRepository).findOne({
where: { email },
relations: ['globalRole'],
});
};
@ -164,13 +154,13 @@ export const getLdapUsers = async (): Promise<User[]> => {
export const mapLdapUserToDbUser = (
ldapUser: LdapUser,
ldapConfig: LdapConfig,
role?: Role,
toCreate = false,
): [string, User] => {
const user = new User();
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
Object.assign(user, data);
if (role) {
user.globalRole = role;
if (toCreate) {
user.role = 'global:member';
user.password = randomPassword();
user.disabled = false;
} else {
@ -270,10 +260,10 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
return await Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId));
};
export const createLdapUserOnLocalDb = async (role: Role, data: Partial<User>, ldapId: string) => {
export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => {
const user = await Container.get(UserRepository).save({
password: randomPassword(),
globalRole: role,
role: 'global:member',
...data,
});
await createLdapAuthIdentity(user, ldapId);

View file

@ -7,7 +7,6 @@ import { ApplicationError, jsonParse } from 'n8n-workflow';
import { Cipher } from 'n8n-core';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
import { SettingsRepository } from '@db/repositories/settings.repository';
@ -30,7 +29,6 @@ import {
escapeFilter,
formatUrl,
getLdapIds,
getLdapUserRole,
getLdapUsers,
getMappingAttributes,
mapLdapUserToDbUser,
@ -346,12 +344,9 @@ export class LdapService {
const localAdUsers = await getLdapIds();
const role = await getLdapUserRole();
const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess(
adUsers,
localAdUsers,
role,
);
this.logger.debug('LDAP - Users processed', {
@ -407,14 +402,13 @@ export class LdapService {
private getUsersToProcess(
adUsers: LdapUser[],
localAdUsers: string[],
role: Role,
): {
usersToCreate: Array<[string, User]>;
usersToUpdate: Array<[string, User]>;
usersToDisable: string[];
} {
return {
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers, role),
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers),
usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers),
usersToDisable: this.getUsersToDisable(adUsers, localAdUsers),
};
@ -424,11 +418,10 @@ export class LdapService {
private getUsersToCreate(
remoteAdUsers: LdapUser[],
localLdapIds: string[],
role: Role,
): Array<[string, User]> {
return remoteAdUsers
.filter((adUser) => !localLdapIds.includes(adUser[this.config.ldapIdAttribute] as string))
.map((adUser) => mapLdapUserToDbUser(adUser, this.config, role));
.map((adUser) => mapLdapUserToDbUser(adUser, this.config, true));
}
/** Get users in LDAP that are already in the database */

View file

@ -98,7 +98,6 @@ async function createApiRouter(
const apiKey = req.headers[schema.name.toLowerCase()] as string;
const user = await Container.get(UserRepository).findOne({
where: { apiKey },
relations: ['globalRole'],
});
if (!user) return false;

View file

@ -3,8 +3,6 @@ import type { IDataObject, ExecutionStatus } from 'n8n-workflow';
import type { User } from '@db/entities/User';
import type { Role } from '@db/entities/Role';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { UserManagementMailer } from '@/UserManagement/email';
@ -25,7 +23,6 @@ export type AuthenticatedRequest<
RequestQuery = {},
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
user: User;
globalMemberRole?: Role;
mailer?: UserManagementMailer;
};

View file

@ -5,7 +5,7 @@ import Container from 'typedi';
export = {
generateAudit: [
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (req: AuditRequest.Generate, res: Response): Promise<Response> => {
try {
const { SecurityAuditService } = await import('@/security-audit/SecurityAudit.service');

View file

@ -23,7 +23,7 @@ import { Container } from 'typedi';
export = {
createCredential: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
validCredentialType,
validCredentialsProperties,
async (
@ -47,7 +47,7 @@ export = {
},
],
deleteCredential: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (
req: CredentialRequest.Delete,
res: express.Response,
@ -55,13 +55,10 @@ export = {
const { id: credentialId } = req.params;
let credential: CredentialsEntity | undefined;
if (!['owner', 'admin'].includes(req.user.globalRole.name)) {
const shared = await getSharedCredentials(req.user.id, credentialId, [
'credentials',
'role',
]);
if (!['global:owner', 'global:admin'].includes(req.user.role)) {
const shared = await getSharedCredentials(req.user.id, credentialId);
if (shared?.role.name === 'owner') {
if (shared?.role === 'credential:owner') {
credential = shared.credentials;
}
} else {
@ -78,7 +75,7 @@ export = {
],
getCredentialType: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: CredentialTypeRequest.Get, res: express.Response): Promise<express.Response> => {
const { credentialTypeName } = req.params;

View file

@ -9,7 +9,6 @@ import { ExternalHooks } from '@/ExternalHooks';
import type { IDependency, IJsonSchema } from '../../../types';
import type { CredentialRequest } from '@/requests';
import { Container } from 'typedi';
import { RoleService } from '@/services/role.service';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
@ -20,14 +19,13 @@ export async function getCredentials(credentialId: string): Promise<ICredentials
export async function getSharedCredentials(
userId: string,
credentialId: string,
relations?: string[],
): Promise<SharedCredentials | null> {
return await Container.get(SharedCredentialsRepository).findOne({
where: {
userId,
credentialsId: credentialId,
},
relations,
relations: ['credentials'],
});
}
@ -60,8 +58,6 @@ export async function saveCredential(
user: User,
encryptedData: ICredentialsDb,
): Promise<CredentialsEntity> {
const role = await Container.get(RoleService).findCredentialOwnerRole();
await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
return await Db.transaction(async (transactionManager) => {
@ -72,7 +68,7 @@ export async function saveCredential(
const newSharedCredential = new SharedCredentials();
Object.assign(newSharedCredential, {
role,
role: 'credential:owner',
user,
credentials: savedCredential,
});

View file

@ -12,7 +12,7 @@ import { ExecutionRepository } from '@db/repositories/execution.repository';
export = {
deleteExecution: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: ExecutionRequest.Delete, res: express.Response): Promise<express.Response> => {
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
@ -44,7 +44,7 @@ export = {
},
],
getExecution: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: ExecutionRequest.Get, res: express.Response): Promise<express.Response> => {
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
@ -75,7 +75,7 @@ export = {
},
],
getExecutions: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
validCursor,
async (req: ExecutionRequest.GetAll, res: express.Response): Promise<express.Response> => {
const {

View file

@ -14,7 +14,7 @@ import { InternalHooks } from '@/InternalHooks';
export = {
pull: [
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (
req: PublicSourceControlRequest.Pull,
res: express.Response,

View file

@ -36,5 +36,7 @@ properties:
description: Last time the user was updated.
format: date-time
readOnly: true
globalRole:
$ref: './role.yml'
role:
type: string
example: owner
readOnly: true

View file

@ -15,7 +15,7 @@ import { InternalHooks } from '@/InternalHooks';
export = {
getUser: [
validLicenseWithUserQuota,
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (req: UserRequest.Get, res: express.Response) => {
const { includeRole = false } = req.query;
const { id } = req.params;
@ -41,7 +41,7 @@ export = {
getUsers: [
validLicenseWithUserQuota,
validCursor,
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (req: UserRequest.Get, res: express.Response) => {
const { offset = 0, limit = 100, includeRole = false } = req.query;

View file

@ -4,24 +4,21 @@ import type { User } from '@db/entities/User';
import pick from 'lodash/pick';
import { validate as uuidValidate } from 'uuid';
export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
return {
user: ['id', 'email', 'firstName', 'lastName', 'createdAt', 'updatedAt', 'isPending'],
role: ['id', 'name', 'scope', 'createdAt', 'updatedAt'],
}[table];
};
export async function getUser(data: {
withIdentifier: string;
includeRole?: boolean;
}): Promise<User | null> {
return await Container.get(UserRepository).findOne({
where: {
...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }),
...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }),
},
relations: data?.includeRole ? ['globalRole'] : undefined,
});
return await Container.get(UserRepository)
.findOne({
where: {
...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }),
...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }),
},
})
.then((user) => {
if (user && !data?.includeRole) delete (user as Partial<User>).role;
return user;
});
}
export async function getAllUsersAndCount(data: {
@ -31,19 +28,29 @@ export async function getAllUsersAndCount(data: {
}): Promise<[User[], number]> {
const users = await Container.get(UserRepository).find({
where: {},
relations: data?.includeRole ? ['globalRole'] : undefined,
skip: data.offset,
take: data.limit,
});
if (!data?.includeRole) {
users.forEach((user) => {
delete (user as Partial<User>).role;
});
}
const count = await Container.get(UserRepository).count();
return [users, count];
}
const userProperties = [
'id',
'email',
'firstName',
'lastName',
'createdAt',
'updatedAt',
'isPending',
];
function pickUserSelectableProperties(user: User, options?: { includeRole: boolean }) {
return pick(
user,
getSelectableProperties('user').concat(options?.includeRole ? ['globalRole'] : []),
);
return pick(user, userProperties.concat(options?.includeRole ? ['role'] : []));
}
export function clean(user: User, options?: { includeRole: boolean }): Partial<User>;

View file

@ -23,7 +23,6 @@ import {
} from './workflows.service';
import { WorkflowService } from '@/workflows/workflow.service';
import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
@ -31,7 +30,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository
export = {
createWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Create, res: express.Response): Promise<express.Response> => {
const workflow = req.body;
@ -42,9 +41,7 @@ export = {
addNodeIds(workflow);
const role = await Container.get(RoleService).findWorkflowOwnerRole();
const createdWorkflow = await createWorkflow(workflow, req.user, role);
const createdWorkflow = await createWorkflow(workflow, req.user, 'workflow:owner');
await Container.get(WorkflowHistoryService).saveVersion(
req.user,
@ -59,7 +56,7 @@ export = {
},
],
deleteWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
const { id: workflowId } = req.params;
@ -74,7 +71,7 @@ export = {
},
],
getWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
const { id } = req.params;
@ -95,7 +92,7 @@ export = {
},
],
getWorkflows: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
validCursor,
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query;
@ -104,7 +101,7 @@ export = {
...(active !== undefined && { active }),
};
if (['owner', 'admin'].includes(req.user.globalRole.name)) {
if (['global:owner', 'global:admin'].includes(req.user.role)) {
if (tags) {
const workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags(
parseTagNames(tags),
@ -159,7 +156,7 @@ export = {
},
],
updateWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Update, res: express.Response): Promise<express.Response> => {
const { id } = req.params;
const updateData = new WorkflowEntity();
@ -221,7 +218,7 @@ export = {
},
],
activateWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
const { id } = req.params;
@ -255,7 +252,7 @@ export = {
},
],
deactivateWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
const { id } = req.params;

View file

@ -1,10 +1,9 @@
import { Container } from 'typedi';
import * as Db from '@/Db';
import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { Role } from '@db/entities/Role';
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import config from '@/config';
import Container from 'typedi';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -13,7 +12,7 @@ function insertIf(condition: boolean, elements: string[]): string[] {
}
export async function getSharedWorkflowIds(user: User): Promise<string[]> {
const where = ['owner', 'admin'].includes(user.globalRole.name) ? {} : { userId: user.id };
const where = ['global:owner', 'global:admin'].includes(user.role) ? {} : { userId: user.id };
const sharedWorkflows = await Container.get(SharedWorkflowRepository).find({
where,
select: ['workflowId'],
@ -27,7 +26,7 @@ export async function getSharedWorkflow(
): Promise<SharedWorkflow | null> {
return await Container.get(SharedWorkflowRepository).findOne({
where: {
...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }),
...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }),
...(workflowId && { workflowId }),
},
relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'],
@ -43,7 +42,7 @@ export async function getWorkflowById(id: string): Promise<WorkflowEntity | null
export async function createWorkflow(
workflow: WorkflowEntity,
user: User,
role: Role,
role: WorkflowSharingRole,
): Promise<WorkflowEntity> {
return await Db.transaction(async (transactionManager) => {
const newWorkflow = new WorkflowEntity();

View file

@ -6,20 +6,18 @@ import { Container } from 'typedi';
import type { AuthenticatedRequest, PaginatedRequest } from '../../../types';
import { decodeCursor } from '../services/pagination.service';
import { License } from '@/License';
import type { RoleNames } from '@/databases/entities/Role';
import type { GlobalRole } from '@db/entities/User';
const UNLIMITED_USERS_QUOTA = -1;
export const authorize =
(authorizedRoles: readonly RoleNames[]) =>
(authorizedRoles: readonly GlobalRole[]) =>
(
req: AuthenticatedRequest,
res: express.Response,
next: express.NextFunction,
): express.Response | void => {
const { name } = req.user.globalRole;
if (!authorizedRoles.includes(name)) {
if (!authorizedRoles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden' });
}

View file

@ -89,7 +89,6 @@ import { OrchestrationController } from './controllers/orchestration.controller'
import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
import { InvitationController } from './controllers/invitation.controller';
import { CollaborationService } from './collaboration/collaboration.service';
import { RoleController } from './controllers/role.controller';
import { BadRequestError } from './errors/response-errors/bad-request.error';
import { OrchestrationService } from '@/services/orchestration.service';
@ -228,7 +227,6 @@ export class Server extends AbstractServer {
VariablesController,
InvitationController,
VariablesController,
RoleController,
ActiveWorkflowsController,
WorkflowsController,
ExecutionsController,

View file

@ -5,7 +5,6 @@ import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow';
import config from '@/config';
import { License } from '@/License';
import { OwnershipService } from '@/services/ownership.service';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -16,7 +15,6 @@ export class PermissionChecker {
private readonly userRepository: UserRepository,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly roleService: RoleService,
private readonly ownershipService: OwnershipService,
private readonly license: License,
) {}
@ -37,7 +35,6 @@ export class PermissionChecker {
const user = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['globalRole'],
});
if (user.hasGlobalScope('workflow:execute')) return;
@ -56,12 +53,8 @@ export class PermissionChecker {
workflowUserIds = workflowSharings.map((s) => s.userId);
}
const roleId = await this.roleService.findCredentialOwnerRoleId();
const credentialSharings = await this.sharedCredentialsRepository.findSharings(
workflowUserIds,
roleId,
);
const credentialSharings =
await this.sharedCredentialsRepository.findOwnedSharings(workflowUserIds);
const accessibleCredIds = credentialSharings.map((s) => s.credentialsId);

View file

@ -56,7 +56,6 @@ export const createPasswordSha = (user: User) =>
export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
const user = await Container.get(UserRepository).findOne({
where: { id: jwtPayload.id },
relations: ['globalRole'],
});
let passwordHash = null;

View file

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

View file

@ -4,7 +4,6 @@ import { InternalHooks } from '@/InternalHooks';
import { LdapService } from '@/Ldap/ldap.service';
import {
createLdapUserOnLocalDb,
getLdapUserRole,
getUserByEmail,
getAuthIdentityByLdapId,
isLdapEnabled,
@ -50,8 +49,7 @@ export const handleLdapLogin = async (
const identity = await createLdapAuthIdentity(emailUser, ldapId);
await updateLdapUserOnLocalDb(identity, ldapAttributesValues);
} else {
const role = await getLdapUserRole();
const user = await createLdapUserOnLocalDb(role, ldapAttributesValues, ldapId);
const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId);
void Container.get(InternalHooks).onUserSignup(user, {
user_type: 'ldap',
was_disabled_ldap_user: false,

View file

@ -3,7 +3,7 @@ import type { DataSourceOptions as ConnectionOptions } from 'typeorm';
import { DataSource as Connection } from 'typeorm';
import { Container } from 'typedi';
import { Logger } from '@/Logger';
import { getConnectionOptions } from '@/Db';
import { getConnectionOptions, setSchema } from '@/Db';
import type { Migration } from '@db/types';
import { wrapMigration } from '@db/utils/migrationHelpers';
import config from '@/config';
@ -40,6 +40,7 @@ export class DbRevertMigrationCommand extends Command {
this.connection = new Connection(connectionOptions);
await this.connection.initialize();
if (dbType === 'postgresdb') await setSchema(this.connection);
await this.connection.undoLastMigration();
await this.connection.destroy();
}

View file

@ -8,13 +8,11 @@ import type { EntityManager } from 'typeorm';
import * as Db from '@/Db';
import type { User } from '@db/entities/User';
import { SharedCredentials } from '@db/entities/SharedCredentials';
import type { Role } from '@db/entities/Role';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import { BaseCommand } from '../BaseCommand';
import type { ICredentialsEncrypted } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import { UM_FIX_INSTRUCTION } from '@/constants';
import { UserRepository } from '@db/repositories/user.repository';
@ -42,8 +40,6 @@ export class ImportCredentialsCommand extends BaseCommand {
}),
};
private ownerCredentialRole: Role;
private transactionManager: EntityManager;
async init() {
@ -71,7 +67,6 @@ export class ImportCredentialsCommand extends BaseCommand {
let totalImported = 0;
const cipher = Container.get(Cipher);
await this.initOwnerCredentialRole();
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
if (flags.separate) {
@ -145,16 +140,6 @@ export class ImportCredentialsCommand extends BaseCommand {
);
}
private async initOwnerCredentialRole() {
const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole();
if (!ownerCredentialRole) {
throw new ApplicationError(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`);
}
this.ownerCredentialRole = ownerCredentialRole;
}
private async storeCredential(credential: Partial<CredentialsEntity>, user: User) {
if (!credential.nodesAccess) {
credential.nodesAccess = [];
@ -165,19 +150,14 @@ export class ImportCredentialsCommand extends BaseCommand {
{
credentialsId: result.identifiers[0].id as string,
userId: user.id,
roleId: this.ownerCredentialRole.id,
role: 'credential:owner',
},
['credentialsId', 'userId'],
);
}
private async getOwner() {
const ownerGlobalRole = await Container.get(RoleService).findGlobalOwnerRole();
const owner =
ownerGlobalRole &&
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole.id }));
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}

View file

@ -11,7 +11,6 @@ import { generateNanoId } from '@db/utils/generators';
import { UserRepository } from '@db/repositories/user.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { IWorkflowToImport } from '@/Interfaces';
import { RoleService } from '@/services/role.service';
import { ImportService } from '@/services/import.service';
import { BaseCommand } from '../BaseCommand';
@ -138,12 +137,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
}
private async getOwner() {
const ownerGlobalRole = await Container.get(RoleService).findGlobalOwnerRole();
const owner =
ownerGlobalRole &&
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole?.id }));
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}

View file

@ -6,7 +6,6 @@ import { SettingsRepository } from '@db/repositories/settings.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { UserRepository } from '@db/repositories/user.repository';
import { RoleService } from '@/services/role.service';
import { BaseCommand } from '../BaseCommand';
const defaultUserProps = {
@ -14,6 +13,7 @@ const defaultUserProps = {
lastName: null,
email: null,
password: null,
role: 'global:owner',
};
export class Reset extends BaseCommand {
@ -24,14 +24,8 @@ export class Reset extends BaseCommand {
async run(): Promise<void> {
const owner = await this.getInstanceOwner();
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole();
await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner, workflowOwnerRole);
await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(
owner,
credentialOwnerRole,
);
await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner);
await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(owner);
await Container.get(UserRepository).deleteAllExcept(owner);
await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps));
@ -45,7 +39,7 @@ export class Reset extends BaseCommand {
Container.get(SharedCredentialsRepository).create({
credentials,
user: owner,
role: credentialOwnerRole,
role: 'credential:owner',
}),
);
await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
@ -59,19 +53,17 @@ export class Reset extends BaseCommand {
}
async getInstanceOwner(): Promise<User> {
const globalRole = await Container.get(RoleService).findGlobalOwnerRole();
const owner = await Container.get(UserRepository).findOneBy({ globalRoleId: globalRole.id });
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (owner) return owner;
const user = new User();
Object.assign(user, { ...defaultUserProps, globalRole });
Object.assign(user, defaultUserProps);
await Container.get(UserRepository).save(user);
return await Container.get(UserRepository).findOneByOrFail({ globalRoleId: globalRole.id });
return await Container.get(UserRepository).findOneByOrFail({ role: 'global:owner' });
}
async catch(error: Error): Promise<void> {

View file

@ -55,7 +55,7 @@ export class AuthController {
const preliminaryUser = await handleEmailLogin(email, password);
// if the user is an owner, continue with the login
if (
preliminaryUser?.globalRole?.name === 'owner' ||
preliminaryUser?.role === 'global:owner' ||
preliminaryUser?.settings?.allowSSOManualLogin
) {
user = preliminaryUser;
@ -65,7 +65,7 @@ export class AuthController {
}
} else if (isLdapCurrentAuthenticationMethod()) {
const preliminaryUser = await handleEmailLogin(email, password);
if (preliminaryUser?.globalRole?.name === 'owner') {
if (preliminaryUser?.role === 'global:owner') {
user = preliminaryUser;
usedAuthenticationMethod = 'email';
} else {
@ -138,7 +138,7 @@ export class AuthController {
}
try {
user = await this.userRepository.findOneOrFail({ where: {}, relations: ['globalRole'] });
user = await this.userRepository.findOneOrFail({ where: {} });
} catch (error) {
throw new InternalServerError(
'No users found in database - did you wipe the users table? Create at least one user.',

View file

@ -1,8 +1,6 @@
import { Request } from 'express';
import { v4 as uuid } from 'uuid';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import { RoleRepository } from '@db/repositories/role.repository';
import { SettingsRepository } from '@db/repositories/settings.repository';
import { UserRepository } from '@db/repositories/user.repository';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
@ -39,7 +37,6 @@ const tablesToTruncate = [
'installed_packages',
'installed_nodes',
'user',
'role',
'variables',
];
@ -87,7 +84,6 @@ export class E2EController {
constructor(
license: License,
private readonly roleRepo: RoleRepository,
private readonly settingsRepo: SettingsRepository,
private readonly userRepo: UserRepository,
private readonly workflowRunner: ActiveWorkflowRunner,
@ -148,7 +144,7 @@ export class E2EController {
private async truncateAll() {
for (const table of tablesToTruncate) {
try {
const { connection } = this.roleRepo.manager;
const { connection } = this.settingsRepo.manager;
await connection.query(
`DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`,
);
@ -163,27 +159,12 @@ export class E2EController {
members: UserSetupPayload[],
admin: UserSetupPayload,
) {
const roles: Array<[Role['name'], Role['scope']]> = [
['owner', 'global'],
['member', 'global'],
['admin', 'global'],
['owner', 'workflow'],
['owner', 'credential'],
['user', 'credential'],
['editor', 'workflow'],
];
const [{ id: globalOwnerRoleId }, { id: globalMemberRoleId }, { id: globalAdminRoleId }] =
await this.roleRepo.save(
roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })),
);
const instanceOwner = {
const instanceOwner = this.userRepo.create({
id: uuid(),
...owner,
password: await this.passwordUtility.hash(owner.password),
globalRoleId: globalOwnerRoleId,
};
role: 'global:owner',
});
if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) {
const { encryptedRecoveryCodes, encryptedSecret } =
@ -192,12 +173,12 @@ export class E2EController {
instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes;
}
const adminUser = {
const adminUser = this.userRepo.create({
id: uuid(),
...admin,
password: await this.passwordUtility.hash(admin.password),
globalRoleId: globalAdminRoleId,
};
role: 'global:admin',
});
const users = [];
@ -209,7 +190,7 @@ export class E2EController {
id: uuid(),
...payload,
password: await this.passwordUtility.hash(password),
globalRoleId: globalMemberRoleId,
role: 'global:member',
}),
);
}

View file

@ -1,4 +1,5 @@
import { Response } from 'express';
import validator from 'validator';
import config from '@/config';
import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators';
@ -12,12 +13,11 @@ import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers';
import { PasswordUtility } from '@/services/password.utility';
import { PostHogClient } from '@/posthog';
import type { User } from '@/databases/entities/User';
import validator from 'validator';
import { UserRepository } from '@db/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { InternalHooks } from '@/InternalHooks';
import { ExternalHooks } from '@/ExternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized()
@RestController('/invitations')
@ -91,13 +91,13 @@ export class InvitationController {
);
}
if (invite.role && !['member', 'admin'].includes(invite.role)) {
if (invite.role && !['global:member', 'global:admin'].includes(invite.role)) {
throw new BadRequestError(
`Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'member' or 'admin'.`,
`Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'global:member' or 'global:admin'.`,
);
}
if (invite.role === 'admin' && !this.license.isAdvancedPermissionsLicensed()) {
if (invite.role === 'global:admin' && !this.license.isAdvancedPermissionsLicensed()) {
throw new UnauthorizedError(
'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.',
);
@ -106,7 +106,7 @@ export class InvitationController {
const attributes = req.body.map(({ email, role }) => ({
email,
role: role ?? 'member',
role: role ?? 'global:member',
}));
const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes);

View file

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

View file

@ -15,7 +15,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalHooks } from '@/InternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized(['global', 'owner'])
@Authorized('global:owner')
@RestController('/owner')
export class OwnerController {
constructor(
@ -35,7 +35,7 @@ export class OwnerController {
@Post('/setup')
async setupOwner(req: OwnerRequest.Post, res: Response) {
const { email, firstName, lastName, password } = req.body;
const { id: userId, globalRole } = req.user;
const { id: userId } = req.user;
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug(
@ -65,17 +65,6 @@ export class OwnerController {
throw new BadRequestError('First and last names are mandatory');
}
// TODO: This check should be in a middleware outside this class
if (globalRole.scope === 'global' && globalRole.name !== 'owner') {
this.logger.debug(
'Request to claim instance ownership failed because user shell does not exist or has wrong role!',
{
userId,
},
);
throw new BadRequestError('Invalid request');
}
let owner = req.user;
Object.assign(owner, {

View file

@ -1,22 +0,0 @@
import { License } from '@/License';
import { Get, RestController } from '@/decorators';
import { RoleService } from '@/services/role.service';
@RestController('/roles')
export class RoleController {
constructor(
private readonly roleService: RoleService,
private readonly license: License,
) {}
@Get('/')
async listRoles() {
return this.roleService.listRoles().map((role) => {
if (role.scope === 'global' && role.name === 'admin') {
return { ...role, isAvailable: this.license.isAdvancedPermissionsLicensed() };
}
return { ...role, isAvailable: true };
});
}
}

View file

@ -23,7 +23,6 @@ import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { UserRepository } from '@db/repositories/user.repository';
import { plainToInstance } from 'class-transformer';
import { RoleService } from '@/services/role.service';
import { UserService } from '@/services/user.service';
import { listQueryMiddleware } from '@/middlewares';
import { Logger } from '@/Logger';
@ -45,7 +44,6 @@ export class UsersController {
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly userRepository: UserRepository,
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly roleService: RoleService,
private readonly userService: UserService,
) {}
@ -70,7 +68,7 @@ export class UsersController {
}
if (filter?.isOwner) {
for (const user of publicUsers) delete user.globalRole;
for (const user of publicUsers) delete user.role;
}
// remove computed fields (unselectable)
@ -92,12 +90,7 @@ export class UsersController {
async listUsers(req: ListQuery.Request) {
const { listQueryOptions } = req;
const globalOwner = await this.roleService.findGlobalOwnerRole();
const findManyOptions = await this.userRepository.toFindManyOptions(
listQueryOptions,
globalOwner.id,
);
const findManyOptions = await this.userRepository.toFindManyOptions(listQueryOptions);
const users = await this.userRepository.find(findManyOptions);
@ -118,7 +111,6 @@ export class UsersController {
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
const user = await this.userRepository.findOneOrFail({
where: { id: req.params.id },
relations: ['globalRole'],
});
if (!user) {
throw new NotFoundError('User not found');
@ -140,7 +132,6 @@ export class UsersController {
const user = await this.userRepository.findOneOrFail({
select: ['settings'],
where: { id },
relations: ['globalRole'],
});
return user.settings;
@ -194,11 +185,6 @@ export class UsersController {
telemetryData.migration_user_id = transferId;
}
const [workflowOwnerRole, credentialOwnerRole] = await Promise.all([
this.roleService.findWorkflowOwnerRole(),
this.roleService.findCredentialOwnerRole(),
]);
if (transferId) {
const transferee = users.find((user) => user.id === transferId);
@ -208,7 +194,7 @@ export class UsersController {
.getRepository(SharedWorkflow)
.find({
select: ['workflowId'],
where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id },
where: { userId: userToDelete.id, role: 'workflow:owner' },
})
.then((sharedWorkflows) => sharedWorkflows.map(({ workflowId }) => workflowId));
@ -223,7 +209,7 @@ export class UsersController {
// Transfer ownership of owned workflows
await transactionManager.update(
SharedWorkflow,
{ user: userToDelete, role: workflowOwnerRole },
{ user: userToDelete, role: 'workflow:owner' },
{ user: transferee },
);
@ -234,7 +220,7 @@ export class UsersController {
.getRepository(SharedCredentials)
.find({
select: ['credentialsId'],
where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id },
where: { userId: userToDelete.id, role: 'credential:owner' },
})
.then((sharedCredentials) => sharedCredentials.map(({ credentialsId }) => credentialsId));
@ -249,7 +235,7 @@ export class UsersController {
// Transfer ownership of owned credentials
await transactionManager.update(
SharedCredentials,
{ user: userToDelete, role: credentialOwnerRole },
{ user: userToDelete, role: 'credential:owner' },
{ user: transferee },
);
@ -271,11 +257,11 @@ export class UsersController {
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
this.sharedWorkflowRepository.find({
relations: ['workflow'],
where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id },
where: { userId: userToDelete.id, role: 'workflow:owner' },
}),
this.sharedCredentialsRepository.find({
relations: ['credentials'],
where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id },
where: { userId: userToDelete.id, role: 'credential:owner' },
}),
]);
@ -318,23 +304,20 @@ export class UsersController {
const targetUser = await this.userRepository.findOne({
where: { id: req.params.id },
relations: ['globalRole'],
});
if (targetUser === null) {
throw new NotFoundError(NO_USER);
}
if (req.user.globalRole.name === 'admin' && targetUser.globalRole.name === 'owner') {
if (req.user.role === 'global:admin' && targetUser.role === 'global:owner') {
throw new UnauthorizedError(NO_ADMIN_ON_OWNER);
}
if (req.user.globalRole.name === 'owner' && targetUser.globalRole.name === 'owner') {
if (req.user.role === 'global:owner' && targetUser.role === 'global:owner') {
throw new UnauthorizedError(NO_OWNER_ON_OWNER);
}
const roleToSet = await this.roleService.findCached('global', payload.newRoleName);
await this.userService.update(targetUser.id, { globalRoleId: roleToSet.id });
await this.userService.update(targetUser.id, { role: payload.newRoleName });
void this.internalHooks.onUserRoleChange({
user: req.user,

View file

@ -45,7 +45,7 @@ EECredentialsController.get(
let credential = await Container.get(CredentialsRepository).findOne({
where: { id: credentialId },
relations: ['shared', 'shared.role', 'shared.user'],
relations: ['shared', 'shared.user'],
});
if (!credential) {
@ -62,7 +62,7 @@ EECredentialsController.get(
credential = Container.get(OwnershipService).addOwnedByAndSharedWith(credential);
if (!includeDecryptedData || !userSharing || userSharing.role.name !== 'owner') {
if (!includeDecryptedData || !userSharing || userSharing.role !== 'credential:owner') {
const { data: _, ...rest } = credential;
return { ...rest };
}
@ -151,10 +151,9 @@ EECredentialsController.put(
const ownerIds = (
await EECredentials.getSharings(Db.getConnection().createEntityManager(), credentialId, [
'shared',
'shared.role',
])
)
.filter((e) => e.role.name === 'owner')
.filter((e) => e.role === 'credential:owner')
.map((e) => e.userId);
let amountRemoved: number | null = null;

View file

@ -147,7 +147,7 @@ credentialsController.patch(
allowGlobalScope: true,
globalScope: 'credential:update',
},
['credentials', 'role'],
['credentials'],
);
if (!sharing) {
@ -163,7 +163,7 @@ credentialsController.patch(
);
}
if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:update')) {
if (sharing.role !== 'credential:owner' && !req.user.hasGlobalScope('credential:update')) {
Container.get(Logger).info(
'Attempt to update credential blocked due to lack of permissions',
{
@ -216,7 +216,7 @@ credentialsController.delete(
allowGlobalScope: true,
globalScope: 'credential:delete',
},
['credentials', 'role'],
['credentials'],
);
if (!sharing) {
@ -232,7 +232,7 @@ credentialsController.delete(
);
}
if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:delete')) {
if (sharing.role !== 'credential:owner' && !req.user.hasGlobalScope('credential:delete')) {
Container.get(Logger).info(
'Attempt to delete credential blocked due to lack of permissions',
{

View file

@ -1,10 +1,9 @@
import { Container } from 'typedi';
import type { EntityManager, FindOptionsWhere } from 'typeorm';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { User } from '@db/entities/User';
import { CredentialsService, type CredentialsGetSharedOptions } from './credentials.service';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { UserRepository } from '@/databases/repositories/user.repository';
@ -15,10 +14,9 @@ export class EECredentialsService extends CredentialsService {
): Promise<{ ownsCredential: boolean; credential?: CredentialsEntity }> {
const sharing = await this.getSharing(user, credentialId, { allowGlobalScope: false }, [
'credentials',
'role',
]);
if (!sharing || sharing.role.name !== 'owner') return { ownsCredential: false };
if (!sharing || sharing.role !== 'credential:owner') return { ownsCredential: false };
const { credentials: credential } = sharing;
@ -67,7 +65,6 @@ export class EECredentialsService extends CredentialsService {
shareWithIds: string[],
): Promise<SharedCredentials[]> {
const users = await Container.get(UserRepository).getByIds(transaction, shareWithIds);
const role = await Container.get(RoleService).findCredentialUserRole();
const newSharedCredentials = users
.filter((user) => !user.isPending)
@ -75,7 +72,7 @@ export class EECredentialsService extends CredentialsService {
Container.get(SharedCredentialsRepository).create({
credentialsId: credential.id,
userId: user.id,
roleId: role?.id,
role: 'credential:user',
}),
);

View file

@ -23,7 +23,6 @@ import { ExternalHooks } from '@/ExternalHooks';
import type { User } from '@db/entities/User';
import type { CredentialRequest, ListQuery } from '@/requests';
import { CredentialTypes } from '@/CredentialTypes';
import { RoleService } from '@/services/role.service';
import { OwnershipService } from '@/services/ownership.service';
import { Logger } from '@/Logger';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
@ -85,13 +84,8 @@ export class CredentialsService {
// global credential permissions. This allows the user to
// access credentials they don't own.
if (!options.allowGlobalScope || !user.hasGlobalScope(options.globalScope)) {
Object.assign(where, {
userId: user.id,
role: { name: 'owner' },
});
if (!relations.includes('role')) {
relations.push('role');
}
where.userId = user.id;
where.role = 'credential:owner';
}
return await Container.get(SharedCredentialsRepository).findOne({ where, relations });
@ -194,8 +188,6 @@ export class CredentialsService {
await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
const role = await Container.get(RoleService).findCredentialOwnerRole();
const result = await Db.transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(newCredential);
@ -204,7 +196,7 @@ export class CredentialsService {
const newSharedCredential = new SharedCredentials();
Object.assign(newSharedCredential, {
role,
role: 'credential:owner',
user,
credentials: savedCredential,
});

View file

@ -1,5 +1,5 @@
import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typeorm';
import { Table, TableColumn } from 'typeorm';
import { Table, TableColumn, TableForeignKey } from 'typeorm';
import LazyPromise from 'p-lazy';
import { Column } from './Column';
import { ApplicationError } from 'n8n-workflow';
@ -118,6 +118,42 @@ export class DropColumns extends TableOperation {
}
}
abstract class ForeignKeyOperation extends TableOperation {
protected foreignKey: TableForeignKey;
constructor(
tableName: string,
columnName: string,
[referencedTableName, referencedColumnName]: [string, string],
prefix: string,
queryRunner: QueryRunner,
customConstraintName?: string,
) {
super(tableName, prefix, queryRunner);
this.foreignKey = new TableForeignKey({
name: customConstraintName,
columnNames: [columnName],
referencedTableName: `${prefix}${referencedTableName}`,
referencedColumnNames: [referencedColumnName],
});
}
}
export class AddForeignKey extends ForeignKeyOperation {
async execute(queryRunner: QueryRunner) {
const { tableName, prefix } = this;
return await queryRunner.createForeignKey(`${prefix}${tableName}`, this.foreignKey);
}
}
export class DropForeignKey extends ForeignKeyOperation {
async execute(queryRunner: QueryRunner) {
const { tableName, prefix } = this;
return await queryRunner.dropForeignKey(`${prefix}${tableName}`, this.foreignKey);
}
}
class ModifyNotNull extends TableOperation {
constructor(
tableName: string,

View file

@ -1,6 +1,15 @@
import type { QueryRunner } from 'typeorm';
import { Column } from './Column';
import { AddColumns, AddNotNull, CreateTable, DropColumns, DropNotNull, DropTable } from './Table';
import {
AddColumns,
AddForeignKey,
AddNotNull,
CreateTable,
DropColumns,
DropForeignKey,
DropNotNull,
DropTable,
} from './Table';
import { CreateIndex, DropIndex } from './Indices';
export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunner) => ({
@ -26,6 +35,36 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne
dropIndex: (tableName: string, columnNames: string[], customIndexName?: string) =>
new DropIndex(tableName, columnNames, tablePrefix, queryRunner, customIndexName),
addForeignKey: (
tableName: string,
columnName: string,
reference: [string, string],
customConstraintName?: string,
) =>
new AddForeignKey(
tableName,
columnName,
reference,
tablePrefix,
queryRunner,
customConstraintName,
),
dropForeignKey: (
tableName: string,
columnName: string,
reference: [string, string],
customConstraintName?: string,
) =>
new DropForeignKey(
tableName,
columnName,
reference,
tablePrefix,
queryRunner,
customConstraintName,
),
addNotNull: (tableName: string, columnName: string) =>
new AddNotNull(tableName, columnName, tablePrefix, queryRunner),
dropNotNull: (tableName: string, columnName: string) =>

View file

@ -1,39 +0,0 @@
import { Column, Entity, OneToMany, PrimaryColumn, Unique } from 'typeorm';
import { IsString, Length } from 'class-validator';
import type { User } from './User';
import type { SharedWorkflow } from './SharedWorkflow';
import type { SharedCredentials } from './SharedCredentials';
import { WithTimestamps } from './AbstractEntity';
import { idStringifier } from '../utils/transformers';
export type RoleNames = 'owner' | 'member' | 'user' | 'editor' | 'admin';
export type RoleScopes = 'global' | 'workflow' | 'credential';
@Entity()
@Unique(['scope', 'name'])
export class Role extends WithTimestamps {
@PrimaryColumn({ transformer: idStringifier })
id: string;
@Column({ length: 32 })
@IsString({ message: 'Role name must be of type string.' })
@Length(1, 32, { message: 'Role name must be 1 to 32 characters long.' })
name: RoleNames;
@Column()
scope: RoleScopes;
@OneToMany('User', 'globalRole')
globalForUsers: User[];
@OneToMany('SharedWorkflow', 'role')
sharedWorkflows: SharedWorkflow[];
@OneToMany('SharedCredentials', 'role')
sharedCredentials: SharedCredentials[];
get cacheKey() {
return `role:${this.scope}:${this.name}`;
}
}

View file

@ -1,16 +1,14 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { CredentialsEntity } from './CredentialsEntity';
import { User } from './User';
import { Role } from './Role';
import { WithTimestamps } from './AbstractEntity';
export type CredentialSharingRole = 'credential:owner' | 'credential:user';
@Entity()
export class SharedCredentials extends WithTimestamps {
@ManyToOne('Role', 'sharedCredentials', { nullable: false })
role: Role;
@Column()
roleId: string;
role: CredentialSharingRole;
@ManyToOne('User', 'sharedCredentials')
user: User;

View file

@ -1,16 +1,14 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { WorkflowEntity } from './WorkflowEntity';
import { User } from './User';
import { Role } from './Role';
import { WithTimestamps } from './AbstractEntity';
export type WorkflowSharingRole = 'workflow:owner' | 'workflow:editor' | 'workflow:user';
@Entity()
export class SharedWorkflow extends WithTimestamps {
@ManyToOne('Role', 'sharedWorkflows', { nullable: false })
role: Role;
@Column()
roleId: string;
role: WorkflowSharingRole;
@ManyToOne('User', 'sharedWorkflows')
user: User;

View file

@ -6,13 +6,11 @@ import {
Entity,
Index,
OneToMany,
ManyToOne,
PrimaryGeneratedColumn,
BeforeInsert,
} from 'typeorm';
import { IsEmail, IsString, Length } from 'class-validator';
import type { IUser, IUserSettings } from 'n8n-workflow';
import { Role } from './Role';
import type { SharedWorkflow } from './SharedWorkflow';
import type { SharedCredentials } from './SharedCredentials';
import { NoXss } from '../utils/customValidators';
@ -23,10 +21,13 @@ import type { AuthIdentity } from './AuthIdentity';
import { ownerPermissions, memberPermissions, adminPermissions } from '@/permissions/roles';
import { hasScope, type ScopeOptions, type Scope } from '@n8n/permissions';
const STATIC_SCOPE_MAP: Record<string, Scope[]> = {
owner: ownerPermissions,
member: memberPermissions,
admin: adminPermissions,
export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member';
export type AssignableRole = Exclude<GlobalRole, 'global:owner'>;
const STATIC_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
'global:owner': ownerPermissions,
'global:member': memberPermissions,
'global:admin': adminPermissions,
};
@Entity()
@ -72,11 +73,8 @@ export class User extends WithTimestamps implements IUser {
})
settings: IUserSettings | null;
@ManyToOne('Role', 'globalForUsers', { nullable: false })
globalRole: Role;
@Column()
globalRoleId: string;
role: GlobalRole;
@OneToMany('AuthIdentity', 'user')
authIdentities: AuthIdentity[];
@ -127,11 +125,11 @@ export class User extends WithTimestamps implements IUser {
@AfterLoad()
computeIsOwner(): void {
this.isOwner = this.globalRole?.name === 'owner';
this.isOwner = this.role === 'global:owner';
}
get globalScopes() {
return STATIC_SCOPE_MAP[this.globalRole?.name] ?? [];
return STATIC_SCOPE_MAP[this.role] ?? [];
}
hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): boolean {

View file

@ -6,7 +6,6 @@ import { EventDestinations } from './EventDestinations';
import { ExecutionEntity } from './ExecutionEntity';
import { InstalledNodes } from './InstalledNodes';
import { InstalledPackages } from './InstalledPackages';
import { Role } from './Role';
import { Settings } from './Settings';
import { SharedCredentials } from './SharedCredentials';
import { SharedWorkflow } from './SharedWorkflow';
@ -29,7 +28,6 @@ export const entities = {
ExecutionEntity,
InstalledNodes,
InstalledPackages,
Role,
Settings,
SharedCredentials,
SharedWorkflow,

View file

@ -0,0 +1,127 @@
import type { MigrationContext, ReversibleMigration } from '@db/types';
type Table = 'user' | 'shared_workflow' | 'shared_credentials';
const idColumns: Record<Table, string> = {
user: 'id',
shared_credentials: 'credentialsId',
shared_workflow: 'workflowId',
};
const roleScopes: Record<Table, string> = {
user: 'global',
shared_credentials: 'credential',
shared_workflow: 'workflow',
};
const foreignKeySuffixes: Record<Table, string> = {
user: 'f0609be844f9200ff4365b1bb3d',
shared_credentials: 'c68e056637562000b68f480815a',
shared_workflow: '3540da03964527aa24ae014b780',
};
export class DropRoleMapping1705429061930 implements ReversibleMigration {
async up(context: MigrationContext) {
await this.migrateUp('user', context);
await this.migrateUp('shared_workflow', context);
await this.migrateUp('shared_credentials', context);
}
async down(context: MigrationContext) {
await this.migrateDown('shared_workflow', context);
await this.migrateDown('shared_credentials', context);
await this.migrateDown('user', context);
}
private async migrateUp(
table: Table,
{
dbType,
escape,
runQuery,
schemaBuilder: { addNotNull, addColumns, dropColumns, dropForeignKey, column },
tablePrefix,
}: MigrationContext,
) {
await addColumns(table, [column('role').text]);
const roleTable = escape.tableName('role');
const tableName = escape.tableName(table);
const idColumn = escape.columnName(idColumns[table]);
const roleColumnName = table === 'user' ? 'globalRoleId' : 'roleId';
const roleColumn = escape.columnName(roleColumnName);
const scope = roleScopes[table];
const isMySQL = ['mariadb', 'mysqldb'].includes(dbType);
const roleField = isMySQL ? `CONCAT('${scope}:', R.name)` : `'${scope}:' || R.name`;
const subQuery = `
SELECT ${roleField} as role, T.${idColumn} as id
FROM ${tableName} T
LEFT JOIN ${roleTable} R
ON T.${roleColumn} = R.id and R.scope = '${scope}'`;
const swQuery = isMySQL
? `UPDATE ${tableName}, (${subQuery}) as mapping
SET ${tableName}.role = mapping.role
WHERE ${tableName}.${idColumn} = mapping.id`
: `UPDATE ${tableName}
SET role = mapping.role
FROM (${subQuery}) as mapping
WHERE ${tableName}.${idColumn} = mapping.id`;
await runQuery(swQuery);
await addNotNull(table, 'role');
await dropForeignKey(
table,
roleColumnName,
['role', 'id'],
`FK_${tablePrefix}${foreignKeySuffixes[table]}`,
);
await dropColumns(table, [roleColumnName]);
}
private async migrateDown(
table: Table,
{
dbType,
escape,
runQuery,
schemaBuilder: { addNotNull, addColumns, dropColumns, addForeignKey, column },
tablePrefix,
}: MigrationContext,
) {
const roleColumnName = table === 'user' ? 'globalRoleId' : 'roleId';
await addColumns(table, [column(roleColumnName).int]);
const roleTable = escape.tableName('role');
const tableName = escape.tableName(table);
const idColumn = escape.columnName(idColumns[table]);
const roleColumn = escape.columnName(roleColumnName);
const scope = roleScopes[table];
const isMySQL = ['mariadb', 'mysqldb'].includes(dbType);
const roleField = isMySQL ? `CONCAT('${scope}:', R.name)` : `'${scope}:' || R.name`;
const subQuery = `
SELECT R.id as role_id, T.${idColumn} as id
FROM ${tableName} T
LEFT JOIN ${roleTable} R
ON T.role = ${roleField} and R.scope = '${scope}'`;
const query = isMySQL
? `UPDATE ${tableName}, (${subQuery}) as mapping
SET ${tableName}.${roleColumn} = mapping.role_id
WHERE ${tableName}.${idColumn} = mapping.id`
: `UPDATE ${tableName}
SET ${roleColumn} = mapping.role_id
FROM (${subQuery}) as mapping
WHERE ${tableName}.${idColumn} = mapping.id`;
await runQuery(query);
await addNotNull(table, roleColumnName);
await addForeignKey(
table,
roleColumnName,
['role', 'id'],
`FK_${tablePrefix}${foreignKeySuffixes[table]}`,
);
await dropColumns(table, ['role']);
}
}

View file

@ -51,6 +51,7 @@ import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-Execut
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238,
@ -105,4 +106,5 @@ export const mysqlMigrations: Migration[] = [
AddWorkflowMetadata1695128658538,
ModifyWorkflowHistoryNodesAndConnections1695829275184,
AddGlobalAdminRole1700571993961,
DropRoleMapping1705429061930,
];

View file

@ -50,6 +50,7 @@ import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWor
import { MigrateToTimestampTz1694091729095 } from './1694091729095-MigrateToTimestampTz';
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
export const postgresMigrations: Migration[] = [
InitialMigration1587669153312,
@ -103,4 +104,5 @@ export const postgresMigrations: Migration[] = [
MigrateToTimestampTz1694091729095,
ModifyWorkflowHistoryNodesAndConnections1695829275184,
AddGlobalAdminRole1700571993961,
DropRoleMapping1705429061930,
];

View file

@ -0,0 +1,5 @@
import { DropRoleMapping1705429061930 as BaseMigration } from '../common/1705429061930-DropRoleMapping';
export class DropRoleMapping1705429061930 extends BaseMigration {
transaction = false as const;
}

View file

@ -48,6 +48,7 @@ import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftD
import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
import { DropRoleMapping1705429061930 } from './1705429061930-DropRoleMapping';
const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
@ -99,6 +100,7 @@ const sqliteMigrations: Migration[] = [
AddWorkflowMetadata1695128658538,
ModifyWorkflowHistoryNodesAndConnections1695829275184,
AddGlobalAdminRole1700571993961,
DropRoleMapping1705429061930,
];
export { sqliteMigrations };

View file

@ -45,7 +45,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
type Select = Array<keyof CredentialsEntity>;
const defaultRelations = ['shared', 'shared.role', 'shared.user'];
const defaultRelations = ['shared', 'shared.user'];
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };
@ -81,7 +81,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
const findManyOptions: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };
if (withSharings) {
findManyOptions.relations = ['shared', 'shared.user', 'shared.role'];
findManyOptions.relations = ['shared', 'shared.user'];
}
return await this.find(findManyOptions);

View file

@ -1,42 +0,0 @@
import { Service } from 'typedi';
import { DataSource, In, Repository } from 'typeorm';
import type { RoleNames, RoleScopes } from '../entities/Role';
import { Role } from '../entities/Role';
import { User } from '../entities/User';
@Service()
export class RoleRepository extends Repository<Role> {
constructor(dataSource: DataSource) {
super(Role, dataSource.manager);
}
async findRole(scope: RoleScopes, name: RoleNames) {
return await this.findOne({ where: { scope, name } });
}
/**
* Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }`
*/
async countUsersByRole() {
type Row = { role_name: string; count: number | string };
const rows: Row[] = await this.createQueryBuilder('role')
.select('role.name')
.addSelect('COUNT(user.id)', 'count')
.innerJoin(User, 'user', 'role.id = user.globalRoleId')
.groupBy('role.name')
.getRawMany();
return rows.reduce<Record<string, number>>((acc, item) => {
acc[item.role_name] = typeof item.count === 'number' ? item.count : parseInt(item.count, 10);
return acc;
}, {});
}
async getIdsInScopeWorkflowByNames(roleNames: RoleNames[]) {
return await this.find({
select: ['id'],
where: { name: In(roleNames), scope: 'workflow' },
}).then((role) => role.map(({ id }) => id));
}
}

View file

@ -1,9 +1,8 @@
import { Service } from 'typedi';
import type { EntityManager, FindOptionsWhere } from 'typeorm';
import type { EntityManager } from 'typeorm';
import { DataSource, In, Not, Repository } from 'typeorm';
import { SharedCredentials } from '../entities/SharedCredentials';
import type { User } from '../entities/User';
import type { Role } from '../entities/Role';
@Service()
export class SharedCredentialsRepository extends Repository<SharedCredentials> {
@ -26,15 +25,15 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
async findByCredentialIds(credentialIds: string[]) {
return await this.find({
relations: ['credentials', 'role', 'user'],
relations: ['credentials', 'user'],
where: {
credentialsId: In(credentialIds),
},
});
}
async makeOwnerOfAllCredentials(user: User, role: Role) {
return await this.update({ userId: Not(user.id), roleId: role.id }, { user });
async makeOwnerOfAllCredentials(user: User) {
return await this.update({ userId: Not(user.id), role: 'credential:owner' }, { user });
}
/**
@ -42,23 +41,22 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
*/
async getAccessibleCredentials(userId: string) {
const sharings = await this.find({
relations: ['role'],
where: {
userId,
role: { name: In(['owner', 'user']), scope: 'credential' },
role: In(['credential:owner', 'credential:user']),
},
});
return sharings.map((s) => s.credentialsId);
}
async findSharings(userIds: string[], roleId?: string) {
const where: FindOptionsWhere<SharedCredentials> = { userId: In(userIds) };
// If credential sharing is not enabled, get only credentials owned by this user
if (roleId) where.roleId = roleId;
return await this.find({ where });
async findOwnedSharings(userIds: string[]) {
return await this.find({
where: {
userId: In(userIds),
role: 'credential:owner',
},
});
}
async deleteByIds(transaction: EntityManager, sharedCredentialsIds: string[], user?: User) {

View file

@ -1,10 +1,9 @@
import { Service } from 'typedi';
import { DataSource, Repository, In, Not } from 'typeorm';
import type { EntityManager, FindOptionsSelect, FindOptionsWhere } from 'typeorm';
import { SharedWorkflow } from '../entities/SharedWorkflow';
import type { EntityManager, FindManyOptions, FindOptionsWhere } from 'typeorm';
import { SharedWorkflow, type WorkflowSharingRole } from '../entities/SharedWorkflow';
import { type User } from '../entities/User';
import type { Scope } from '@n8n/permissions';
import type { Role } from '../entities/Role';
import type { WorkflowEntity } from '../entities/WorkflowEntity';
@Service()
@ -35,22 +34,29 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
async findByWorkflowIds(workflowIds: string[]) {
return await this.find({
relations: ['role', 'user'],
relations: ['user'],
where: {
role: {
name: 'owner',
scope: 'workflow',
},
role: 'workflow:owner',
workflowId: In(workflowIds),
},
});
}
async findSharingRole(
userId: string,
workflowId: string,
): Promise<WorkflowSharingRole | undefined> {
return await this.findOne({
select: ['role'],
where: { workflowId, userId },
}).then((shared) => shared?.role);
}
async findSharing(
workflowId: string,
user: User,
scope: Scope,
{ roles, extraRelations }: { roles?: string[]; extraRelations?: string[] } = {},
{ roles, extraRelations }: { roles?: WorkflowSharingRole[]; extraRelations?: string[] } = {},
) {
const where: FindOptionsWhere<SharedWorkflow> = {
workflow: { id: workflowId },
@ -61,18 +67,18 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
}
if (roles) {
where.role = { name: In(roles) };
where.role = In(roles);
}
const relations = ['workflow', 'role'];
const relations = ['workflow'];
if (extraRelations) relations.push(...extraRelations);
return await this.findOne({ relations, where });
}
async makeOwnerOfAllWorkflows(user: User, role: Role) {
return await this.update({ userId: Not(user.id), roleId: role.id }, { user });
async makeOwnerOfAllWorkflows(user: User) {
return await this.update({ userId: Not(user.id), role: 'workflow:owner' }, { user });
}
async getSharing(
@ -102,14 +108,14 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
): Promise<SharedWorkflow[]> {
return await this.find({
where: {
...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }),
...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }),
...(options.workflowIds && { workflowId: In(options.workflowIds) }),
},
...(options.relations && { relations: options.relations }),
});
}
async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[], roleId: string) {
async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[]) {
const newSharedWorkflows = users.reduce<SharedWorkflow[]>((acc, user) => {
if (user.isPending) {
return acc;
@ -117,7 +123,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
const entity: Partial<SharedWorkflow> = {
workflowId: workflow.id,
userId: user.id,
roleId,
role: 'workflow:editor',
};
acc.push(this.create(entity));
return acc;
@ -126,12 +132,15 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
return await transaction.save(newSharedWorkflows);
}
async findWithFields(workflowIds: string[], { fields }: { fields: string[] }) {
async findWithFields(
workflowIds: string[],
{ select }: Pick<FindManyOptions<SharedWorkflow>, 'select'>,
) {
return await this.find({
where: {
workflowId: In(workflowIds),
},
select: fields as FindOptionsSelect<SharedWorkflow>,
select,
});
}

View file

@ -1,9 +1,9 @@
import { Service } from 'typedi';
import type { EntityManager, FindManyOptions } from 'typeorm';
import { DataSource, In, IsNull, Not, Repository } from 'typeorm';
import { User } from '../entities/User';
import type { ListQuery } from '@/requests';
import { type GlobalRole, User } from '../entities/User';
@Service()
export class UserRepository extends Repository<User> {
constructor(dataSource: DataSource) {
@ -13,7 +13,6 @@ export class UserRepository extends Repository<User> {
async findManyByIds(userIds: string[]) {
return await this.find({
where: { id: In(userIds) },
relations: ['globalRole'],
});
}
@ -28,7 +27,6 @@ export class UserRepository extends Repository<User> {
async findManyByEmail(emails: string[]) {
return await this.find({
where: { email: In(emails) },
relations: ['globalRole'],
select: ['email', 'password', 'id'],
});
}
@ -43,15 +41,30 @@ export class UserRepository extends Repository<User> {
email,
password: Not(IsNull()),
},
relations: ['authIdentities', 'globalRole'],
relations: ['authIdentities'],
});
}
async toFindManyOptions(listQueryOptions?: ListQuery.Options, globalOwnerRoleId?: string) {
/** Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }` */
async countUsersByRole() {
const rows = (await this.createQueryBuilder()
.select(['role', 'COUNT(role) as count'])
.groupBy('role')
.execute()) as Array<{ role: GlobalRole; count: string }>;
return rows.reduce(
(acc, row) => {
acc[row.role] = parseInt(row.count, 10);
return acc;
},
{} as Record<GlobalRole, number>,
);
}
async toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<User> = {};
if (!listQueryOptions) {
findManyOptions.relations = ['globalRole', 'authIdentities'];
findManyOptions.relations = ['authIdentities'];
return findManyOptions;
}
@ -62,7 +75,7 @@ export class UserRepository extends Repository<User> {
if (skip) findManyOptions.skip = skip;
if (take && !select) {
findManyOptions.relations = ['globalRole', 'authIdentities'];
findManyOptions.relations = ['authIdentities'];
}
if (take && select && !select?.id) {
@ -74,11 +87,8 @@ export class UserRepository extends Repository<User> {
findManyOptions.where = otherFilters;
if (isOwner !== undefined && globalOwnerRoleId) {
findManyOptions.relations = ['globalRole'];
findManyOptions.where.globalRole = {
id: isOwner ? globalOwnerRoleId : Not(globalOwnerRoleId),
};
if (isOwner !== undefined) {
findManyOptions.where.role = isOwner ? 'global:owner' : Not('global:owner');
}
}

View file

@ -35,7 +35,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
async getAllActive() {
return await this.find({
where: { active: true },
relations: ['shared', 'shared.user', 'shared.user.globalRole', 'shared.role'],
relations: ['shared', 'shared.user'],
});
}
@ -50,7 +50,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
async findById(workflowId: string) {
return await this.findOne({
where: { id: workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole', 'shared.role'],
relations: ['shared', 'shared.user'],
});
}
@ -135,7 +135,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
createdAt: true,
updatedAt: true,
versionId: true,
shared: { userId: true, roleId: true },
shared: { userId: true, role: true },
};
delete select?.ownedBy; // remove non-entity field, handled after query
@ -152,7 +152,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
select.tags = { id: true, name: true };
}
if (isOwnedByIncluded) relations.push('shared', 'shared.role', 'shared.user');
if (isOwnedByIncluded) relations.push('shared', 'shared.user');
if (typeof where.name === 'string' && where.name !== '') {
where.name = Like(`%${where.name}%`);

View file

@ -5,7 +5,6 @@ import { StatisticsNames, WorkflowStatistics } from '../entities/WorkflowStatist
import type { User } from '@/databases/entities/User';
import { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
import { Role } from '@/databases/entities/Role';
type StatisticsInsertResult = 'insert' | 'failed' | 'alreadyExists';
type StatisticsUpsertResult = StatisticsInsertResult | 'update';
@ -110,12 +109,11 @@ export class WorkflowStatisticsRepository extends Repository<WorkflowStatistics>
'shared_workflow',
'shared_workflow.workflowId = workflow_statistics.workflowId',
)
.innerJoin(Role, 'role', 'role.id = shared_workflow.roleId')
.where('shared_workflow.userId = :userId', { userId })
.andWhere('workflow.active = :isActive', { isActive: true })
.andWhere('workflow_statistics.name = :name', { name: StatisticsNames.productionSuccess })
.andWhere('workflow_statistics.count >= 5')
.andWhere('role.name = :roleName', { roleName: 'owner' })
.andWhere('role = :roleName', { roleName: 'workflow:owner' })
.getCount();
}
}

View file

@ -36,9 +36,7 @@ export const createAuthMiddleware =
if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' });
const { globalRole } = user;
if (authRole === 'any' || (globalRole.scope === authRole[0] && globalRole.name === authRole[1]))
return next();
if (authRole === 'any' || authRole === user.role) return next();
res.status(403).json({ status: 'error', message: 'Unauthorized' });
};

View file

@ -1,11 +1,11 @@
import type { Request, Response, RequestHandler } from 'express';
import type { RoleNames, RoleScopes } from '@db/entities/Role';
import type { GlobalRole } from '@db/entities/User';
import type { BooleanLicenseFeature } from '@/Interfaces';
import type { Scope } from '@n8n/permissions';
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none';
export type AuthRole = GlobalRole | 'any' | 'none';
export type AuthRoleMetadata = Record<string, AuthRole>;
export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>;

View file

@ -23,12 +23,10 @@ import { isUniqueConstraintError } from '@/ResponseHelper';
import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId';
import { getCredentialExportPath, getWorkflowExportPath } from './sourceControlHelper.ee';
import type { SourceControlledFile } from './types/sourceControlledFile';
import { RoleService } from '@/services/role.service';
import { VariablesService } from '../variables/variables.service.ee';
import { TagRepository } from '@db/repositories/tag.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { UserRepository } from '@db/repositories/user.repository';
import { UM_FIX_INSTRUCTION } from '@/constants';
import { Logger } from '@/Logger';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
@ -59,36 +57,6 @@ export class SourceControlImportService {
);
}
private async getOwnerGlobalRole() {
const globalOwnerRole = await Container.get(RoleService).findGlobalOwnerRole();
if (!globalOwnerRole) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return globalOwnerRole;
}
private async getCredentialOwnerRole() {
const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole();
if (!credentialOwnerRole) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return credentialOwnerRole;
}
private async getWorkflowOwnerRole() {
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
if (!workflowOwnerRole) {
throw new ApplicationError(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
}
return workflowOwnerRole;
}
public async getRemoteVersionIdsFromFiles(): Promise<SourceControlWorkflowVersionId[]> {
const remoteWorkflowFiles = await glob('*.json', {
cwd: this.workflowExportFolder,
@ -222,7 +190,6 @@ export class SourceControlImportService {
}
public async importWorkflowFromWorkFolder(candidates: SourceControlledFile[], userId: string) {
const ownerWorkflowRole = await this.getWorkflowOwnerRole();
const workflowRunner = this.activeWorkflowRunner;
const candidateIds = candidates.map((c) => c.id);
const existingWorkflows = await Container.get(WorkflowRepository).findByIds(candidateIds, {
@ -230,7 +197,7 @@ export class SourceControlImportService {
});
const allSharedWorkflows = await Container.get(SharedWorkflowRepository).findWithFields(
candidateIds,
{ fields: ['workflowId', 'roleId', 'userId'] },
{ select: ['workflowId', 'role', 'userId'] },
);
const cachedOwnerIds = new Map<string, string>();
const importWorkflowsResult = await Promise.all(
@ -273,35 +240,29 @@ export class SourceControlImportService {
}
const existingSharedWorkflowOwnerByRoleId = allSharedWorkflows.find(
(e) =>
e.workflowId === importedWorkflow.id &&
e.roleId.toString() === ownerWorkflowRole.id.toString(),
(e) => e.workflowId === importedWorkflow.id && e.role === 'workflow:owner',
);
const existingSharedWorkflowOwnerByUserId = allSharedWorkflows.find(
(e) =>
e.workflowId === importedWorkflow.id &&
e.roleId.toString() === workflowOwnerId.toString(),
(e) => e.workflowId === importedWorkflow.id && e.role === 'workflow:owner',
);
if (!existingSharedWorkflowOwnerByUserId && !existingSharedWorkflowOwnerByRoleId) {
// no owner exists yet, so create one
await Container.get(SharedWorkflowRepository).insert({
workflowId: importedWorkflow.id,
userId: workflowOwnerId,
roleId: ownerWorkflowRole.id,
role: 'workflow:owner',
});
} else if (existingSharedWorkflowOwnerByRoleId) {
// skip, because the workflow already has a global owner
} else if (existingSharedWorkflowOwnerByUserId && !existingSharedWorkflowOwnerByRoleId) {
// if the worklflow has a non-global owner that is referenced by the owner file,
// if the workflow has a non-global owner that is referenced by the owner file,
// and no existing global owner, update the owner to the user referenced in the owner file
await Container.get(SharedWorkflowRepository).update(
{
workflowId: importedWorkflow.id,
userId: workflowOwnerId,
},
{
roleId: ownerWorkflowRole.id,
},
{ role: 'workflow:owner' },
);
}
if (existingWorkflow?.active) {
@ -343,13 +304,11 @@ export class SourceControlImportService {
},
select: ['id', 'name', 'type', 'data'],
});
const ownerCredentialRole = await this.getCredentialOwnerRole();
const ownerGlobalRole = await this.getOwnerGlobalRole();
const existingSharedCredentials = await Container.get(SharedCredentialsRepository).find({
select: ['userId', 'credentialsId', 'roleId'],
select: ['userId', 'credentialsId', 'role'],
where: {
credentialsId: In(candidateIds),
roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]),
role: 'credential:owner',
},
});
let importCredentialsResult: Array<{ id: string; name: string; type: string }> = [];
@ -382,7 +341,7 @@ export class SourceControlImportService {
const newSharedCredential = new SharedCredentials();
newSharedCredential.credentialsId = newCredentialObject.id as string;
newSharedCredential.userId = userId;
newSharedCredential.roleId = ownerCredentialRole.id;
newSharedCredential.role = 'credential:owner';
await Container.get(SharedCredentialsRepository).upsert({ ...newSharedCredential }, [
'credentialsId',

View file

@ -22,7 +22,7 @@ export class EnterpriseExecutionsService {
if (!execution) return;
const relations = ['shared', 'shared.user', 'shared.role'];
const relations = ['shared', 'shared.user'];
const workflow = (await this.workflowRepository.get(
{ id: execution.workflowId },

View file

@ -27,7 +27,7 @@ export class ExecutionsController {
private async getAccessibleWorkflowIds(user: User) {
return this.license.isSharingEnabled()
? await this.workflowSharingService.getSharedWorkflowIds(user)
: await this.workflowSharingService.getSharedWorkflowIds(user, ['owner']);
: await this.workflowSharingService.getSharedWorkflowIds(user, ['workflow:owner']);
}
@Get('/')

View file

@ -14,8 +14,7 @@ import type {
import { IsBoolean, IsEmail, IsIn, IsOptional, IsString, Length } from 'class-validator';
import { NoXss } from '@db/utils/customValidators';
import type { PublicUser, SecretsProvider, SecretsProviderState } from '@/Interfaces';
import type { Role, RoleNames } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { AssignableRole, type User } from '@db/entities/User';
import type { UserManagementMailer } from '@/UserManagement/email';
import type { Variables } from '@db/entities/Variables';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
@ -48,8 +47,8 @@ export class UserSettingsUpdatePayload {
}
export class UserRoleChangePayload {
@IsIn(['member', 'admin'])
newRoleName: Exclude<RoleNames, 'user' | 'editor' | 'owner'>;
@IsIn(['global:admin', 'global:member'])
newRoleName: AssignableRole;
}
export type AuthlessRequest<
@ -67,7 +66,6 @@ export type AuthenticatedRequest<
> = Omit<express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery>, 'user'> & {
user: User;
mailer?: UserManagementMailer;
globalMemberRole?: Role;
};
// ----------------------------------
@ -225,7 +223,7 @@ export declare namespace UserRequest {
export type Invite = AuthenticatedRequest<
{},
{},
Array<{ email: string; role?: 'member' | 'admin' }>
Array<{ email: string; role?: AssignableRole }>
>;
export type InviteResponse = {

View file

@ -4,16 +4,13 @@ import { type INode, type INodeCredentialsDetails } from 'n8n-workflow';
import { Logger } from '@/Logger';
import * as Db from '@/Db';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
import { RoleService } from '@/services/role.service';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { TagRepository } from '@db/repositories/tag.repository';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { replaceInvalidCredentials } from '@/WorkflowHelpers';
import { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
import { WorkflowTagMapping } from '@/databases/entities/WorkflowTagMapping';
import type { TagEntity } from '@/databases/entities/TagEntity';
import type { Role } from '@/databases/entities/Role';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { WorkflowTagMapping } from '@db/entities/WorkflowTagMapping';
import type { TagEntity } from '@db/entities/TagEntity';
import type { ICredentialsDb } from '@/Interfaces';
@Service()
@ -22,19 +19,15 @@ export class ImportService {
private dbTags: TagEntity[] = [];
private workflowOwnerRole: Role;
constructor(
private readonly logger: Logger,
private readonly credentialsRepository: CredentialsRepository,
private readonly tagRepository: TagRepository,
private readonly roleService: RoleService,
) {}
async initRecords() {
this.dbCredentials = await this.credentialsRepository.find();
this.dbTags = await this.tagRepository.find();
this.workflowOwnerRole = await this.roleService.findWorkflowOwnerRole();
}
async importWorkflows(workflows: WorkflowEntity[], userId: string) {
@ -64,7 +57,7 @@ export class ImportService {
const workflowId = upsertResult.identifiers.at(0)?.id as string;
await tx.upsert(SharedWorkflow, { workflowId, userId, roleId: this.workflowOwnerRole.id }, [
await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [
'workflowId',
'userId',
]);

View file

@ -2,17 +2,14 @@ import { Service } from 'typedi';
import { CacheService } from '@/services/cache/cache.service';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import type { User } from '@db/entities/User';
import { RoleService } from './role.service';
import { UserRepository } from '@/databases/repositories/user.repository';
import { UserRepository } from '@db/repositories/user.repository';
import type { ListQuery } from '@/requests';
import { ApplicationError } from 'n8n-workflow';
@Service()
export class OwnershipService {
constructor(
private cacheService: CacheService,
private userRepository: UserRepository,
private roleService: RoleService,
private sharedWorkflowRepository: SharedWorkflowRepository,
) {}
@ -27,13 +24,9 @@ export class OwnershipService {
if (cachedValue) return this.userRepository.create(cachedValue);
const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole();
if (!workflowOwnerRole) throw new ApplicationError('Failed to find workflow owner role');
const sharedWorkflow = await this.sharedWorkflowRepository.findOneOrFail({
where: { workflowId, roleId: workflowOwnerRole.id },
relations: ['user', 'user.globalRole'],
where: { workflowId, role: 'workflow:owner' },
relations: ['user'],
});
void this.cacheService.setHash('workflow-ownership', { [workflowId]: sharedWorkflow.user });
@ -61,7 +54,7 @@ export class OwnershipService {
shared?.forEach(({ user, role }) => {
const { id, email, firstName, lastName } = user;
if (role.name === 'owner') {
if (role === 'credential:owner' || role === 'workflow:owner') {
entity.ownedBy = { id, email, firstName, lastName };
} else {
entity.sharedWith.push({ id, email, firstName, lastName });
@ -72,11 +65,8 @@ export class OwnershipService {
}
async getInstanceOwner() {
const globalOwnerRole = await this.roleService.findGlobalOwnerRole();
return await this.userRepository.findOneOrFail({
where: { globalRoleId: globalOwnerRole.id },
relations: ['globalRole'],
where: { role: 'global:owner' },
});
}
}

View file

@ -1,109 +0,0 @@
import { Service } from 'typedi';
import { RoleRepository } from '@db/repositories/role.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { CacheService } from '@/services/cache/cache.service';
import type { RoleNames, RoleScopes } from '@db/entities/Role';
import { InvalidRoleError } from '@/errors/invalid-role.error';
import { License } from '@/License';
@Service()
export class RoleService {
constructor(
private roleRepository: RoleRepository,
private sharedWorkflowRepository: SharedWorkflowRepository,
private cacheService: CacheService,
private readonly license: License,
) {
void this.populateCache();
}
async populateCache() {
const allRoles = await this.roleRepository.find({});
if (!allRoles) return;
void this.cacheService.setMany(allRoles.map((r) => [r.cacheKey, r]));
}
async findCached(scope: RoleScopes, name: RoleNames) {
const cacheKey = `role:${scope}:${name}`;
const cachedRole = await this.cacheService.get(cacheKey);
if (cachedRole) return this.roleRepository.create(cachedRole);
let dbRole = await this.roleRepository.findRole(scope, name);
if (dbRole === null) {
if (!this.isValid(scope, name)) {
throw new InvalidRoleError(`${scope}:${name} is not a valid role`);
}
const toSave = this.roleRepository.create({ scope, name });
dbRole = await this.roleRepository.save(toSave);
}
void this.cacheService.set(cacheKey, dbRole);
return dbRole;
}
private roles: Array<{ name: RoleNames; scope: RoleScopes }> = [
{ scope: 'global', name: 'owner' },
{ scope: 'global', name: 'member' },
{ scope: 'global', name: 'admin' },
{ scope: 'workflow', name: 'owner' },
{ scope: 'credential', name: 'owner' },
{ scope: 'credential', name: 'user' },
{ scope: 'workflow', name: 'editor' },
];
listRoles() {
return this.roles;
}
private isValid(scope: RoleScopes, name: RoleNames) {
return this.roles.some((r) => r.scope === scope && r.name === name);
}
async findGlobalOwnerRole() {
return await this.findCached('global', 'owner');
}
async findGlobalMemberRole() {
return await this.findCached('global', 'member');
}
async findGlobalAdminRole() {
return await this.findCached('global', 'admin');
}
async findWorkflowOwnerRole() {
return await this.findCached('workflow', 'owner');
}
async findWorkflowEditorRole() {
return await this.findCached('workflow', 'editor');
}
async findCredentialOwnerRole() {
return await this.findCached('credential', 'owner');
}
async findCredentialUserRole() {
return await this.findCached('credential', 'user');
}
async findRoleByUserAndWorkflow(userId: string, workflowId: string) {
return await this.sharedWorkflowRepository
.findOne({
where: { workflowId, userId },
relations: ['role'],
})
.then((shared) => shared?.role);
}
async findCredentialOwnerRoleId() {
return this.license.isSharingEnabled() ? undefined : (await this.findCredentialOwnerRole()).id;
}
}

View file

@ -1,5 +1,5 @@
import { Container, Service } from 'typedi';
import { User } from '@db/entities/User';
import { type AssignableRole, User } from '@db/entities/User';
import type { IUserSettings } from 'n8n-workflow';
import { UserRepository } from '@db/repositories/user.repository';
import type { PublicUser } from '@/Interfaces';
@ -10,7 +10,6 @@ import { Logger } from '@/Logger';
import { createPasswordSha } from '@/auth/jwt';
import { UserManagementMailer } from '@/UserManagement/email';
import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import { UrlService } from '@/services/url.service';
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import type { UserRequest } from '@/requests';
@ -23,7 +22,6 @@ export class UserService {
private readonly userRepository: UserRepository,
private readonly jwtService: JwtService,
private readonly mailer: UserManagementMailer,
private readonly roleService: RoleService,
private readonly urlService: UrlService,
) {}
@ -73,7 +71,7 @@ export class UserService {
const user = await this.userRepository.findOne({
where: { id: decodedToken.sub },
relations: ['authIdentities', 'globalRole'],
relations: ['authIdentities'],
});
if (!user) {
@ -162,7 +160,7 @@ export class UserService {
private async sendEmails(
owner: User,
toInviteUsers: { [key: string]: string },
role: 'member' | 'admin',
role: AssignableRole,
) {
const domain = this.urlService.getInstanceBaseUrl();
@ -224,9 +222,7 @@ export class UserService {
);
}
async inviteUsers(owner: User, attributes: Array<{ email: string; role: 'member' | 'admin' }>) {
const memberRole = await this.roleService.findGlobalMemberRole();
const adminRole = await this.roleService.findGlobalAdminRole();
async inviteUsers(owner: User, attributes: Array<{ email: string; role: AssignableRole }>) {
const emails = attributes.map(({ email }) => email);
const existingUsers = await this.userRepository.findManyByEmail(emails);
@ -250,10 +246,7 @@ export class UserService {
async (transactionManager) =>
await Promise.all(
toCreateUsers.map(async ({ email, role }) => {
const newUser = Object.assign(new User(), {
email,
globalRole: role === 'member' ? memberRole : adminRole,
});
const newUser = transactionManager.create(User, { email, role });
const savedUser = await transactionManager.save<User>(newUser);
createdUsers.set(email, savedUser.id);
return savedUser;

View file

@ -4,7 +4,6 @@ import { In } from 'typeorm';
import type { User } from '@db/entities/User';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { RoleService } from '@/services/role.service';
import { UserService } from '@/services/user.service';
@Service()
@ -12,7 +11,6 @@ export class UserOnboardingService {
constructor(
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowRepository: WorkflowRepository,
private readonly roleService: RoleService,
private readonly userService: UserService,
) {}
@ -24,12 +22,11 @@ export class UserOnboardingService {
let belowThreshold = true;
const skippedTypes = ['n8n-nodes-base.start', 'n8n-nodes-base.stickyNote'];
const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole();
const ownedWorkflowsIds = await this.sharedWorkflowRepository
.find({
where: {
userId: user.id,
roleId: workflowOwnerRole?.id,
role: 'workflow:owner',
},
select: ['workflowId'],
})

View file

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

View file

@ -17,7 +17,6 @@ import {
} from '../ssoHelpers';
import { getServiceProviderConfigTestReturnUrl } from './serviceProvider.ee';
import type { SamlConfiguration } from './types/requests';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
@ -104,7 +103,7 @@ export async function createUserFromSamlAttributes(attributes: SamlUserAttribute
user.email = lowerCasedEmail;
user.firstName = attributes.firstName;
user.lastName = attributes.lastName;
user.globalRole = await Container.get(RoleService).findGlobalMemberRole();
user.role = 'global:member';
// generates a password that is not used or known to the user
user.password = await Container.get(PasswordUtility).hash(generatePassword());
authIdentity.providerId = attributes.userPrincipalName;

View file

@ -11,7 +11,7 @@ import { License } from '@/License';
import { N8N_VERSION } from '@/constants';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee';
import { RoleRepository } from '@/databases/repositories/role.repository';
import { UserRepository } from '@db/repositories/user.repository';
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
@ -111,7 +111,7 @@ export class Telemetry {
plan_name_current: this.license.getPlanName(),
quota: this.license.getTriggerLimit(),
usage: await this.workflowRepository.getActiveTriggerCount(),
role_count: await Container.get(RoleRepository).countUsersByRole(),
role_count: await Container.get(UserRepository).countUsersByRole(),
source_control_set_up: Container.get(SourceControlPreferencesService).isSourceControlSetup(),
branchName: sourceControlPreferences.branchName,
read_only_instance: sourceControlPreferences.branchReadOnly,

View file

@ -34,10 +34,10 @@ export class EnterpriseWorkflowService {
user,
workflowId,
{ allowGlobalScope: false },
['workflow', 'role'],
['workflow'],
);
if (!sharing || sharing.role.name !== 'owner') return { ownsWorkflow: false };
if (!sharing || sharing.role !== 'workflow:owner') return { ownsWorkflow: false };
const { workflow } = sharing;
@ -54,7 +54,7 @@ export class EnterpriseWorkflowService {
workflow.shared?.forEach(({ user, role }) => {
const { id, email, firstName, lastName } = user;
if (role.name === 'owner') {
if (role === 'workflow:owner') {
workflow.ownedBy = { id, email, firstName, lastName };
return;
}
@ -101,7 +101,7 @@ export class EnterpriseWorkflowService {
};
credential.shared?.forEach(({ user, role }) => {
const { id, email, firstName, lastName } = user;
if (role.name === 'owner') {
if (role === 'credential:owner') {
workflowCredential.ownedBy = { id, email, firstName, lastName };
} else {
workflowCredential.sharedWith?.push({ id, email, firstName, lastName });

View file

@ -8,6 +8,7 @@ import { BinaryDataService } from 'n8n-core';
import config from '@/config';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository';
@ -60,7 +61,7 @@ export class WorkflowService {
workflowId: string,
tagIds?: string[],
forceSave?: boolean,
roles?: string[],
roles?: WorkflowSharingRole[],
): Promise<WorkflowEntity> {
const shared = await this.sharedWorkflowRepository.findSharing(
workflowId,
@ -250,7 +251,7 @@ export class WorkflowService {
workflowId,
user,
'workflow:delete',
{ roles: ['owner'] },
{ roles: ['workflow:owner'] },
);
if (!sharedWorkflow) {

View file

@ -1,31 +1,25 @@
import { Service } from 'typedi';
import { In, type FindOptionsWhere } from 'typeorm';
import type { RoleNames } from '@db/entities/Role';
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { SharedWorkflow, WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import type { User } from '@db/entities/User';
import { RoleRepository } from '@db/repositories/role.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@Service()
export class WorkflowSharingService {
constructor(
private readonly roleRepository: RoleRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
) {}
constructor(private readonly sharedWorkflowRepository: SharedWorkflowRepository) {}
/**
* Get the IDs of the workflows that have been shared with the user.
* Returns all IDs if user has the 'workflow:read' scope.
*/
async getSharedWorkflowIds(user: User, roleNames?: RoleNames[]): Promise<string[]> {
async getSharedWorkflowIds(user: User, roles?: WorkflowSharingRole[]): Promise<string[]> {
const where: FindOptionsWhere<SharedWorkflow> = {};
if (!user.hasGlobalScope('workflow:read')) {
where.userId = user.id;
}
if (roleNames?.length) {
const roleIds = await this.roleRepository.getIdsInScopeWorkflowByNames(roleNames);
where.roleId = In(roleIds);
if (roles?.length) {
where.role = In(roles);
}
const sharedWorkflows = await this.sharedWorkflowRepository.find({
where,

View file

@ -10,8 +10,7 @@ import * as WorkflowHelpers from '@/WorkflowHelpers';
import type { IWorkflowResponse } from '@/Interfaces';
import config from '@/config';
import { Authorized, Delete, Get, Patch, Post, Put, RestController } from '@/decorators';
import type { RoleNames } from '@db/entities/Role';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { TagRepository } from '@db/repositories/tag.repository';
@ -23,7 +22,6 @@ import { ListQuery } from '@/requests';
import { WorkflowService } from './workflow.service';
import { License } from '@/License';
import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import * as utils from '@/utils';
import { listQueryMiddleware } from '@/middlewares';
import { TagService } from '@/services/tag.service';
@ -53,7 +51,6 @@ export class WorkflowsController {
private readonly externalHooks: ExternalHooks,
private readonly tagRepository: TagRepository,
private readonly enterpriseWorkflowService: EnterpriseWorkflowService,
private readonly roleService: RoleService,
private readonly workflowHistoryService: WorkflowHistoryService,
private readonly tagService: TagService,
private readonly namingService: NamingService,
@ -116,12 +113,10 @@ export class WorkflowsController {
await Db.transaction(async (transactionManager) => {
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
const role = await this.roleService.findWorkflowOwnerRole();
const newSharedWorkflow = new SharedWorkflow();
Object.assign(newSharedWorkflow, {
role,
role: 'workflow:owner',
user: req.user,
workflow: savedWorkflow,
});
@ -151,7 +146,9 @@ export class WorkflowsController {
@Get('/', { middlewares: listQueryMiddleware })
async getAll(req: ListQuery.Request, res: express.Response) {
try {
const roles: RoleNames[] = this.license.isSharingEnabled() ? [] : ['owner'];
const roles: WorkflowSharingRole[] = this.license.isSharingEnabled()
? []
: ['workflow:owner'];
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(
req.user,
roles,
@ -223,7 +220,7 @@ export class WorkflowsController {
const { id: workflowId } = req.params;
if (this.license.isSharingEnabled()) {
const relations = ['shared', 'shared.user', 'shared.role'];
const relations = ['shared', 'shared.user'];
if (!config.getEnv('workflowTagsDisabled')) {
relations.push('tags');
}
@ -281,7 +278,8 @@ export class WorkflowsController {
const { tags, ...rest } = req.body;
Object.assign(updateData, rest);
if (this.license.isSharingEnabled()) {
const isSharingEnabled = this.license.isSharingEnabled();
if (isSharingEnabled) {
updateData = await this.enterpriseWorkflowService.preventTampering(
updateData,
workflowId,
@ -294,8 +292,8 @@ export class WorkflowsController {
updateData,
workflowId,
tags,
this.license.isSharingEnabled() ? forceSave : true,
this.license.isSharingEnabled() ? undefined : ['owner'],
isSharingEnabled ? forceSave : true,
isSharingEnabled ? undefined : ['workflow:owner'],
);
return updatedWorkflow;
@ -378,10 +376,10 @@ export class WorkflowsController {
await this.workflowRepository.getSharings(
Db.getConnection().createEntityManager(),
workflowId,
['shared', 'shared.role'],
['shared'],
)
)
.filter((e) => e.role.name === 'owner')
.filter((e) => e.role === 'workflow:owner')
.map((e) => e.userId);
let newShareeIds: string[] = [];
@ -399,9 +397,7 @@ export class WorkflowsController {
if (newShareeIds.length) {
const users = await this.userRepository.getByIds(trx, newShareeIds);
const role = await this.roleService.findWorkflowEditorRole();
await this.sharedWorkflowRepository.share(trx, workflow!, users, role.id);
await this.sharedWorkflowRepository.share(trx, workflow!, users);
}
});

View file

@ -3,19 +3,15 @@ import { Container } from 'typedi';
import validator from 'validator';
import config from '@/config';
import { AUTH_COOKIE_NAME } from '@/constants';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants';
import { randomValidPassword } from './shared/random';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils/';
import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles';
import { createUser, createUserShell } from './shared/db/users';
import { UserRepository } from '@db/repositories/user.repository';
import { MfaService } from '@/Mfa/mfa.service';
let globalOwnerRole: Role;
let globalMemberRole: Role;
let owner: User;
let authOwnerAgent: SuperAgentTest;
const ownerPassword = randomValidPassword();
@ -26,8 +22,6 @@ const license = testServer.license;
let mfaService: MfaService;
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
globalMemberRole = await getGlobalMemberRole();
mfaService = Container.get(MfaService);
});
@ -41,7 +35,7 @@ describe('POST /login', () => {
beforeEach(async () => {
owner = await createUser({
password: ownerPassword,
globalRole: globalOwnerRole,
role: 'global:owner',
});
});
@ -60,7 +54,7 @@ describe('POST /login', () => {
lastName,
password,
personalizationAnswers,
globalRole,
role,
apiKey,
globalScopes,
mfaSecret,
@ -74,9 +68,7 @@ describe('POST /login', () => {
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
expect(globalScopes).toBeDefined();
expect(mfaRecoveryCodes).toBeUndefined();
@ -107,7 +99,7 @@ describe('POST /login', () => {
lastName,
password,
personalizationAnswers,
globalRole,
role,
apiKey,
mfaRecoveryCodes,
mfaSecret,
@ -120,9 +112,7 @@ describe('POST /login', () => {
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
expect(mfaRecoveryCodes).toBeUndefined();
expect(mfaSecret).toBeUndefined();
@ -149,7 +139,7 @@ describe('POST /login', () => {
license.setQuota('quota:users', 0);
const ownerUser = await createUser({
password: randomValidPassword(),
globalRole: globalOwnerRole,
role: 'global:owner',
});
const response = await testServer.authAgentFor(ownerUser).get('/login');
@ -168,7 +158,7 @@ describe('GET /login', () => {
});
test('should return cookie if UM is disabled and no cookie is already set', async () => {
await createUserShell(globalOwnerRole);
await createUserShell('global:owner');
await utils.setInstanceOwnerSetUp(false);
const response = await testServer.authlessAgent.get('/login');
@ -191,7 +181,7 @@ describe('GET /login', () => {
});
test('should return logged-in owner shell', async () => {
const ownerShell = await createUserShell(globalOwnerRole);
const ownerShell = await createUserShell('global:owner');
const response = await testServer.authAgentFor(ownerShell).get('/login');
@ -204,7 +194,7 @@ describe('GET /login', () => {
lastName,
password,
personalizationAnswers,
globalRole,
role,
apiKey,
globalScopes,
} = response.body.data;
@ -216,9 +206,7 @@ describe('GET /login', () => {
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
expect(globalScopes).toBeDefined();
expect(globalScopes).toContain('workflow:read');
@ -228,7 +216,7 @@ describe('GET /login', () => {
});
test('should return logged-in member shell', async () => {
const memberShell = await createUserShell(globalMemberRole);
const memberShell = await createUserShell('global:member');
const response = await testServer.authAgentFor(memberShell).get('/login');
@ -241,7 +229,7 @@ describe('GET /login', () => {
lastName,
password,
personalizationAnswers,
globalRole,
role,
apiKey,
globalScopes,
} = response.body.data;
@ -253,9 +241,7 @@ describe('GET /login', () => {
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:member');
expect(apiKey).toBeUndefined();
expect(globalScopes).toBeDefined();
expect(globalScopes).not.toContain('workflow:read');
@ -265,7 +251,7 @@ describe('GET /login', () => {
});
test('should return logged-in owner', async () => {
const owner = await createUser({ globalRole: globalOwnerRole });
const owner = await createUser({ role: 'global:owner' });
const response = await testServer.authAgentFor(owner).get('/login');
@ -278,7 +264,7 @@ describe('GET /login', () => {
lastName,
password,
personalizationAnswers,
globalRole,
role,
apiKey,
globalScopes,
} = response.body.data;
@ -290,9 +276,7 @@ describe('GET /login', () => {
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
expect(globalScopes).toBeDefined();
expect(globalScopes).toContain('workflow:read');
@ -302,7 +286,7 @@ describe('GET /login', () => {
});
test('should return logged-in member', async () => {
const member = await createUser({ globalRole: globalMemberRole });
const member = await createUser({ role: 'global:member' });
const response = await testServer.authAgentFor(member).get('/login');
@ -315,7 +299,7 @@ describe('GET /login', () => {
lastName,
password,
personalizationAnswers,
globalRole,
role,
apiKey,
globalScopes,
} = response.body.data;
@ -327,9 +311,7 @@ describe('GET /login', () => {
expect(password).toBeUndefined();
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(globalRole).toBeDefined();
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:member');
expect(apiKey).toBeUndefined();
expect(globalScopes).toBeDefined();
expect(globalScopes).not.toContain('workflow:read');
@ -343,13 +325,13 @@ describe('GET /resolve-signup-token', () => {
beforeEach(async () => {
owner = await createUser({
password: ownerPassword,
globalRole: globalOwnerRole,
role: 'global:owner',
});
authOwnerAgent = testServer.authAgentFor(owner);
});
test('should validate invite token', async () => {
const memberShell = await createUserShell(globalMemberRole);
const memberShell = await createUserShell('global:member');
const response = await authOwnerAgent
.get('/resolve-signup-token')
@ -369,7 +351,7 @@ describe('GET /resolve-signup-token', () => {
test('should return 403 if user quota reached', async () => {
license.setQuota('quota:users', 0);
const memberShell = await createUserShell(globalMemberRole);
const memberShell = await createUserShell('global:member');
const response = await authOwnerAgent
.get('/resolve-signup-token')
@ -380,7 +362,7 @@ describe('GET /resolve-signup-token', () => {
});
test('should fail with invalid inputs', async () => {
const { id: inviteeId } = await createUser({ globalRole: globalMemberRole });
const { id: inviteeId } = await createUser({ role: 'global:member' });
const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id });
@ -412,7 +394,7 @@ describe('GET /resolve-signup-token', () => {
describe('POST /logout', () => {
test('should log user out', async () => {
const owner = await createUser({ globalRole: globalOwnerRole });
const owner = await createUser({ role: 'global:owner' });
const response = await testServer.authAgentFor(owner).post('/logout');

View file

@ -1,8 +1,8 @@
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import type { SuperAgentTest } from 'supertest';
import * as utils from './shared/utils/';
import { getGlobalMemberRole } from './shared/db/roles';
import { createUser } from './shared/db/users';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { mockInstance } from '../shared/mocking';
describe('Auth Middleware', () => {
@ -42,8 +42,7 @@ describe('Auth Middleware', () => {
describe('Routes requiring Authorization', () => {
let authMemberAgent: SuperAgentTest;
beforeAll(async () => {
const globalMemberRole = await getGlobalMemberRole();
const member = await createUser({ globalRole: globalMemberRole });
const member = await createUser({ role: 'global:member' });
authMemberAgent = testServer.authAgentFor(member);
});

View file

@ -1,5 +1,4 @@
import { Reset } from '@/commands/user-management/reset';
import type { Role } from '@db/entities/Role';
import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
@ -8,18 +7,13 @@ import { UserRepository } from '@db/repositories/user.repository';
import { mockInstance } from '../../shared/mocking';
import * as testDb from '../shared/testDb';
import { getGlobalOwnerRole } from '../shared/db/roles';
import { createUser } from '../shared/db/users';
let globalOwnerRole: Role;
beforeAll(async () => {
mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
mockInstance(NodeTypes);
await testDb.init();
globalOwnerRole = await getGlobalOwnerRole();
});
beforeEach(async () => {
@ -32,11 +26,11 @@ afterAll(async () => {
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
test.skip('user-management:reset should reset DB to default user state', async () => {
await createUser({ globalRole: globalOwnerRole });
await createUser({ role: 'global:owner' });
await Reset.run();
const user = await Container.get(UserRepository).findOneBy({ globalRoleId: globalOwnerRole.id });
const user = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
if (!user) {
fail('No owner found after DB reset to default user state');

View file

@ -18,6 +18,7 @@ import { OrchestrationHandlerWorkerService } from '@/services/orchestration/work
import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service';
import { OrchestrationService } from '@/services/orchestration.service';
import * as testDb from '../shared/testDb';
import { mockInstance } from '../../shared/mocking';
const oclifConfig = new Config({ root: __dirname });
@ -39,6 +40,11 @@ beforeAll(async () => {
mockInstance(RedisServicePubSubPublisher);
mockInstance(RedisServicePubSubSubscriber);
mockInstance(OrchestrationService);
await testDb.init();
});
afterAll(async () => {
await testDb.terminate();
});
test('worker initializes all its components', async () => {

View file

@ -5,7 +5,6 @@ import { setupTestServer } from './shared/utils/';
import { randomCredentialPayload as payload } from './shared/random';
import { saveCredential } from './shared/db/credentials';
import { createMember, createOwner } from './shared/db/users';
import { getCredentialOwnerRole } from './shared/db/roles';
const { any } = expect;
@ -26,10 +25,14 @@ type GetAllResponse = { body: { data: ListQuery.Credentials.WithOwnedByAndShared
describe('GET /credentials', () => {
describe('should return', () => {
test('all credentials for owner', async () => {
const role = await getCredentialOwnerRole();
const { id: id1 } = await saveCredential(payload(), { user: owner, role });
const { id: id2 } = await saveCredential(payload(), { user: member, role });
const { id: id1 } = await saveCredential(payload(), {
user: owner,
role: 'credential:owner',
});
const { id: id2 } = await saveCredential(payload(), {
user: member,
role: 'credential:owner',
});
const response: GetAllResponse = await testServer
.authAgentFor(owner)
@ -47,13 +50,11 @@ describe('GET /credentials', () => {
});
test('only own credentials for member', async () => {
const role = await getCredentialOwnerRole();
const firstMember = member;
const secondMember = await createMember();
const c1 = await saveCredential(payload(), { user: firstMember, role });
const c2 = await saveCredential(payload(), { user: secondMember, role });
const c1 = await saveCredential(payload(), { user: firstMember, role: 'credential:owner' });
const c2 = await saveCredential(payload(), { user: secondMember, role: 'credential:owner' });
const response: GetAllResponse = await testServer
.authAgentFor(firstMember)
@ -72,8 +73,7 @@ describe('GET /credentials', () => {
describe('filter', () => {
test('should filter credentials by field: name - full match', async () => {
const role = await getCredentialOwnerRole();
const savedCred = await saveCredential(payload(), { user: owner, role });
const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response: GetAllResponse = await testServer
.authAgentFor(owner)
@ -97,8 +97,7 @@ describe('GET /credentials', () => {
});
test('should filter credentials by field: name - partial match', async () => {
const role = await getCredentialOwnerRole();
const savedCred = await saveCredential(payload(), { user: owner, role });
const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const partialName = savedCred.name.slice(3);
@ -124,9 +123,7 @@ describe('GET /credentials', () => {
});
test('should filter credentials by field: type - full match', async () => {
const role = await getCredentialOwnerRole();
const savedCred = await saveCredential(payload(), { user: owner, role });
const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response: GetAllResponse = await testServer
.authAgentFor(owner)
@ -150,9 +147,7 @@ describe('GET /credentials', () => {
});
test('should filter credentials by field: type - partial match', async () => {
const role = await getCredentialOwnerRole();
const savedCred = await saveCredential(payload(), { user: owner, role });
const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const partialType = savedCred.type.slice(3);
@ -180,10 +175,8 @@ describe('GET /credentials', () => {
describe('select', () => {
test('should select credential field: id', async () => {
const role = await getCredentialOwnerRole();
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response: GetAllResponse = await testServer
.authAgentFor(owner)
@ -197,10 +190,8 @@ describe('GET /credentials', () => {
});
test('should select credential field: name', async () => {
const role = await getCredentialOwnerRole();
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response: GetAllResponse = await testServer
.authAgentFor(owner)
@ -214,10 +205,8 @@ describe('GET /credentials', () => {
});
test('should select credential field: type', async () => {
const role = await getCredentialOwnerRole();
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response: GetAllResponse = await testServer
.authAgentFor(owner)
@ -233,10 +222,8 @@ describe('GET /credentials', () => {
describe('take', () => {
test('should return n credentials or less, without skip', async () => {
const role = await getCredentialOwnerRole();
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response = await testServer
.authAgentFor(owner)
@ -260,10 +247,8 @@ describe('GET /credentials', () => {
});
test('should return n credentials or less, with skip', async () => {
const role = await getCredentialOwnerRole();
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const response = await testServer
.authAgentFor(owner)

View file

@ -1,28 +1,26 @@
import { Container } from 'typedi';
import type { SuperAgentTest } from 'supertest';
import { In } from 'typeorm';
import type { IUser } from 'n8n-workflow';
import type { ListQuery } from '@/requests';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { License } from '@/License';
import { randomCredentialPayload } from './shared/random';
import * as testDb from './shared/testDb';
import type { SaveCredentialFunction } from './shared/types';
import * as utils from './shared/utils/';
import { affixRoleToSaveCredential, shareCredentialWithUsers } from './shared/db/credentials';
import { getCredentialOwnerRole, getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles';
import { createManyUsers, createUser, createUserShell } from './shared/db/users';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import Container from 'typedi';
import { License } from '@/License';
import { mockInstance } from '../shared/mocking';
import { UserManagementMailer } from '@/UserManagement/email';
import { mockInstance } from '../shared/mocking';
const sharingSpy = jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(true);
const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] });
let globalMemberRole: Role;
let owner: User;
let member: User;
let anotherMember: User;
@ -32,18 +30,14 @@ let saveCredential: SaveCredentialFunction;
const mailer = mockInstance(UserManagementMailer);
beforeAll(async () => {
const globalOwnerRole = await getGlobalOwnerRole();
globalMemberRole = await getGlobalMemberRole();
const credentialOwnerRole = await getCredentialOwnerRole();
owner = await createUser({ globalRole: globalOwnerRole });
member = await createUser({ globalRole: globalMemberRole });
anotherMember = await createUser({ globalRole: globalMemberRole });
owner = await createUser({ role: 'global:owner' });
member = await createUser({ role: 'global:member' });
anotherMember = await createUser({ role: 'global:member' });
authOwnerAgent = testServer.authAgentFor(owner);
authAnotherMemberAgent = testServer.authAgentFor(anotherMember);
saveCredential = affixRoleToSaveCredential(credentialOwnerRole);
saveCredential = affixRoleToSaveCredential('credential:owner');
});
beforeEach(async () => {
@ -92,7 +86,7 @@ describe('router should switch based on flag', () => {
describe('GET /credentials', () => {
test('should return all creds for owner', async () => {
const [member1, member2, member3] = await createManyUsers(3, {
globalRole: globalMemberRole,
role: 'global:member',
});
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
@ -156,7 +150,7 @@ describe('GET /credentials', () => {
test('should return only relevant creds for member', async () => {
const [member1, member2] = await createManyUsers(2, {
globalRole: globalMemberRole,
role: 'global:member',
});
await saveCredential(randomCredentialPayload(), { user: member2 });
@ -232,7 +226,7 @@ describe('GET /credentials/:id', () => {
test('should retrieve non-owned cred for owner', async () => {
const [member1, member2] = await createManyUsers(2, {
globalRole: globalMemberRole,
role: 'global:member',
});
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
@ -271,7 +265,7 @@ describe('GET /credentials/:id', () => {
test('should retrieve owned cred for member', async () => {
const [member1, member2, member3] = await createManyUsers(3, {
globalRole: globalMemberRole,
role: 'global:member',
});
const authMemberAgent = testServer.authAgentFor(member1);
const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 });
@ -339,7 +333,7 @@ describe('PUT /credentials/:id/share', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2, member3, member4, member5] = await createManyUsers(5, {
globalRole: globalMemberRole,
role: 'global:member',
});
const shareWithIds = [member1.id, member2.id, member3.id];
@ -353,7 +347,6 @@ describe('PUT /credentials/:id/share', () => {
expect(response.body.data).toBeUndefined();
const sharedCredentials = await Container.get(SharedCredentialsRepository).find({
relations: ['role'],
where: { credentialsId: savedCredential.id },
});
@ -362,13 +355,11 @@ describe('PUT /credentials/:id/share', () => {
sharedCredentials.forEach((sharedCredential) => {
if (sharedCredential.userId === owner.id) {
expect(sharedCredential.role.name).toBe('owner');
expect(sharedCredential.role.scope).toBe('credential');
expect(sharedCredential.role).toBe('credential:owner');
return;
}
expect(shareWithIds).toContain(sharedCredential.userId);
expect(sharedCredential.role.name).toBe('user');
expect(sharedCredential.role.scope).toBe('credential');
expect(sharedCredential.role).toBe('credential:user');
});
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1);
@ -376,7 +367,7 @@ describe('PUT /credentials/:id/share', () => {
test('should share the credential with the provided userIds', async () => {
const [member1, member2, member3] = await createManyUsers(3, {
globalRole: globalMemberRole,
role: 'global:member',
});
const memberIds = [member1.id, member2.id, member3.id];
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
@ -390,25 +381,21 @@ describe('PUT /credentials/:id/share', () => {
// check that sharings got correctly set in DB
const sharedCredentials = await Container.get(SharedCredentialsRepository).find({
relations: ['role'],
where: { credentialsId: savedCredential.id, userId: In([...memberIds]) },
});
expect(sharedCredentials.length).toBe(memberIds.length);
sharedCredentials.forEach((sharedCredential) => {
expect(sharedCredential.role.name).toBe('user');
expect(sharedCredential.role.scope).toBe('credential');
expect(sharedCredential.role).toBe('credential:user');
});
// check that owner still exists
const ownerSharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({
relations: ['role'],
where: { credentialsId: savedCredential.id, userId: owner.id },
});
expect(ownerSharedCredential.role.name).toBe('owner');
expect(ownerSharedCredential.role.scope).toBe('credential');
expect(ownerSharedCredential.role).toBe('credential:owner');
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1);
});
@ -456,7 +443,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({ globalRole: globalMemberRole });
const tempUser = await createUser({ role: 'global:member' });
const response = await authAnotherMemberAgent
.put(`/credentials/${savedCredential.id}/share`)
@ -487,7 +474,7 @@ describe('PUT /credentials/:id/share', () => {
});
test('should ignore pending sharee', async () => {
const memberShell = await createUserShell(globalMemberRole);
const memberShell = await createUserShell('global:member');
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const response = await authOwnerAgent
@ -538,7 +525,7 @@ describe('PUT /credentials/:id/share', () => {
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
const [member1, member2] = await createManyUsers(2, {
globalRole: globalMemberRole,
role: 'global:member',
});
await shareCredentialWithUsers(savedCredential, [member1, member2]);

View file

@ -1,28 +1,24 @@
import { Container } from 'typedi';
import type { SuperAgentTest } from 'supertest';
import config from '@/config';
import type { ListQuery } from '@/requests';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { License } from '@/License';
import { randomCredentialPayload, randomName, randomString } from './shared/random';
import * as testDb from './shared/testDb';
import type { SaveCredentialFunction } from './shared/types';
import * as utils from './shared/utils/';
import { affixRoleToSaveCredential, shareCredentialWithUsers } from './shared/db/credentials';
import { getCredentialOwnerRole, getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles';
import { createManyUsers, createUser } from './shared/db/users';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import Container from 'typedi';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { License } from '@/License';
// mock that credentialsSharing is not enabled
jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(false);
const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] });
let globalOwnerRole: Role;
let globalMemberRole: Role;
let owner: User;
let member: User;
let secondMember: User;
@ -31,15 +27,11 @@ let authMemberAgent: SuperAgentTest;
let saveCredential: SaveCredentialFunction;
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
globalMemberRole = await getGlobalMemberRole();
const credentialOwnerRole = await getCredentialOwnerRole();
owner = await createUser({ role: 'global:owner' });
member = await createUser({ role: 'global:member' });
secondMember = await createUser({ role: 'global:member' });
owner = await createUser({ globalRole: globalOwnerRole });
member = await createUser({ globalRole: globalMemberRole });
secondMember = await createUser({ globalRole: globalMemberRole });
saveCredential = affixRoleToSaveCredential(credentialOwnerRole);
saveCredential = affixRoleToSaveCredential('credential:owner');
authOwnerAgent = testServer.authAgentFor(owner);
authMemberAgent = testServer.authAgentFor(member);
@ -74,7 +66,7 @@ describe('GET /credentials', () => {
test('should return only own creds for member', async () => {
const [member1, member2] = await createManyUsers(2, {
globalRole: globalMemberRole,
role: 'global:member',
});
const [savedCredential1] = await Promise.all([

View file

@ -8,7 +8,6 @@ import { SourceControlService } from '@/environments/sourceControl/sourceControl
import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile';
import * as utils from '../shared/utils/';
import { getGlobalOwnerRole } from '../shared/db/roles';
import { createUser } from '../shared/db/users';
let authOwnerAgent: SuperAgentTest;
@ -20,8 +19,7 @@ const testServer = utils.setupTestServer({
});
beforeAll(async () => {
const globalOwnerRole = await getGlobalOwnerRole();
owner = await createUser({ globalRole: globalOwnerRole });
owner = await createUser({ role: 'global:owner' });
authOwnerAgent = testServer.authAgentFor(owner);
Container.get(SourceControlPreferencesService).isSourceControlConnected = () => true;

View file

@ -3,7 +3,6 @@ import axios from 'axios';
import syslog from 'syslog-client';
import { v4 as uuid } from 'uuid';
import type { SuperAgentTest } from 'supertest';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import type {
MessageEventBusDestinationSentryOptions,
@ -26,7 +25,6 @@ import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessag
import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode';
import * as utils from './shared/utils';
import { getGlobalOwnerRole } from './shared/db/roles';
import { createUser } from './shared/db/users';
jest.unmock('@/eventbus/MessageEventBus/MessageEventBus');
@ -35,7 +33,6 @@ const mockedAxios = axios as jest.Mocked<typeof axios>;
jest.mock('syslog-client');
const mockedSyslog = syslog as jest.Mocked<typeof syslog>;
let globalOwnerRole: Role;
let owner: User;
let authOwnerAgent: SuperAgentTest;
@ -85,8 +82,7 @@ const testServer = utils.setupTestServer({
});
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
owner = await createUser({ globalRole: globalOwnerRole });
owner = await createUser({ role: 'global:owner' });
authOwnerAgent = testServer.authAgentFor(owner);
mockedSyslog.createClient.mockImplementation(() => new syslog.Client());

View file

@ -1,8 +1,6 @@
import type { SuperAgentTest } from 'supertest';
import * as utils from './shared/utils/';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { getGlobalOwnerRole } from './shared/db/roles';
import { createUser } from './shared/db/users';
/**
@ -11,7 +9,6 @@ import { createUser } from './shared/db/users';
* The tests in this file are only checking endpoint permissions.
*/
let globalOwnerRole: Role;
let owner: User;
let authOwnerAgent: SuperAgentTest;
@ -21,8 +18,7 @@ const testServer = utils.setupTestServer({
});
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
owner = await createUser({ globalRole: globalOwnerRole });
owner = await createUser({ role: 'global:owner' });
authOwnerAgent = testServer.authAgentFor(owner);
});

View file

@ -6,7 +6,6 @@ import type { INode } from 'n8n-workflow';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
import { ImportService } from '@/services/import.service';
import { RoleService } from '@/services/role.service';
import { TagEntity } from '@/databases/entities/TagEntity';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
@ -34,12 +33,7 @@ describe('ImportService', () => {
credentialsRepository.find.mockResolvedValue([]);
importService = new ImportService(
mock(),
credentialsRepository,
tagRepository,
Container.get(RoleService),
);
importService = new ImportService(mock(), credentialsRepository, tagRepository);
});
afterEach(async () => {
@ -67,10 +61,8 @@ describe('ImportService', () => {
await importService.importWorkflows([workflowToImport], owner.id);
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
const dbSharing = await Container.get(SharedWorkflowRepository).findOneOrFail({
where: { workflowId: workflowToImport.id, userId: owner.id, roleId: workflowOwnerRole.id },
where: { workflowId: workflowToImport.id, userId: owner.id, role: 'workflow:owner' },
});
expect(dbSharing.userId).toBe(owner.id);

View file

@ -17,7 +17,6 @@ import {
} from './shared/random';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils/';
import { getGlobalAdminRole, getGlobalMemberRole } from './shared/db/roles';
import { createMember, createOwner, createUser, createUserShell } from './shared/db/users';
import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
@ -56,8 +55,7 @@ describe('POST /invitations/:id/accept', () => {
});
test('should fill out a member shell', async () => {
const globalMemberRole = await getGlobalMemberRole();
const memberShell = await createUserShell(globalMemberRole);
const memberShell = await createUserShell('global:member');
const memberData = {
inviterId: owner.id,
@ -78,7 +76,7 @@ describe('POST /invitations/:id/accept', () => {
lastName,
personalizationAnswers,
password,
globalRole,
role,
isPending,
apiKey,
globalScopes,
@ -91,8 +89,7 @@ describe('POST /invitations/:id/accept', () => {
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole.scope).toBe('global');
expect(globalRole.name).toBe('member');
expect(role).toBe('global:member');
expect(apiKey).not.toBeDefined();
expect(globalScopes).toBeDefined();
expect(globalScopes).not.toHaveLength(0);
@ -110,8 +107,7 @@ describe('POST /invitations/:id/accept', () => {
});
test('should fill out an admin shell', async () => {
const globalAdminRole = await getGlobalAdminRole();
const adminShell = await createUserShell(globalAdminRole);
const adminShell = await createUserShell('global:admin');
const memberData = {
inviterId: owner.id,
@ -132,7 +128,7 @@ describe('POST /invitations/:id/accept', () => {
lastName,
personalizationAnswers,
password,
globalRole,
role,
isPending,
apiKey,
globalScopes,
@ -145,8 +141,7 @@ describe('POST /invitations/:id/accept', () => {
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole.scope).toBe('global');
expect(globalRole.name).toBe('admin');
expect(role).toBe('global:admin');
expect(apiKey).not.toBeDefined();
expect(globalScopes).toBeDefined();
expect(globalScopes).not.toHaveLength(0);
@ -166,11 +161,9 @@ describe('POST /invitations/:id/accept', () => {
test('should fail with invalid payloads', async () => {
const memberShellEmail = randomEmail();
const globalMemberRole = await getGlobalMemberRole();
const memberShell = await Container.get(UserRepository).save({
email: memberShellEmail,
globalRole: globalMemberRole,
role: 'global:member',
});
const invalidPayloads = [
@ -219,8 +212,7 @@ describe('POST /invitations/:id/accept', () => {
});
test('should fail with already accepted invite', async () => {
const globalMemberRole = await getGlobalMemberRole();
const member = await createUser({ globalRole: globalMemberRole });
const member = await createUser({ role: 'global:member' });
const memberData = {
inviterId: owner.id,
@ -334,7 +326,7 @@ describe('POST /invitations', () => {
const response = await ownerAgent
.post('/invitations')
.send([{ email: randomEmail(), role: 'admin' }])
.send([{ email: randomEmail(), role: 'global:admin' }])
.expect(200);
const [result] = response.body.data as UserInvitationResponse[];
@ -349,11 +341,11 @@ describe('POST /invitations', () => {
test('should reinvite member', async () => {
mailer.invite.mockResolvedValue({ emailSent: false });
await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'member' }]);
await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'global:member' }]);
await ownerAgent
.post('/invitations')
.send([{ email: randomEmail(), role: 'member' }])
.send([{ email: randomEmail(), role: 'global:member' }])
.expect(200);
});
@ -361,11 +353,11 @@ describe('POST /invitations', () => {
license.isAdvancedPermissionsLicensed.mockReturnValue(true);
mailer.invite.mockResolvedValue({ emailSent: false });
await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'admin' }]);
await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'global:admin' }]);
await ownerAgent
.post('/invitations')
.send([{ email: randomEmail(), role: 'admin' }])
.send([{ email: randomEmail(), role: 'global:admin' }])
.expect(200);
});
@ -375,7 +367,7 @@ describe('POST /invitations', () => {
await ownerAgent
.post('/invitations')
.send([{ email: randomEmail(), role: 'admin' }])
.send([{ email: randomEmail(), role: 'global:admin' }])
.expect(403);
});
@ -384,8 +376,7 @@ describe('POST /invitations', () => {
mailer.invite.mockResolvedValue({ emailSent: true });
const globalMemberRole = await getGlobalMemberRole();
const memberShell = await createUserShell(globalMemberRole);
const memberShell = await createUserShell('global:member');
const newUser = randomEmail();

View file

@ -6,7 +6,6 @@ import { jsonParse } from 'n8n-workflow';
import { Cipher } from 'n8n-core';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants';
import { LdapService } from '@/Ldap/ldap.service';
@ -18,7 +17,6 @@ import { randomEmail, randomName, uniqueId } from './../shared/random';
import * as testDb from './../shared/testDb';
import * as utils from '../shared/utils/';
import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles';
import { createLdapUser, createUser, getAllUsers, getLdapIdentities } from '../shared/db/users';
import { UserRepository } from '@db/repositories/user.repository';
import { SettingsRepository } from '@db/repositories/settings.repository';
@ -26,7 +24,6 @@ import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProvider
jest.mock('@/telemetry');
let globalMemberRole: Role;
let owner: User;
let authOwnerAgent: SuperAgentTest;
@ -50,14 +47,7 @@ const testServer = utils.setupTestServer({
});
beforeAll(async () => {
const [globalOwnerRole, fetchedGlobalMemberRole] = await Promise.all([
getGlobalOwnerRole(),
getGlobalMemberRole(),
]);
globalMemberRole = fetchedGlobalMemberRole;
owner = await createUser({ globalRole: globalOwnerRole, password: 'password' });
owner = await createUser({ role: 'global:owner', password: 'password' });
authOwnerAgent = testServer.authAgentFor(owner);
defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt(
@ -97,7 +87,7 @@ const createLdapConfig = async (attributes: Partial<LdapConfig> = {}): Promise<L
};
test('Member role should not be able to access ldap routes', async () => {
const member = await createUser({ globalRole: globalMemberRole });
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);
@ -169,7 +159,7 @@ describe('PUT /ldap/config', () => {
const ldapConfig = await createLdapConfig();
Container.get(LdapService).setConfig(ldapConfig);
const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId());
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
const configuration = ldapConfig;
@ -282,7 +272,7 @@ describe('POST /ldap/sync', () => {
const ldapUserId = uniqueId();
const member = await createLdapUser(
{ globalRole: globalMemberRole, email: ldapUserEmail },
{ role: 'global:member', email: ldapUserEmail },
ldapUserId,
);
@ -311,7 +301,7 @@ describe('POST /ldap/sync', () => {
const ldapUserId = uniqueId();
const member = await createLdapUser(
{ globalRole: globalMemberRole, email: ldapUserEmail },
{ role: 'global:member', email: ldapUserEmail },
ldapUserId,
);
@ -394,7 +384,7 @@ describe('POST /ldap/sync', () => {
await createLdapUser(
{
globalRole: globalMemberRole,
role: 'global:member',
email: ldapUser.mail,
firstName: ldapUser.givenName,
lastName: randomName(),
@ -427,7 +417,7 @@ describe('POST /ldap/sync', () => {
await createLdapUser(
{
globalRole: globalMemberRole,
role: 'global:member',
email: ldapUser.mail,
firstName: ldapUser.givenName,
lastName: ldapUser.sn,
@ -456,7 +446,7 @@ describe('POST /ldap/sync', () => {
});
test('should remove user instance access once the user is disabled during synchronization', async () => {
const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId());
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]);
@ -543,7 +533,7 @@ describe('POST /login', () => {
await createLdapUser(
{
globalRole: globalMemberRole,
role: 'global:member',
email: ldapUser.mail,
firstName: 'firstname',
lastName: 'lastname',
@ -577,7 +567,7 @@ describe('POST /login', () => {
};
await createUser({
globalRole: globalMemberRole,
role: 'global:member',
email: ldapUser.mail,
firstName: ldapUser.givenName,
lastName: 'lastname',
@ -592,7 +582,7 @@ describe('Instance owner should able to delete LDAP users', () => {
const ldapConfig = await createLdapConfig();
Container.get(LdapService).setConfig(ldapConfig);
const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId());
const member = await createLdapUser({ role: 'global:member' }, uniqueId());
await authOwnerAgent.post(`/users/${member.id}`);
});
@ -601,7 +591,7 @@ describe('Instance owner should able to delete LDAP users', () => {
const ldapConfig = await createLdapConfig();
Container.get(LdapService).setConfig(ldapConfig);
const member = await createLdapUser({ globalRole: globalMemberRole }, 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}`);

View file

@ -5,7 +5,6 @@ import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
import { License } from '@/License';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils/';
import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles';
import { createUserShell } from './shared/db/users';
const MOCK_SERVER_URL = 'https://server.com/v1';
@ -19,10 +18,8 @@ let authMemberAgent: SuperAgentTest;
const testServer = utils.setupTestServer({ endpointGroups: ['license'] });
beforeAll(async () => {
const globalOwnerRole = await getGlobalOwnerRole();
const globalMemberRole = await getGlobalMemberRole();
owner = await createUserShell(globalOwnerRole);
member = await createUserShell(globalMemberRole);
owner = await createUserShell('global:owner');
member = await createUserShell('global:member');
authOwnerAgent = testServer.authAgentFor(owner);
authMemberAgent = testServer.authAgentFor(member);

View file

@ -1,7 +1,6 @@
import type { SuperAgentTest } from 'supertest';
import { IsNull } from 'typeorm';
import validator from 'validator';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import {
@ -13,21 +12,12 @@ import {
} from './shared/random';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils/';
import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles';
import { addApiKey, createUser, createUserShell } from './shared/db/users';
import Container from 'typedi';
import { UserRepository } from '@db/repositories/user.repository';
const testServer = utils.setupTestServer({ endpointGroups: ['me'] });
let globalOwnerRole: Role;
let globalMemberRole: Role;
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
globalMemberRole = await getGlobalMemberRole();
});
beforeEach(async () => {
await testDb.truncate(['User']);
});
@ -37,7 +27,7 @@ describe('Owner shell', () => {
let authOwnerShellAgent: SuperAgentTest;
beforeEach(async () => {
ownerShell = await createUserShell(globalOwnerRole);
ownerShell = await createUserShell('global:owner');
await addApiKey(ownerShell);
authOwnerShellAgent = testServer.authAgentFor(ownerShell);
});
@ -54,7 +44,7 @@ describe('Owner shell', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
apiKey,
@ -67,8 +57,7 @@ describe('Owner shell', () => {
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
const storedOwnerShell = await Container.get(UserRepository).findOneByOrFail({ id });
@ -177,7 +166,7 @@ describe('Member', () => {
beforeEach(async () => {
member = await createUser({
password: memberPassword,
globalRole: globalMemberRole,
role: 'global:member',
apiKey: randomApiKey(),
});
authMemberAgent = testServer.authAgentFor(member);
@ -197,7 +186,7 @@ describe('Member', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
apiKey,
@ -210,8 +199,7 @@ describe('Member', () => {
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole.name).toBe('member');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:member');
expect(apiKey).toBeUndefined();
const storedMember = await Container.get(UserRepository).findOneByOrFail({ id });
@ -317,7 +305,7 @@ describe('Owner', () => {
});
test('PATCH /me should succeed with valid inputs', async () => {
const owner = await createUser({ globalRole: globalOwnerRole });
const owner = await createUser({ role: 'global:owner' });
const authOwnerAgent = testServer.authAgentFor(owner);
for (const validPayload of VALID_PATCH_ME_PAYLOADS) {
@ -331,7 +319,7 @@ describe('Owner', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
apiKey,
@ -344,8 +332,7 @@ describe('Owner', () => {
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
const storedOwner = await Container.get(UserRepository).findOneByOrFail({ id });

View file

@ -1,6 +1,5 @@
import Container from 'typedi';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { randomPassword } from '@/Ldap/helpers';
import { TOTPService } from '@/Mfa/totp.service';
@ -13,7 +12,6 @@ import { UserRepository } from '@db/repositories/user.repository';
jest.mock('@/telemetry');
let globalOwnerRole: Role;
let owner: User;
const testServer = utils.setupTestServer({
@ -23,7 +21,7 @@ const testServer = utils.setupTestServer({
beforeEach(async () => {
await testDb.truncate(['User']);
owner = await createUser({ globalRole: globalOwnerRole });
owner = await createUser({ role: 'global:owner' });
config.set('userManagement.disabled', false);
});

View file

@ -2,7 +2,6 @@ import validator from 'validator';
import type { SuperAgentTest } from 'supertest';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import {
randomEmail,
@ -12,23 +11,17 @@ import {
} from './shared/random';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils/';
import { getGlobalOwnerRole } from './shared/db/roles';
import { createUserShell } from './shared/db/users';
import { UserRepository } from '@db/repositories/user.repository';
import Container from 'typedi';
const testServer = utils.setupTestServer({ endpointGroups: ['owner'] });
let globalOwnerRole: Role;
let ownerShell: User;
let authOwnerShellAgent: SuperAgentTest;
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
});
beforeEach(async () => {
ownerShell = await createUserShell(globalOwnerRole);
ownerShell = await createUserShell('global:owner');
authOwnerShellAgent = testServer.authAgentFor(ownerShell);
config.set('userManagement.isInstanceOwnerSetUp', false);
});
@ -56,7 +49,7 @@ describe('POST /owner/setup', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
apiKey,
@ -70,8 +63,7 @@ describe('POST /owner/setup', () => {
expect(personalizationAnswers).toBeNull();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole.name).toBe('owner');
expect(globalRole.scope).toBe('global');
expect(role).toBe('global:owner');
expect(apiKey).toBeUndefined();
expect(globalScopes).not.toHaveLength(0);

View file

@ -5,7 +5,6 @@ import { mock } from 'jest-mock-extended';
import { License } from '@/License';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { ExternalHooks } from '@/ExternalHooks';
@ -24,14 +23,11 @@ import {
randomValidPassword,
} from './shared/random';
import * as testDb from './shared/testDb';
import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles';
import { createUser } from './shared/db/users';
import { PasswordUtility } from '@/services/password.utility';
config.set('userManagement.jwtSecret', randomString(5, 10));
let globalOwnerRole: Role;
let globalMemberRole: Role;
let owner: User;
let member: User;
@ -41,15 +37,10 @@ const testServer = setupTestServer({ endpointGroups: ['passwordReset'] });
const jwtService = Container.get(JwtService);
let userService: UserService;
beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole();
globalMemberRole = await getGlobalMemberRole();
});
beforeEach(async () => {
await testDb.truncate(['User']);
owner = await createUser({ globalRole: globalOwnerRole });
member = await createUser({ globalRole: globalMemberRole });
owner = await createUser({ role: 'global:owner' });
member = await createUser({ role: 'global:member' });
externalHooks.run.mockReset();
jest.replaceProperty(mailer, 'isEmailSetUp', true);
userService = Container.get(UserService);
@ -59,7 +50,7 @@ describe('POST /forgot-password', () => {
test('should send password reset email', async () => {
const member = await createUser({
email: 'test@test.com',
globalRole: globalMemberRole,
role: 'global:member',
});
await Promise.all(
@ -85,7 +76,7 @@ describe('POST /forgot-password', () => {
await setCurrentAuthenticationMethod('saml');
const member = await createUser({
email: 'test@test.com',
globalRole: globalMemberRole,
role: 'global:member',
});
await testServer.authlessAgent

View file

@ -1,5 +1,4 @@
import type { SuperAgentTest } from 'supertest';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import { randomApiKey, randomName, randomString } from '../shared/random';
@ -7,14 +6,11 @@ import * as utils from '../shared/utils/';
import type { CredentialPayload, SaveCredentialFunction } from '../shared/types';
import * as testDb from '../shared/testDb';
import { affixRoleToSaveCredential } from '../shared/db/credentials';
import { getAllRoles } from '../shared/db/roles';
import { addApiKey, createUser, createUserShell } from '../shared/db/users';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import Container from 'typedi';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
let globalMemberRole: Role;
let credentialOwnerRole: Role;
let owner: User;
let member: User;
let authOwnerAgent: SuperAgentTest;
@ -25,19 +21,13 @@ let saveCredential: SaveCredentialFunction;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => {
const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] =
await getAllRoles();
globalMemberRole = fetchedGlobalMemberRole;
credentialOwnerRole = fetchedCredentialOwnerRole;
owner = await addApiKey(await createUserShell(globalOwnerRole));
member = await createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() });
owner = await addApiKey(await createUserShell('global:owner'));
member = await createUser({ role: 'global:member', apiKey: randomApiKey() });
authOwnerAgent = testServer.publicApiAgentFor(owner);
authMemberAgent = testServer.publicApiAgentFor(member);
saveCredential = affixRoleToSaveCredential(credentialOwnerRole);
saveCredential = affixRoleToSaveCredential('credential:owner');
await utils.initCredentialsTypes();
});
@ -73,11 +63,11 @@ describe('POST /credentials', () => {
expect(credential.data).not.toBe(payload.data);
const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({
relations: ['user', 'credentials', 'role'],
relations: ['user', 'credentials'],
where: { credentialsId: credential.id, userId: owner.id },
});
expect(sharedCredential.role).toEqual(credentialOwnerRole);
expect(sharedCredential.role).toEqual('credential:owner');
expect(sharedCredential.credentials.name).toBe(payload.name);
});
@ -156,7 +146,7 @@ describe('DELETE /credentials/:id', () => {
test('should delete owned cred for member but leave others untouched', async () => {
const anotherMember = await createUser({
globalRole: globalMemberRole,
role: 'global:member',
apiKey: randomApiKey(),
});

View file

@ -5,7 +5,6 @@ import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { randomApiKey } from '../shared/random';
import * as utils from '../shared/utils/';
import * as testDb from '../shared/testDb';
import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles';
import { createUser } from '../shared/db/users';
import {
createManyWorkflows,
@ -30,11 +29,9 @@ let workflowRunner: ActiveWorkflowRunner;
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
beforeAll(async () => {
const globalOwnerRole = await getGlobalOwnerRole();
const globalUserRole = await getGlobalMemberRole();
owner = await createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() });
user1 = await createUser({ globalRole: globalUserRole, apiKey: randomApiKey() });
user2 = await createUser({ globalRole: globalUserRole, apiKey: randomApiKey() });
owner = await createUser({ role: 'global:owner', apiKey: randomApiKey() });
user1 = await createUser({ role: 'global:member', apiKey: randomApiKey() });
user2 = await createUser({ role: 'global:member', apiKey: randomApiKey() });
// TODO: mock BinaryDataService instead
await utils.initBinaryDataService();

View file

@ -2,14 +2,12 @@ import type { SuperAgentTest } from 'supertest';
import validator from 'validator';
import { v4 as uuid } from 'uuid';
import type { Role } from '@db/entities/Role';
import { License } from '@/License';
import { mockInstance } from '../../shared/mocking';
import { randomApiKey } from '../shared/random';
import * as utils from '../shared/utils/';
import * as testDb from '../shared/testDb';
import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles';
import { createUser, createUserShell } from '../shared/db/users';
mockInstance(License, {
@ -18,16 +16,6 @@ mockInstance(License, {
const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] });
let globalOwnerRole: Role;
let globalMemberRole: Role;
beforeAll(async () => {
[globalOwnerRole, globalMemberRole] = await Promise.all([
getGlobalOwnerRole(),
getGlobalMemberRole(),
]);
});
beforeEach(async () => {
await testDb.truncate(['SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials', 'User']);
});
@ -35,14 +23,14 @@ beforeEach(async () => {
describe('With license unlimited quota:users', () => {
describe('GET /users', () => {
test('should fail due to missing API Key', async () => {
const owner = await createUser({ globalRole: globalOwnerRole });
const owner = await createUser({ role: 'global:owner' });
const authOwnerAgent = testServer.publicApiAgentFor(owner);
await authOwnerAgent.get('/users').expect(401);
});
test('should fail due to invalid API Key', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
owner.apiKey = 'invalid-key';
@ -58,7 +46,7 @@ describe('With license unlimited quota:users', () => {
test('should return all users', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
@ -77,7 +65,7 @@ describe('With license unlimited quota:users', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
createdAt,
@ -91,7 +79,7 @@ describe('With license unlimited quota:users', () => {
expect(personalizationAnswers).toBeUndefined();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole).toBeUndefined();
expect(role).toBeUndefined();
expect(createdAt).toBeDefined();
expect(updatedAt).toBeDefined();
}
@ -100,14 +88,14 @@ describe('With license unlimited quota:users', () => {
describe('GET /users/:id', () => {
test('should fail due to missing API Key', async () => {
const owner = await createUser({ globalRole: globalOwnerRole });
const owner = await createUser({ role: 'global:owner' });
const authOwnerAgent = testServer.publicApiAgentFor(owner);
await authOwnerAgent.get(`/users/${owner.id}`).expect(401);
});
test('should fail due to invalid API Key', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
owner.apiKey = 'invalid-key';
@ -122,7 +110,7 @@ describe('With license unlimited quota:users', () => {
});
test('should return 404 for non-existing id ', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
const authOwnerAgent = testServer.publicApiAgentFor(owner);
@ -131,11 +119,11 @@ describe('With license unlimited quota:users', () => {
test('should return a pending user', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
const { id: memberId } = await createUserShell(globalMemberRole);
const { id: memberId } = await createUserShell('global:member');
const authOwnerAgent = testServer.publicApiAgentFor(owner);
const response = await authOwnerAgent.get(`/users/${memberId}`).expect(200);
@ -146,7 +134,7 @@ describe('With license unlimited quota:users', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
createdAt,
@ -159,7 +147,7 @@ describe('With license unlimited quota:users', () => {
expect(lastName).toBeDefined();
expect(personalizationAnswers).toBeUndefined();
expect(password).toBeUndefined();
expect(globalRole).toBeUndefined();
expect(role).toBeUndefined();
expect(createdAt).toBeDefined();
expect(isPending).toBeDefined();
expect(isPending).toBeTruthy();
@ -170,7 +158,7 @@ describe('With license unlimited quota:users', () => {
describe('GET /users/:email', () => {
test('with non-existing email should return 404', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
const authOwnerAgent = testServer.publicApiAgentFor(owner);
@ -179,7 +167,7 @@ describe('With license unlimited quota:users', () => {
test('should return a user', async () => {
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
@ -192,7 +180,7 @@ describe('With license unlimited quota:users', () => {
firstName,
lastName,
personalizationAnswers,
globalRole,
role,
password,
isPending,
createdAt,
@ -206,7 +194,7 @@ describe('With license unlimited quota:users', () => {
expect(personalizationAnswers).toBeUndefined();
expect(password).toBeUndefined();
expect(isPending).toBe(false);
expect(globalRole).toBeUndefined();
expect(role).toBeUndefined();
expect(createdAt).toBeDefined();
expect(updatedAt).toBeDefined();
});
@ -220,7 +208,7 @@ describe('With license without quota:users', () => {
mockInstance(License, { getUsersLimit: jest.fn().mockReturnValue(null) });
const owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
authOwnerAgent = testServer.publicApiAgentFor(owner);

View file

@ -2,25 +2,22 @@ import type { SuperAgentTest } from 'supertest';
import Container from 'typedi';
import type { INode } from 'n8n-workflow';
import { STARTING_NODES } from '@/constants';
import type { Role } from '@db/entities/Role';
import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { Push } from '@/push';
import { ExecutionService } from '@/executions/execution.service';
import { randomApiKey } from '../shared/random';
import * as utils from '../shared/utils/';
import * as testDb from '../shared/testDb';
import { getAllRoles } from '../shared/db/roles';
import { createUser } from '../shared/db/users';
import { createWorkflow, createWorkflowWithTrigger } from '../shared/db/workflows';
import { createTag } from '../shared/db/tags';
import { mockInstance } from '../../shared/mocking';
import { Push } from '@/push';
import { ExecutionService } from '@/executions/execution.service';
let workflowOwnerRole: Role;
let owner: User;
let member: User;
let authOwnerAgent: SuperAgentTest;
@ -34,17 +31,13 @@ mockInstance(Push);
mockInstance(ExecutionService);
beforeAll(async () => {
const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await getAllRoles();
workflowOwnerRole = fetchedWorkflowOwnerRole;
owner = await createUser({
globalRole: globalOwnerRole,
role: 'global:owner',
apiKey: randomApiKey(),
});
member = await createUser({
globalRole: globalMemberRole,
role: 'global:member',
apiKey: randomApiKey(),
});
@ -693,12 +686,12 @@ describe('POST /workflows', () => {
userId: member.id,
workflowId: response.body.id,
},
relations: ['workflow', 'role'],
relations: ['workflow'],
});
expect(sharedWorkflow?.workflow.name).toBe(name);
expect(sharedWorkflow?.workflow.createdAt.toISOString()).toBe(createdAt);
expect(sharedWorkflow?.role).toEqual(workflowOwnerRole);
expect(sharedWorkflow?.role).toEqual('workflow:owner');
});
test('should create workflow history version when licensed', async () => {
@ -1110,13 +1103,13 @@ describe('PUT /workflows/:id', () => {
userId: member.id,
workflowId: response.body.id,
},
relations: ['workflow', 'role'],
relations: ['workflow'],
});
expect(sharedWorkflow?.workflow.name).toBe(payload.name);
expect(sharedWorkflow?.workflow.updatedAt.getTime()).toBeGreaterThan(
workflow.updatedAt.getTime(),
);
expect(sharedWorkflow?.role).toEqual(workflowOwnerRole);
expect(sharedWorkflow?.role).toEqual('workflow:owner');
});
});

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