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
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({ const workflowData = await this.workflowRepository.findOne({
where: { id: webhook.workflowId }, where: { id: webhook.workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole'], relations: ['shared', 'shared.user'],
}); });
if (workflowData === null) { if (workflowData === null) {

View File

@@ -229,7 +229,7 @@ export class ActiveWorkflowRunner {
async clearWebhooks(workflowId: string) { async clearWebhooks(workflowId: string) {
const workflowData = await this.workflowRepository.findOne({ const workflowData = await this.workflowRepository.findOne({
where: { id: workflowId }, where: { id: workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole'], relations: ['shared', 'shared.user'],
}); });
if (workflowData === null) { 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) { if (!sharing) {
throw new WorkflowActivationError(`Workflow ${dbWorkflow.display()} has no owner`); 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({ const credential = await this.sharedCredentialsRepository.findOne({
where: { where: {
role: { role: 'credential:owner',
scope: 'credential',
name: 'owner',
},
user: { user: {
globalRole: { role: 'global:owner',
scope: 'global',
name: 'owner',
},
}, },
credentials: { credentials: {
id: nodeCredential.id, 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> { export async function init(testConnectionOptions?: ConnectionOptions): Promise<void> {
if (connectionState.connected) return; if (connectionState.connected) return;
@@ -130,13 +140,7 @@ export async function init(testConnectionOptions?: ConnectionOptions): Promise<v
await connection.initialize(); await connection.initialize();
if (dbType === 'postgresdb') { if (dbType === 'postgresdb') {
const schema = config.getEnv('database.postgresdb.schema'); await setSchema(connection);
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(',')};`);
} }
connectionState.connected = true; connectionState.connected = true;

View File

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

View File

@@ -22,11 +22,11 @@ import { Telemetry } from '@/telemetry';
import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { AuthProviderType } from '@db/entities/AuthIdentity';
import { eventBus } from './eventbus'; import { eventBus } from './eventbus';
import { EventsService } from '@/services/events.service'; 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 { N8N_VERSION } from '@/constants';
import { NodeTypes } from './NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata'; 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 type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions'; import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
@@ -36,14 +36,14 @@ function userToPayload(user: User): {
_email: string; _email: string;
_firstName: string; _firstName: string;
_lastName: string; _lastName: string;
globalRole?: string; globalRole: GlobalRole;
} { } {
return { return {
userId: user.id, userId: user.id,
_email: user.email, _email: user.email,
_firstName: user.firstName, _firstName: user.firstName,
_lastName: user.lastName, _lastName: user.lastName,
globalRole: user.globalRole?.name, globalRole: user.role,
}; };
} }
@@ -52,7 +52,7 @@ export class InternalHooks {
constructor( constructor(
private telemetry: Telemetry, private telemetry: Telemetry,
private nodeTypes: NodeTypes, private nodeTypes: NodeTypes,
private roleService: RoleService, private sharedWorkflowRepository: SharedWorkflowRepository,
eventsService: EventsService, eventsService: EventsService,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
) { ) {
@@ -166,9 +166,9 @@ export class InternalHooks {
let userRole: 'owner' | 'sharee' | undefined = undefined; let userRole: 'owner' | 'sharee' | undefined = undefined;
if (user.id && workflow.id) { 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) { 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; let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId) { if (userId) {
const role = await this.roleService.findRoleByUserAndWorkflow(userId, workflow.id); const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
if (role) { 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 { validate } from 'jsonschema';
import * as Db from '@/Db'; import * as Db from '@/Db';
import config from '@/config'; import config from '@/config';
import type { Role } from '@db/entities/Role';
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import { AuthIdentity } from '@db/entities/AuthIdentity'; import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory'; import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
@@ -18,7 +17,6 @@ import {
} from './constants'; } from './constants';
import type { ConnectionSecurity, LdapConfig } from './types'; import type { ConnectionSecurity, LdapConfig } from './types';
import { License } from '@/License'; import { License } from '@/License';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository'; import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
@@ -47,13 +45,6 @@ export const randomPassword = (): string => {
return Math.random().toString(36).slice(-8); 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 * Validate the structure of the LDAP configuration schema
*/ */
@@ -102,7 +93,7 @@ export const getAuthIdentityByLdapId = async (
idAttributeValue: string, idAttributeValue: string,
): Promise<AuthIdentity | null> => { ): Promise<AuthIdentity | null> => {
return await Container.get(AuthIdentityRepository).findOne({ return await Container.get(AuthIdentityRepository).findOne({
relations: ['user', 'user.globalRole'], relations: ['user'],
where: { where: {
providerId: idAttributeValue, providerId: idAttributeValue,
providerType: 'ldap', providerType: 'ldap',
@@ -113,7 +104,6 @@ export const getAuthIdentityByLdapId = async (
export const getUserByEmail = async (email: string): Promise<User | null> => { export const getUserByEmail = async (email: string): Promise<User | null> => {
return await Container.get(UserRepository).findOne({ return await Container.get(UserRepository).findOne({
where: { email }, where: { email },
relations: ['globalRole'],
}); });
}; };
@@ -164,13 +154,13 @@ export const getLdapUsers = async (): Promise<User[]> => {
export const mapLdapUserToDbUser = ( export const mapLdapUserToDbUser = (
ldapUser: LdapUser, ldapUser: LdapUser,
ldapConfig: LdapConfig, ldapConfig: LdapConfig,
role?: Role, toCreate = false,
): [string, User] => { ): [string, User] => {
const user = new User(); const user = new User();
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig); const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
Object.assign(user, data); Object.assign(user, data);
if (role) { if (toCreate) {
user.globalRole = role; user.role = 'global:member';
user.password = randomPassword(); user.password = randomPassword();
user.disabled = false; user.disabled = false;
} else { } else {
@@ -270,10 +260,10 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
return await Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId)); 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({ const user = await Container.get(UserRepository).save({
password: randomPassword(), password: randomPassword(),
globalRole: role, role: 'global:member',
...data, ...data,
}); });
await createLdapAuthIdentity(user, ldapId); await createLdapAuthIdentity(user, ldapId);

View File

@@ -7,7 +7,6 @@ import { ApplicationError, jsonParse } from 'n8n-workflow';
import { Cipher } from 'n8n-core'; import { Cipher } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory'; import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
@@ -30,7 +29,6 @@ import {
escapeFilter, escapeFilter,
formatUrl, formatUrl,
getLdapIds, getLdapIds,
getLdapUserRole,
getLdapUsers, getLdapUsers,
getMappingAttributes, getMappingAttributes,
mapLdapUserToDbUser, mapLdapUserToDbUser,
@@ -346,12 +344,9 @@ export class LdapService {
const localAdUsers = await getLdapIds(); const localAdUsers = await getLdapIds();
const role = await getLdapUserRole();
const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess( const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess(
adUsers, adUsers,
localAdUsers, localAdUsers,
role,
); );
this.logger.debug('LDAP - Users processed', { this.logger.debug('LDAP - Users processed', {
@@ -407,14 +402,13 @@ export class LdapService {
private getUsersToProcess( private getUsersToProcess(
adUsers: LdapUser[], adUsers: LdapUser[],
localAdUsers: string[], localAdUsers: string[],
role: Role,
): { ): {
usersToCreate: Array<[string, User]>; usersToCreate: Array<[string, User]>;
usersToUpdate: Array<[string, User]>; usersToUpdate: Array<[string, User]>;
usersToDisable: string[]; usersToDisable: string[];
} { } {
return { return {
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers, role), usersToCreate: this.getUsersToCreate(adUsers, localAdUsers),
usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers), usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers),
usersToDisable: this.getUsersToDisable(adUsers, localAdUsers), usersToDisable: this.getUsersToDisable(adUsers, localAdUsers),
}; };
@@ -424,11 +418,10 @@ export class LdapService {
private getUsersToCreate( private getUsersToCreate(
remoteAdUsers: LdapUser[], remoteAdUsers: LdapUser[],
localLdapIds: string[], localLdapIds: string[],
role: Role,
): Array<[string, User]> { ): Array<[string, User]> {
return remoteAdUsers return remoteAdUsers
.filter((adUser) => !localLdapIds.includes(adUser[this.config.ldapIdAttribute] as string)) .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 */ /** 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 apiKey = req.headers[schema.name.toLowerCase()] as string;
const user = await Container.get(UserRepository).findOne({ const user = await Container.get(UserRepository).findOne({
where: { apiKey }, where: { apiKey },
relations: ['globalRole'],
}); });
if (!user) return false; 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 { User } from '@db/entities/User';
import type { Role } from '@db/entities/Role';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { UserManagementMailer } from '@/UserManagement/email'; import type { UserManagementMailer } from '@/UserManagement/email';
@@ -25,7 +23,6 @@ export type AuthenticatedRequest<
RequestQuery = {}, RequestQuery = {},
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & { > = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
user: User; user: User;
globalMemberRole?: Role;
mailer?: UserManagementMailer; mailer?: UserManagementMailer;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ import { InternalHooks } from '@/InternalHooks';
export = { export = {
getUser: [ getUser: [
validLicenseWithUserQuota, validLicenseWithUserQuota,
authorize(['owner', 'admin']), authorize(['global:owner', 'global:admin']),
async (req: UserRequest.Get, res: express.Response) => { async (req: UserRequest.Get, res: express.Response) => {
const { includeRole = false } = req.query; const { includeRole = false } = req.query;
const { id } = req.params; const { id } = req.params;
@@ -41,7 +41,7 @@ export = {
getUsers: [ getUsers: [
validLicenseWithUserQuota, validLicenseWithUserQuota,
validCursor, validCursor,
authorize(['owner', 'admin']), authorize(['global:owner', 'global:admin']),
async (req: UserRequest.Get, res: express.Response) => { async (req: UserRequest.Get, res: express.Response) => {
const { offset = 0, limit = 100, includeRole = false } = req.query; 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 pick from 'lodash/pick';
import { validate as uuidValidate } from 'uuid'; 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: { export async function getUser(data: {
withIdentifier: string; withIdentifier: string;
includeRole?: boolean; includeRole?: boolean;
}): Promise<User | null> { }): Promise<User | null> {
return await Container.get(UserRepository).findOne({ return await Container.get(UserRepository)
where: { .findOne({
...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }), where: {
...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }), ...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }),
}, ...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }),
relations: data?.includeRole ? ['globalRole'] : undefined, },
}); })
.then((user) => {
if (user && !data?.includeRole) delete (user as Partial<User>).role;
return user;
});
} }
export async function getAllUsersAndCount(data: { export async function getAllUsersAndCount(data: {
@@ -31,19 +28,29 @@ export async function getAllUsersAndCount(data: {
}): Promise<[User[], number]> { }): Promise<[User[], number]> {
const users = await Container.get(UserRepository).find({ const users = await Container.get(UserRepository).find({
where: {}, where: {},
relations: data?.includeRole ? ['globalRole'] : undefined,
skip: data.offset, skip: data.offset,
take: data.limit, take: data.limit,
}); });
if (!data?.includeRole) {
users.forEach((user) => {
delete (user as Partial<User>).role;
});
}
const count = await Container.get(UserRepository).count(); const count = await Container.get(UserRepository).count();
return [users, count]; return [users, count];
} }
const userProperties = [
'id',
'email',
'firstName',
'lastName',
'createdAt',
'updatedAt',
'isPending',
];
function pickUserSelectableProperties(user: User, options?: { includeRole: boolean }) { function pickUserSelectableProperties(user: User, options?: { includeRole: boolean }) {
return pick( return pick(user, userProperties.concat(options?.includeRole ? ['role'] : []));
user,
getSelectableProperties('user').concat(options?.includeRole ? ['globalRole'] : []),
);
} }
export function clean(user: User, options?: { includeRole: boolean }): Partial<User>; export function clean(user: User, options?: { includeRole: boolean }): Partial<User>;

View File

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

View File

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

View File

@@ -6,20 +6,18 @@ import { Container } from 'typedi';
import type { AuthenticatedRequest, PaginatedRequest } from '../../../types'; import type { AuthenticatedRequest, PaginatedRequest } from '../../../types';
import { decodeCursor } from '../services/pagination.service'; import { decodeCursor } from '../services/pagination.service';
import { License } from '@/License'; import { License } from '@/License';
import type { RoleNames } from '@/databases/entities/Role'; import type { GlobalRole } from '@db/entities/User';
const UNLIMITED_USERS_QUOTA = -1; const UNLIMITED_USERS_QUOTA = -1;
export const authorize = export const authorize =
(authorizedRoles: readonly RoleNames[]) => (authorizedRoles: readonly GlobalRole[]) =>
( (
req: AuthenticatedRequest, req: AuthenticatedRequest,
res: express.Response, res: express.Response,
next: express.NextFunction, next: express.NextFunction,
): express.Response | void => { ): express.Response | void => {
const { name } = req.user.globalRole; if (!authorizedRoles.includes(req.user.role)) {
if (!authorizedRoles.includes(name)) {
return res.status(403).json({ message: 'Forbidden' }); 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 { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
import { InvitationController } from './controllers/invitation.controller'; import { InvitationController } from './controllers/invitation.controller';
import { CollaborationService } from './collaboration/collaboration.service'; import { CollaborationService } from './collaboration/collaboration.service';
import { RoleController } from './controllers/role.controller';
import { BadRequestError } from './errors/response-errors/bad-request.error'; import { BadRequestError } from './errors/response-errors/bad-request.error';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
@@ -228,7 +227,6 @@ export class Server extends AbstractServer {
VariablesController, VariablesController,
InvitationController, InvitationController,
VariablesController, VariablesController,
RoleController,
ActiveWorkflowsController, ActiveWorkflowsController,
WorkflowsController, WorkflowsController,
ExecutionsController, ExecutionsController,

View File

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

View File

@@ -12,7 +12,7 @@ export const handleEmailLogin = async (
): Promise<User | undefined> => { ): Promise<User | undefined> => {
const user = await Container.get(UserRepository).findOne({ const user = await Container.get(UserRepository).findOne({
where: { email }, where: { email },
relations: ['globalRole', 'authIdentities'], relations: ['authIdentities'],
}); });
if (user?.password && (await Container.get(PasswordUtility).compare(password, user.password))) { 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 { LdapService } from '@/Ldap/ldap.service';
import { import {
createLdapUserOnLocalDb, createLdapUserOnLocalDb,
getLdapUserRole,
getUserByEmail, getUserByEmail,
getAuthIdentityByLdapId, getAuthIdentityByLdapId,
isLdapEnabled, isLdapEnabled,
@@ -50,8 +49,7 @@ export const handleLdapLogin = async (
const identity = await createLdapAuthIdentity(emailUser, ldapId); const identity = await createLdapAuthIdentity(emailUser, ldapId);
await updateLdapUserOnLocalDb(identity, ldapAttributesValues); await updateLdapUserOnLocalDb(identity, ldapAttributesValues);
} else { } else {
const role = await getLdapUserRole(); const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId);
const user = await createLdapUserOnLocalDb(role, ldapAttributesValues, ldapId);
void Container.get(InternalHooks).onUserSignup(user, { void Container.get(InternalHooks).onUserSignup(user, {
user_type: 'ldap', user_type: 'ldap',
was_disabled_ldap_user: false, 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 { DataSource as Connection } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { getConnectionOptions } from '@/Db'; import { getConnectionOptions, setSchema } from '@/Db';
import type { Migration } from '@db/types'; import type { Migration } from '@db/types';
import { wrapMigration } from '@db/utils/migrationHelpers'; import { wrapMigration } from '@db/utils/migrationHelpers';
import config from '@/config'; import config from '@/config';
@@ -40,6 +40,7 @@ export class DbRevertMigrationCommand extends Command {
this.connection = new Connection(connectionOptions); this.connection = new Connection(connectionOptions);
await this.connection.initialize(); await this.connection.initialize();
if (dbType === 'postgresdb') await setSchema(this.connection);
await this.connection.undoLastMigration(); await this.connection.undoLastMigration();
await this.connection.destroy(); await this.connection.destroy();
} }

View File

@@ -8,13 +8,11 @@ import type { EntityManager } from 'typeorm';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedCredentials } from '@db/entities/SharedCredentials';
import type { Role } from '@db/entities/Role';
import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers'; import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
import type { ICredentialsEncrypted } from 'n8n-workflow'; import type { ICredentialsEncrypted } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import { UM_FIX_INSTRUCTION } from '@/constants'; import { UM_FIX_INSTRUCTION } from '@/constants';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
@@ -42,8 +40,6 @@ export class ImportCredentialsCommand extends BaseCommand {
}), }),
}; };
private ownerCredentialRole: Role;
private transactionManager: EntityManager; private transactionManager: EntityManager;
async init() { async init() {
@@ -71,7 +67,6 @@ export class ImportCredentialsCommand extends BaseCommand {
let totalImported = 0; let totalImported = 0;
const cipher = Container.get(Cipher); const cipher = Container.get(Cipher);
await this.initOwnerCredentialRole();
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
if (flags.separate) { 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) { private async storeCredential(credential: Partial<CredentialsEntity>, user: User) {
if (!credential.nodesAccess) { if (!credential.nodesAccess) {
credential.nodesAccess = []; credential.nodesAccess = [];
@@ -165,19 +150,14 @@ export class ImportCredentialsCommand extends BaseCommand {
{ {
credentialsId: result.identifiers[0].id as string, credentialsId: result.identifiers[0].id as string,
userId: user.id, userId: user.id,
roleId: this.ownerCredentialRole.id, role: 'credential:owner',
}, },
['credentialsId', 'userId'], ['credentialsId', 'userId'],
); );
} }
private async getOwner() { private async getOwner() {
const ownerGlobalRole = await Container.get(RoleService).findGlobalOwnerRole(); const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
const owner =
ownerGlobalRole &&
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole.id }));
if (!owner) { if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); 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 { UserRepository } from '@db/repositories/user.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { IWorkflowToImport } from '@/Interfaces'; import type { IWorkflowToImport } from '@/Interfaces';
import { RoleService } from '@/services/role.service';
import { ImportService } from '@/services/import.service'; import { ImportService } from '@/services/import.service';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
@@ -138,12 +137,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
} }
private async getOwner() { private async getOwner() {
const ownerGlobalRole = await Container.get(RoleService).findGlobalOwnerRole(); const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
const owner =
ownerGlobalRole &&
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole?.id }));
if (!owner) { if (!owner) {
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); 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 { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { RoleService } from '@/services/role.service';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
const defaultUserProps = { const defaultUserProps = {
@@ -14,6 +13,7 @@ const defaultUserProps = {
lastName: null, lastName: null,
email: null, email: null,
password: null, password: null,
role: 'global:owner',
}; };
export class Reset extends BaseCommand { export class Reset extends BaseCommand {
@@ -24,14 +24,8 @@ export class Reset extends BaseCommand {
async run(): Promise<void> { async run(): Promise<void> {
const owner = await this.getInstanceOwner(); const owner = await this.getInstanceOwner();
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole(); await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner);
const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole(); await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(owner);
await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner, workflowOwnerRole);
await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(
owner,
credentialOwnerRole,
);
await Container.get(UserRepository).deleteAllExcept(owner); await Container.get(UserRepository).deleteAllExcept(owner);
await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps)); await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps));
@@ -45,7 +39,7 @@ export class Reset extends BaseCommand {
Container.get(SharedCredentialsRepository).create({ Container.get(SharedCredentialsRepository).create({
credentials, credentials,
user: owner, user: owner,
role: credentialOwnerRole, role: 'credential:owner',
}), }),
); );
await Container.get(SharedCredentialsRepository).save(newSharedCredentials); await Container.get(SharedCredentialsRepository).save(newSharedCredentials);
@@ -59,19 +53,17 @@ export class Reset extends BaseCommand {
} }
async getInstanceOwner(): Promise<User> { async getInstanceOwner(): Promise<User> {
const globalRole = await Container.get(RoleService).findGlobalOwnerRole(); const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
const owner = await Container.get(UserRepository).findOneBy({ globalRoleId: globalRole.id });
if (owner) return owner; if (owner) return owner;
const user = new User(); const user = new User();
Object.assign(user, { ...defaultUserProps, globalRole }); Object.assign(user, defaultUserProps);
await Container.get(UserRepository).save(user); 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> { async catch(error: Error): Promise<void> {

View File

@@ -55,7 +55,7 @@ export class AuthController {
const preliminaryUser = await handleEmailLogin(email, password); const preliminaryUser = await handleEmailLogin(email, password);
// if the user is an owner, continue with the login // if the user is an owner, continue with the login
if ( if (
preliminaryUser?.globalRole?.name === 'owner' || preliminaryUser?.role === 'global:owner' ||
preliminaryUser?.settings?.allowSSOManualLogin preliminaryUser?.settings?.allowSSOManualLogin
) { ) {
user = preliminaryUser; user = preliminaryUser;
@@ -65,7 +65,7 @@ export class AuthController {
} }
} else if (isLdapCurrentAuthenticationMethod()) { } else if (isLdapCurrentAuthenticationMethod()) {
const preliminaryUser = await handleEmailLogin(email, password); const preliminaryUser = await handleEmailLogin(email, password);
if (preliminaryUser?.globalRole?.name === 'owner') { if (preliminaryUser?.role === 'global:owner') {
user = preliminaryUser; user = preliminaryUser;
usedAuthenticationMethod = 'email'; usedAuthenticationMethod = 'email';
} else { } else {
@@ -138,7 +138,7 @@ export class AuthController {
} }
try { try {
user = await this.userRepository.findOneOrFail({ where: {}, relations: ['globalRole'] }); user = await this.userRepository.findOneOrFail({ where: {} });
} catch (error) { } catch (error) {
throw new InternalServerError( throw new InternalServerError(
'No users found in database - did you wipe the users table? Create at least one user.', '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 { Request } from 'express';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import config from '@/config'; 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 { SettingsRepository } from '@db/repositories/settings.repository';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
@@ -39,7 +37,6 @@ const tablesToTruncate = [
'installed_packages', 'installed_packages',
'installed_nodes', 'installed_nodes',
'user', 'user',
'role',
'variables', 'variables',
]; ];
@@ -87,7 +84,6 @@ export class E2EController {
constructor( constructor(
license: License, license: License,
private readonly roleRepo: RoleRepository,
private readonly settingsRepo: SettingsRepository, private readonly settingsRepo: SettingsRepository,
private readonly userRepo: UserRepository, private readonly userRepo: UserRepository,
private readonly workflowRunner: ActiveWorkflowRunner, private readonly workflowRunner: ActiveWorkflowRunner,
@@ -148,7 +144,7 @@ export class E2EController {
private async truncateAll() { private async truncateAll() {
for (const table of tablesToTruncate) { for (const table of tablesToTruncate) {
try { try {
const { connection } = this.roleRepo.manager; const { connection } = this.settingsRepo.manager;
await connection.query( await connection.query(
`DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`,
); );
@@ -163,27 +159,12 @@ export class E2EController {
members: UserSetupPayload[], members: UserSetupPayload[],
admin: UserSetupPayload, admin: UserSetupPayload,
) { ) {
const roles: Array<[Role['name'], Role['scope']]> = [ const instanceOwner = this.userRepo.create({
['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 = {
id: uuid(), id: uuid(),
...owner, ...owner,
password: await this.passwordUtility.hash(owner.password), password: await this.passwordUtility.hash(owner.password),
globalRoleId: globalOwnerRoleId, role: 'global:owner',
}; });
if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) { if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) {
const { encryptedRecoveryCodes, encryptedSecret } = const { encryptedRecoveryCodes, encryptedSecret } =
@@ -192,12 +173,12 @@ export class E2EController {
instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes; instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes;
} }
const adminUser = { const adminUser = this.userRepo.create({
id: uuid(), id: uuid(),
...admin, ...admin,
password: await this.passwordUtility.hash(admin.password), password: await this.passwordUtility.hash(admin.password),
globalRoleId: globalAdminRoleId, role: 'global:admin',
}; });
const users = []; const users = [];
@@ -209,7 +190,7 @@ export class E2EController {
id: uuid(), id: uuid(),
...payload, ...payload,
password: await this.passwordUtility.hash(password), password: await this.passwordUtility.hash(password),
globalRoleId: globalMemberRoleId, role: 'global:member',
}), }),
); );
} }

View File

@@ -1,4 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import validator from 'validator';
import config from '@/config'; import config from '@/config';
import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators'; 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 { PasswordUtility } from '@/services/password.utility';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import type { User } from '@/databases/entities/User'; 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 { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized() @Authorized()
@RestController('/invitations') @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( 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( throw new UnauthorizedError(
'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.', '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 }) => ({ const attributes = req.body.map(({ email, role }) => ({
email, email,
role: role ?? 'member', role: role ?? 'global:member',
})); }));
const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes); 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); await this.userService.update(userId, payload);
const user = await this.userRepository.findOneOrFail({ const user = await this.userRepository.findOneOrFail({
where: { id: userId }, where: { id: userId },
relations: ['globalRole'],
}); });
this.logger.info('User updated successfully', { userId }); this.logger.info('User updated successfully', { userId });
@@ -235,7 +234,6 @@ export class MeController {
const user = await this.userRepository.findOneOrFail({ const user = await this.userRepository.findOneOrFail({
select: ['settings'], select: ['settings'],
where: { id }, where: { id },
relations: ['globalRole'],
}); });
return user.settings; return user.settings;

View File

@@ -15,7 +15,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized(['global', 'owner']) @Authorized('global:owner')
@RestController('/owner') @RestController('/owner')
export class OwnerController { export class OwnerController {
constructor( constructor(
@@ -35,7 +35,7 @@ export class OwnerController {
@Post('/setup') @Post('/setup')
async setupOwner(req: OwnerRequest.Post, res: Response) { async setupOwner(req: OwnerRequest.Post, res: Response) {
const { email, firstName, lastName, password } = req.body; const { email, firstName, lastName, password } = req.body;
const { id: userId, globalRole } = req.user; const { id: userId } = req.user;
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) { if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug( this.logger.debug(
@@ -65,17 +65,6 @@ export class OwnerController {
throw new BadRequestError('First and last names are mandatory'); 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; let owner = req.user;
Object.assign(owner, { 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 { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { RoleService } from '@/services/role.service';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
import { listQueryMiddleware } from '@/middlewares'; import { listQueryMiddleware } from '@/middlewares';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
@@ -45,7 +44,6 @@ export class UsersController {
private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
private readonly activeWorkflowRunner: ActiveWorkflowRunner, private readonly activeWorkflowRunner: ActiveWorkflowRunner,
private readonly roleService: RoleService,
private readonly userService: UserService, private readonly userService: UserService,
) {} ) {}
@@ -70,7 +68,7 @@ export class UsersController {
} }
if (filter?.isOwner) { if (filter?.isOwner) {
for (const user of publicUsers) delete user.globalRole; for (const user of publicUsers) delete user.role;
} }
// remove computed fields (unselectable) // remove computed fields (unselectable)
@@ -92,12 +90,7 @@ export class UsersController {
async listUsers(req: ListQuery.Request) { async listUsers(req: ListQuery.Request) {
const { listQueryOptions } = req; const { listQueryOptions } = req;
const globalOwner = await this.roleService.findGlobalOwnerRole(); const findManyOptions = await this.userRepository.toFindManyOptions(listQueryOptions);
const findManyOptions = await this.userRepository.toFindManyOptions(
listQueryOptions,
globalOwner.id,
);
const users = await this.userRepository.find(findManyOptions); const users = await this.userRepository.find(findManyOptions);
@@ -118,7 +111,6 @@ export class UsersController {
async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) { async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) {
const user = await this.userRepository.findOneOrFail({ const user = await this.userRepository.findOneOrFail({
where: { id: req.params.id }, where: { id: req.params.id },
relations: ['globalRole'],
}); });
if (!user) { if (!user) {
throw new NotFoundError('User not found'); throw new NotFoundError('User not found');
@@ -140,7 +132,6 @@ export class UsersController {
const user = await this.userRepository.findOneOrFail({ const user = await this.userRepository.findOneOrFail({
select: ['settings'], select: ['settings'],
where: { id }, where: { id },
relations: ['globalRole'],
}); });
return user.settings; return user.settings;
@@ -194,11 +185,6 @@ export class UsersController {
telemetryData.migration_user_id = transferId; telemetryData.migration_user_id = transferId;
} }
const [workflowOwnerRole, credentialOwnerRole] = await Promise.all([
this.roleService.findWorkflowOwnerRole(),
this.roleService.findCredentialOwnerRole(),
]);
if (transferId) { if (transferId) {
const transferee = users.find((user) => user.id === transferId); const transferee = users.find((user) => user.id === transferId);
@@ -208,7 +194,7 @@ export class UsersController {
.getRepository(SharedWorkflow) .getRepository(SharedWorkflow)
.find({ .find({
select: ['workflowId'], select: ['workflowId'],
where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id }, where: { userId: userToDelete.id, role: 'workflow:owner' },
}) })
.then((sharedWorkflows) => sharedWorkflows.map(({ workflowId }) => workflowId)); .then((sharedWorkflows) => sharedWorkflows.map(({ workflowId }) => workflowId));
@@ -223,7 +209,7 @@ export class UsersController {
// Transfer ownership of owned workflows // Transfer ownership of owned workflows
await transactionManager.update( await transactionManager.update(
SharedWorkflow, SharedWorkflow,
{ user: userToDelete, role: workflowOwnerRole }, { user: userToDelete, role: 'workflow:owner' },
{ user: transferee }, { user: transferee },
); );
@@ -234,7 +220,7 @@ export class UsersController {
.getRepository(SharedCredentials) .getRepository(SharedCredentials)
.find({ .find({
select: ['credentialsId'], select: ['credentialsId'],
where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id }, where: { userId: userToDelete.id, role: 'credential:owner' },
}) })
.then((sharedCredentials) => sharedCredentials.map(({ credentialsId }) => credentialsId)); .then((sharedCredentials) => sharedCredentials.map(({ credentialsId }) => credentialsId));
@@ -249,7 +235,7 @@ export class UsersController {
// Transfer ownership of owned credentials // Transfer ownership of owned credentials
await transactionManager.update( await transactionManager.update(
SharedCredentials, SharedCredentials,
{ user: userToDelete, role: credentialOwnerRole }, { user: userToDelete, role: 'credential:owner' },
{ user: transferee }, { user: transferee },
); );
@@ -271,11 +257,11 @@ export class UsersController {
const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([ const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([
this.sharedWorkflowRepository.find({ this.sharedWorkflowRepository.find({
relations: ['workflow'], relations: ['workflow'],
where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id }, where: { userId: userToDelete.id, role: 'workflow:owner' },
}), }),
this.sharedCredentialsRepository.find({ this.sharedCredentialsRepository.find({
relations: ['credentials'], 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({ const targetUser = await this.userRepository.findOne({
where: { id: req.params.id }, where: { id: req.params.id },
relations: ['globalRole'],
}); });
if (targetUser === null) { if (targetUser === null) {
throw new NotFoundError(NO_USER); 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); 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); throw new UnauthorizedError(NO_OWNER_ON_OWNER);
} }
const roleToSet = await this.roleService.findCached('global', payload.newRoleName); await this.userService.update(targetUser.id, { role: payload.newRoleName });
await this.userService.update(targetUser.id, { globalRoleId: roleToSet.id });
void this.internalHooks.onUserRoleChange({ void this.internalHooks.onUserRoleChange({
user: req.user, user: req.user,

View File

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

View File

@@ -147,7 +147,7 @@ credentialsController.patch(
allowGlobalScope: true, allowGlobalScope: true,
globalScope: 'credential:update', globalScope: 'credential:update',
}, },
['credentials', 'role'], ['credentials'],
); );
if (!sharing) { 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( Container.get(Logger).info(
'Attempt to update credential blocked due to lack of permissions', 'Attempt to update credential blocked due to lack of permissions',
{ {
@@ -216,7 +216,7 @@ credentialsController.delete(
allowGlobalScope: true, allowGlobalScope: true,
globalScope: 'credential:delete', globalScope: 'credential:delete',
}, },
['credentials', 'role'], ['credentials'],
); );
if (!sharing) { 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( Container.get(Logger).info(
'Attempt to delete credential blocked due to lack of permissions', '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 type { EntityManager, FindOptionsWhere } from 'typeorm';
import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { SharedCredentials } from '@db/entities/SharedCredentials'; import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { CredentialsService, type CredentialsGetSharedOptions } from './credentials.service'; 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 { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
@@ -15,10 +14,9 @@ export class EECredentialsService extends CredentialsService {
): Promise<{ ownsCredential: boolean; credential?: CredentialsEntity }> { ): Promise<{ ownsCredential: boolean; credential?: CredentialsEntity }> {
const sharing = await this.getSharing(user, credentialId, { allowGlobalScope: false }, [ const sharing = await this.getSharing(user, credentialId, { allowGlobalScope: false }, [
'credentials', 'credentials',
'role',
]); ]);
if (!sharing || sharing.role.name !== 'owner') return { ownsCredential: false }; if (!sharing || sharing.role !== 'credential:owner') return { ownsCredential: false };
const { credentials: credential } = sharing; const { credentials: credential } = sharing;
@@ -67,7 +65,6 @@ export class EECredentialsService extends CredentialsService {
shareWithIds: string[], shareWithIds: string[],
): Promise<SharedCredentials[]> { ): Promise<SharedCredentials[]> {
const users = await Container.get(UserRepository).getByIds(transaction, shareWithIds); const users = await Container.get(UserRepository).getByIds(transaction, shareWithIds);
const role = await Container.get(RoleService).findCredentialUserRole();
const newSharedCredentials = users const newSharedCredentials = users
.filter((user) => !user.isPending) .filter((user) => !user.isPending)
@@ -75,7 +72,7 @@ export class EECredentialsService extends CredentialsService {
Container.get(SharedCredentialsRepository).create({ Container.get(SharedCredentialsRepository).create({
credentialsId: credential.id, credentialsId: credential.id,
userId: user.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 { User } from '@db/entities/User';
import type { CredentialRequest, ListQuery } from '@/requests'; import type { CredentialRequest, ListQuery } from '@/requests';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { RoleService } from '@/services/role.service';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
@@ -85,13 +84,8 @@ export class CredentialsService {
// global credential permissions. This allows the user to // global credential permissions. This allows the user to
// access credentials they don't own. // access credentials they don't own.
if (!options.allowGlobalScope || !user.hasGlobalScope(options.globalScope)) { if (!options.allowGlobalScope || !user.hasGlobalScope(options.globalScope)) {
Object.assign(where, { where.userId = user.id;
userId: user.id, where.role = 'credential:owner';
role: { name: 'owner' },
});
if (!relations.includes('role')) {
relations.push('role');
}
} }
return await Container.get(SharedCredentialsRepository).findOne({ where, relations }); return await Container.get(SharedCredentialsRepository).findOne({ where, relations });
@@ -194,8 +188,6 @@ export class CredentialsService {
await Container.get(ExternalHooks).run('credentials.create', [encryptedData]); await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
const role = await Container.get(RoleService).findCredentialOwnerRole();
const result = await Db.transaction(async (transactionManager) => { const result = await Db.transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(newCredential); const savedCredential = await transactionManager.save<CredentialsEntity>(newCredential);
@@ -204,7 +196,7 @@ export class CredentialsService {
const newSharedCredential = new SharedCredentials(); const newSharedCredential = new SharedCredentials();
Object.assign(newSharedCredential, { Object.assign(newSharedCredential, {
role, role: 'credential:owner',
user, user,
credentials: savedCredential, credentials: savedCredential,
}); });

View File

@@ -1,5 +1,5 @@
import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typeorm'; import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typeorm';
import { Table, TableColumn } from 'typeorm'; import { Table, TableColumn, TableForeignKey } from 'typeorm';
import LazyPromise from 'p-lazy'; import LazyPromise from 'p-lazy';
import { Column } from './Column'; import { Column } from './Column';
import { ApplicationError } from 'n8n-workflow'; 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 { class ModifyNotNull extends TableOperation {
constructor( constructor(
tableName: string, tableName: string,

View File

@@ -1,6 +1,15 @@
import type { QueryRunner } from 'typeorm'; import type { QueryRunner } from 'typeorm';
import { Column } from './Column'; 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'; import { CreateIndex, DropIndex } from './Indices';
export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunner) => ({ 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) => dropIndex: (tableName: string, columnNames: string[], customIndexName?: string) =>
new DropIndex(tableName, columnNames, tablePrefix, queryRunner, customIndexName), 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) => addNotNull: (tableName: string, columnName: string) =>
new AddNotNull(tableName, columnName, tablePrefix, queryRunner), new AddNotNull(tableName, columnName, tablePrefix, queryRunner),
dropNotNull: (tableName: string, columnName: string) => 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 { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { CredentialsEntity } from './CredentialsEntity'; import { CredentialsEntity } from './CredentialsEntity';
import { User } from './User'; import { User } from './User';
import { Role } from './Role';
import { WithTimestamps } from './AbstractEntity'; import { WithTimestamps } from './AbstractEntity';
export type CredentialSharingRole = 'credential:owner' | 'credential:user';
@Entity() @Entity()
export class SharedCredentials extends WithTimestamps { export class SharedCredentials extends WithTimestamps {
@ManyToOne('Role', 'sharedCredentials', { nullable: false })
role: Role;
@Column() @Column()
roleId: string; role: CredentialSharingRole;
@ManyToOne('User', 'sharedCredentials') @ManyToOne('User', 'sharedCredentials')
user: User; user: User;

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import { EventDestinations } from './EventDestinations';
import { ExecutionEntity } from './ExecutionEntity'; import { ExecutionEntity } from './ExecutionEntity';
import { InstalledNodes } from './InstalledNodes'; import { InstalledNodes } from './InstalledNodes';
import { InstalledPackages } from './InstalledPackages'; import { InstalledPackages } from './InstalledPackages';
import { Role } from './Role';
import { Settings } from './Settings'; import { Settings } from './Settings';
import { SharedCredentials } from './SharedCredentials'; import { SharedCredentials } from './SharedCredentials';
import { SharedWorkflow } from './SharedWorkflow'; import { SharedWorkflow } from './SharedWorkflow';
@@ -29,7 +28,6 @@ export const entities = {
ExecutionEntity, ExecutionEntity,
InstalledNodes, InstalledNodes,
InstalledPackages, InstalledPackages,
Role,
Settings, Settings,
SharedCredentials, SharedCredentials,
SharedWorkflow, 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 { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata';
import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections'; import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections';
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole'; import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
export const mysqlMigrations: Migration[] = [ export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238, InitialMigration1588157391238,
@@ -105,4 +106,5 @@ export const mysqlMigrations: Migration[] = [
AddWorkflowMetadata1695128658538, AddWorkflowMetadata1695128658538,
ModifyWorkflowHistoryNodesAndConnections1695829275184, ModifyWorkflowHistoryNodesAndConnections1695829275184,
AddGlobalAdminRole1700571993961, AddGlobalAdminRole1700571993961,
DropRoleMapping1705429061930,
]; ];

View File

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

View File

@@ -45,7 +45,7 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
type Select = Array<keyof 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']; const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations }; 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) } }; const findManyOptions: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };
if (withSharings) { if (withSharings) {
findManyOptions.relations = ['shared', 'shared.user', 'shared.role']; findManyOptions.relations = ['shared', 'shared.user'];
} }
return await this.find(findManyOptions); 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 { Service } from 'typedi';
import type { EntityManager, FindOptionsWhere } from 'typeorm'; import type { EntityManager } from 'typeorm';
import { DataSource, In, Not, Repository } from 'typeorm'; import { DataSource, In, Not, Repository } from 'typeorm';
import { SharedCredentials } from '../entities/SharedCredentials'; import { SharedCredentials } from '../entities/SharedCredentials';
import type { User } from '../entities/User'; import type { User } from '../entities/User';
import type { Role } from '../entities/Role';
@Service() @Service()
export class SharedCredentialsRepository extends Repository<SharedCredentials> { export class SharedCredentialsRepository extends Repository<SharedCredentials> {
@@ -26,15 +25,15 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
async findByCredentialIds(credentialIds: string[]) { async findByCredentialIds(credentialIds: string[]) {
return await this.find({ return await this.find({
relations: ['credentials', 'role', 'user'], relations: ['credentials', 'user'],
where: { where: {
credentialsId: In(credentialIds), credentialsId: In(credentialIds),
}, },
}); });
} }
async makeOwnerOfAllCredentials(user: User, role: Role) { async makeOwnerOfAllCredentials(user: User) {
return await this.update({ userId: Not(user.id), roleId: role.id }, { 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) { async getAccessibleCredentials(userId: string) {
const sharings = await this.find({ const sharings = await this.find({
relations: ['role'],
where: { where: {
userId, userId,
role: { name: In(['owner', 'user']), scope: 'credential' }, role: In(['credential:owner', 'credential:user']),
}, },
}); });
return sharings.map((s) => s.credentialsId); return sharings.map((s) => s.credentialsId);
} }
async findSharings(userIds: string[], roleId?: string) { async findOwnedSharings(userIds: string[]) {
const where: FindOptionsWhere<SharedCredentials> = { userId: In(userIds) }; return await this.find({
where: {
// If credential sharing is not enabled, get only credentials owned by this user userId: In(userIds),
if (roleId) where.roleId = roleId; role: 'credential:owner',
},
return await this.find({ where }); });
} }
async deleteByIds(transaction: EntityManager, sharedCredentialsIds: string[], user?: User) { async deleteByIds(transaction: EntityManager, sharedCredentialsIds: string[], user?: User) {

View File

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

View File

@@ -1,9 +1,9 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { EntityManager, FindManyOptions } from 'typeorm'; import type { EntityManager, FindManyOptions } from 'typeorm';
import { DataSource, In, IsNull, Not, Repository } from 'typeorm'; import { DataSource, In, IsNull, Not, Repository } from 'typeorm';
import { User } from '../entities/User';
import type { ListQuery } from '@/requests'; import type { ListQuery } from '@/requests';
import { type GlobalRole, User } from '../entities/User';
@Service() @Service()
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
constructor(dataSource: DataSource) { constructor(dataSource: DataSource) {
@@ -13,7 +13,6 @@ export class UserRepository extends Repository<User> {
async findManyByIds(userIds: string[]) { async findManyByIds(userIds: string[]) {
return await this.find({ return await this.find({
where: { id: In(userIds) }, where: { id: In(userIds) },
relations: ['globalRole'],
}); });
} }
@@ -28,7 +27,6 @@ export class UserRepository extends Repository<User> {
async findManyByEmail(emails: string[]) { async findManyByEmail(emails: string[]) {
return await this.find({ return await this.find({
where: { email: In(emails) }, where: { email: In(emails) },
relations: ['globalRole'],
select: ['email', 'password', 'id'], select: ['email', 'password', 'id'],
}); });
} }
@@ -43,15 +41,30 @@ export class UserRepository extends Repository<User> {
email, email,
password: Not(IsNull()), 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> = {}; const findManyOptions: FindManyOptions<User> = {};
if (!listQueryOptions) { if (!listQueryOptions) {
findManyOptions.relations = ['globalRole', 'authIdentities']; findManyOptions.relations = ['authIdentities'];
return findManyOptions; return findManyOptions;
} }
@@ -62,7 +75,7 @@ export class UserRepository extends Repository<User> {
if (skip) findManyOptions.skip = skip; if (skip) findManyOptions.skip = skip;
if (take && !select) { if (take && !select) {
findManyOptions.relations = ['globalRole', 'authIdentities']; findManyOptions.relations = ['authIdentities'];
} }
if (take && select && !select?.id) { if (take && select && !select?.id) {
@@ -74,11 +87,8 @@ export class UserRepository extends Repository<User> {
findManyOptions.where = otherFilters; findManyOptions.where = otherFilters;
if (isOwner !== undefined && globalOwnerRoleId) { if (isOwner !== undefined) {
findManyOptions.relations = ['globalRole']; findManyOptions.where.role = isOwner ? 'global:owner' : Not('global:owner');
findManyOptions.where.globalRole = {
id: isOwner ? globalOwnerRoleId : Not(globalOwnerRoleId),
};
} }
} }

View File

@@ -35,7 +35,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
async getAllActive() { async getAllActive() {
return await this.find({ return await this.find({
where: { active: true }, 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) { async findById(workflowId: string) {
return await this.findOne({ return await this.findOne({
where: { id: workflowId }, 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, createdAt: true,
updatedAt: true, updatedAt: true,
versionId: true, versionId: true,
shared: { userId: true, roleId: true }, shared: { userId: true, role: true },
}; };
delete select?.ownedBy; // remove non-entity field, handled after query 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 }; 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 !== '') { if (typeof where.name === 'string' && where.name !== '') {
where.name = Like(`%${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 type { User } from '@/databases/entities/User';
import { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow'; import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
import { Role } from '@/databases/entities/Role';
type StatisticsInsertResult = 'insert' | 'failed' | 'alreadyExists'; type StatisticsInsertResult = 'insert' | 'failed' | 'alreadyExists';
type StatisticsUpsertResult = StatisticsInsertResult | 'update'; type StatisticsUpsertResult = StatisticsInsertResult | 'update';
@@ -110,12 +109,11 @@ export class WorkflowStatisticsRepository extends Repository<WorkflowStatistics>
'shared_workflow', 'shared_workflow',
'shared_workflow.workflowId = workflow_statistics.workflowId', 'shared_workflow.workflowId = workflow_statistics.workflowId',
) )
.innerJoin(Role, 'role', 'role.id = shared_workflow.roleId')
.where('shared_workflow.userId = :userId', { userId }) .where('shared_workflow.userId = :userId', { userId })
.andWhere('workflow.active = :isActive', { isActive: true }) .andWhere('workflow.active = :isActive', { isActive: true })
.andWhere('workflow_statistics.name = :name', { name: StatisticsNames.productionSuccess }) .andWhere('workflow_statistics.name = :name', { name: StatisticsNames.productionSuccess })
.andWhere('workflow_statistics.count >= 5') .andWhere('workflow_statistics.count >= 5')
.andWhere('role.name = :roleName', { roleName: 'owner' }) .andWhere('role = :roleName', { roleName: 'workflow:owner' })
.getCount(); .getCount();
} }
} }

View File

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

View File

@@ -1,11 +1,11 @@
import type { Request, Response, RequestHandler } from 'express'; 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 { BooleanLicenseFeature } from '@/Interfaces';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; 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 AuthRoleMetadata = Record<string, AuthRole>;
export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>; export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { Reset } from '@/commands/user-management/reset'; import { Reset } from '@/commands/user-management/reset';
import type { Role } from '@db/entities/Role';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@@ -8,18 +7,13 @@ import { UserRepository } from '@db/repositories/user.repository';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import { getGlobalOwnerRole } from '../shared/db/roles';
import { createUser } from '../shared/db/users'; import { createUser } from '../shared/db/users';
let globalOwnerRole: Role;
beforeAll(async () => { beforeAll(async () => {
mockInstance(InternalHooks); mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials); mockInstance(LoadNodesAndCredentials);
mockInstance(NodeTypes); mockInstance(NodeTypes);
await testDb.init(); await testDb.init();
globalOwnerRole = await getGlobalOwnerRole();
}); });
beforeEach(async () => { beforeEach(async () => {
@@ -32,11 +26,11 @@ afterAll(async () => {
// eslint-disable-next-line n8n-local-rules/no-skipped-tests // eslint-disable-next-line n8n-local-rules/no-skipped-tests
test.skip('user-management:reset should reset DB to default user state', async () => { 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(); 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) { if (!user) {
fail('No owner found after DB reset to default user state'); 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 { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import * as testDb from '../shared/testDb';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
const oclifConfig = new Config({ root: __dirname }); const oclifConfig = new Config({ root: __dirname });
@@ -39,6 +40,11 @@ beforeAll(async () => {
mockInstance(RedisServicePubSubPublisher); mockInstance(RedisServicePubSubPublisher);
mockInstance(RedisServicePubSubSubscriber); mockInstance(RedisServicePubSubSubscriber);
mockInstance(OrchestrationService); mockInstance(OrchestrationService);
await testDb.init();
});
afterAll(async () => {
await testDb.terminate();
}); });
test('worker initializes all its components', async () => { 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 { randomCredentialPayload as payload } from './shared/random';
import { saveCredential } from './shared/db/credentials'; import { saveCredential } from './shared/db/credentials';
import { createMember, createOwner } from './shared/db/users'; import { createMember, createOwner } from './shared/db/users';
import { getCredentialOwnerRole } from './shared/db/roles';
const { any } = expect; const { any } = expect;
@@ -26,10 +25,14 @@ type GetAllResponse = { body: { data: ListQuery.Credentials.WithOwnedByAndShared
describe('GET /credentials', () => { describe('GET /credentials', () => {
describe('should return', () => { describe('should return', () => {
test('all credentials for owner', async () => { test('all credentials for owner', async () => {
const role = await getCredentialOwnerRole(); const { id: id1 } = await saveCredential(payload(), {
user: owner,
const { id: id1 } = await saveCredential(payload(), { user: owner, role }); role: 'credential:owner',
const { id: id2 } = await saveCredential(payload(), { user: member, role }); });
const { id: id2 } = await saveCredential(payload(), {
user: member,
role: 'credential:owner',
});
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -47,13 +50,11 @@ describe('GET /credentials', () => {
}); });
test('only own credentials for member', async () => { test('only own credentials for member', async () => {
const role = await getCredentialOwnerRole();
const firstMember = member; const firstMember = member;
const secondMember = await createMember(); const secondMember = await createMember();
const c1 = await saveCredential(payload(), { user: firstMember, role }); const c1 = await saveCredential(payload(), { user: firstMember, role: 'credential:owner' });
const c2 = await saveCredential(payload(), { user: secondMember, role }); const c2 = await saveCredential(payload(), { user: secondMember, role: 'credential:owner' });
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(firstMember) .authAgentFor(firstMember)
@@ -72,8 +73,7 @@ describe('GET /credentials', () => {
describe('filter', () => { describe('filter', () => {
test('should filter credentials by field: name - full match', async () => { test('should filter credentials by field: name - full match', async () => {
const role = await getCredentialOwnerRole(); const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const savedCred = await saveCredential(payload(), { user: owner, role });
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -97,8 +97,7 @@ describe('GET /credentials', () => {
}); });
test('should filter credentials by field: name - partial match', async () => { test('should filter credentials by field: name - partial match', async () => {
const role = await getCredentialOwnerRole(); const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const savedCred = await saveCredential(payload(), { user: owner, role });
const partialName = savedCred.name.slice(3); const partialName = savedCred.name.slice(3);
@@ -124,9 +123,7 @@ describe('GET /credentials', () => {
}); });
test('should filter credentials by field: type - full match', async () => { test('should filter credentials by field: type - full match', async () => {
const role = await getCredentialOwnerRole(); const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const savedCred = await saveCredential(payload(), { user: owner, role });
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -150,9 +147,7 @@ describe('GET /credentials', () => {
}); });
test('should filter credentials by field: type - partial match', async () => { test('should filter credentials by field: type - partial match', async () => {
const role = await getCredentialOwnerRole(); const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' });
const savedCred = await saveCredential(payload(), { user: owner, role });
const partialType = savedCred.type.slice(3); const partialType = savedCred.type.slice(3);
@@ -180,10 +175,8 @@ describe('GET /credentials', () => {
describe('select', () => { describe('select', () => {
test('should select credential field: id', async () => { test('should select credential field: id', async () => {
const role = await getCredentialOwnerRole(); await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -197,10 +190,8 @@ describe('GET /credentials', () => {
}); });
test('should select credential field: name', async () => { test('should select credential field: name', async () => {
const role = await getCredentialOwnerRole(); await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -214,10 +205,8 @@ describe('GET /credentials', () => {
}); });
test('should select credential field: type', async () => { test('should select credential field: type', async () => {
const role = await getCredentialOwnerRole(); await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
const response: GetAllResponse = await testServer const response: GetAllResponse = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -233,10 +222,8 @@ describe('GET /credentials', () => {
describe('take', () => { describe('take', () => {
test('should return n credentials or less, without skip', async () => { test('should return n credentials or less, without skip', async () => {
const role = await getCredentialOwnerRole(); await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
const response = await testServer const response = await testServer
.authAgentFor(owner) .authAgentFor(owner)
@@ -260,10 +247,8 @@ describe('GET /credentials', () => {
}); });
test('should return n credentials or less, with skip', async () => { test('should return n credentials or less, with skip', async () => {
const role = await getCredentialOwnerRole(); await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role: 'credential:owner' });
await saveCredential(payload(), { user: owner, role });
await saveCredential(payload(), { user: owner, role });
const response = await testServer const response = await testServer
.authAgentFor(owner) .authAgentFor(owner)

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import * as utils from './shared/utils/'; import * as utils from './shared/utils/';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { getGlobalOwnerRole } from './shared/db/roles';
import { createUser } from './shared/db/users'; 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. * The tests in this file are only checking endpoint permissions.
*/ */
let globalOwnerRole: Role;
let owner: User; let owner: User;
let authOwnerAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest;
@@ -21,8 +18,7 @@ const testServer = utils.setupTestServer({
}); });
beforeAll(async () => { beforeAll(async () => {
globalOwnerRole = await getGlobalOwnerRole(); owner = await createUser({ role: 'global:owner' });
owner = await createUser({ globalRole: globalOwnerRole });
authOwnerAgent = testServer.authAgentFor(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 { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { TagRepository } from '@/databases/repositories/tag.repository'; import { TagRepository } from '@/databases/repositories/tag.repository';
import { ImportService } from '@/services/import.service'; import { ImportService } from '@/services/import.service';
import { RoleService } from '@/services/role.service';
import { TagEntity } from '@/databases/entities/TagEntity'; import { TagEntity } from '@/databases/entities/TagEntity';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
@@ -34,12 +33,7 @@ describe('ImportService', () => {
credentialsRepository.find.mockResolvedValue([]); credentialsRepository.find.mockResolvedValue([]);
importService = new ImportService( importService = new ImportService(mock(), credentialsRepository, tagRepository);
mock(),
credentialsRepository,
tagRepository,
Container.get(RoleService),
);
}); });
afterEach(async () => { afterEach(async () => {
@@ -67,10 +61,8 @@ describe('ImportService', () => {
await importService.importWorkflows([workflowToImport], owner.id); await importService.importWorkflows([workflowToImport], owner.id);
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
const dbSharing = await Container.get(SharedWorkflowRepository).findOneOrFail({ 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); expect(dbSharing.userId).toBe(owner.id);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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