From d6deceacde93e732f767d23d1feafdfb5accea20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Wed, 24 Jan 2024 13:38:57 +0100 Subject: [PATCH] refactor(core): Remove roleId indirection (no-changelog) (#8413) --- packages/cli/src/ActiveWebhooks.ts | 2 +- packages/cli/src/ActiveWorkflowRunner.ts | 4 +- packages/cli/src/CredentialsHelper.ts | 10 +- packages/cli/src/Db.ts | 18 +- packages/cli/src/Interfaces.ts | 5 +- packages/cli/src/InternalHooks.ts | 20 +- packages/cli/src/Ldap/helpers.ts | 22 +- packages/cli/src/Ldap/ldap.service.ts | 11 +- packages/cli/src/PublicApi/index.ts | 1 - packages/cli/src/PublicApi/types.ts | 3 - .../v1/handlers/audit/audit.handler.ts | 2 +- .../credentials/credentials.handler.ts | 15 +- .../credentials/credentials.service.ts | 8 +- .../handlers/executions/executions.handler.ts | 6 +- .../sourceControl/sourceControl.handler.ts | 2 +- .../v1/handlers/users/spec/schemas/user.yml | 6 +- .../v1/handlers/users/users.handler.ee.ts | 4 +- .../v1/handlers/users/users.service.ee.ts | 45 ++-- .../handlers/workflows/workflows.handler.ts | 21 +- .../handlers/workflows/workflows.service.ts | 11 +- .../shared/middlewares/global.middleware.ts | 8 +- packages/cli/src/Server.ts | 2 - .../src/UserManagement/PermissionChecker.ts | 11 +- packages/cli/src/auth/jwt.ts | 1 - packages/cli/src/auth/methods/email.ts | 2 +- packages/cli/src/auth/methods/ldap.ts | 4 +- packages/cli/src/commands/db/revert.ts | 3 +- .../cli/src/commands/import/credentials.ts | 24 +- packages/cli/src/commands/import/workflow.ts | 8 +- .../cli/src/commands/user-management/reset.ts | 22 +- .../cli/src/controllers/auth.controller.ts | 6 +- .../cli/src/controllers/e2e.controller.ts | 35 +-- .../src/controllers/invitation.controller.ts | 12 +- packages/cli/src/controllers/me.controller.ts | 2 - .../cli/src/controllers/owner.controller.ts | 15 +- .../cli/src/controllers/role.controller.ts | 22 -- .../cli/src/controllers/users.controller.ts | 39 +-- .../credentials/credentials.controller.ee.ts | 7 +- .../src/credentials/credentials.controller.ts | 8 +- .../src/credentials/credentials.service.ee.ts | 9 +- .../src/credentials/credentials.service.ts | 14 +- packages/cli/src/databases/dsl/Table.ts | 38 ++- packages/cli/src/databases/dsl/index.ts | 41 +++- packages/cli/src/databases/entities/Role.ts | 39 --- .../databases/entities/SharedCredentials.ts | 8 +- .../src/databases/entities/SharedWorkflow.ts | 8 +- packages/cli/src/databases/entities/User.ts | 22 +- packages/cli/src/databases/entities/index.ts | 2 - .../common/1705429061930-DropRoleMapping.ts | 127 ++++++++++ .../src/databases/migrations/mysqldb/index.ts | 2 + .../databases/migrations/postgresdb/index.ts | 2 + .../sqlite/1705429061930-DropRoleMapping.ts | 5 + .../src/databases/migrations/sqlite/index.ts | 2 + .../repositories/credentials.repository.ts | 4 +- .../databases/repositories/role.repository.ts | 42 ---- .../sharedCredentials.repository.ts | 26 +- .../repositories/sharedWorkflow.repository.ts | 45 ++-- .../databases/repositories/user.repository.ts | 34 ++- .../repositories/workflow.repository.ts | 8 +- .../workflowStatistics.repository.ts | 4 +- .../cli/src/decorators/registerController.ts | 4 +- packages/cli/src/decorators/types.ts | 4 +- .../sourceControlImport.service.ee.ts | 59 +---- .../src/executions/execution.service.ee.ts | 2 +- .../src/executions/executions.controller.ts | 2 +- packages/cli/src/requests.ts | 10 +- packages/cli/src/services/import.service.ts | 21 +- .../cli/src/services/ownership.service.ts | 20 +- packages/cli/src/services/role.service.ts | 109 --------- packages/cli/src/services/user.service.ts | 17 +- .../src/services/userOnboarding.service.ts | 5 +- packages/cli/src/sso/saml/saml.service.ee.ts | 2 +- packages/cli/src/sso/saml/samlHelpers.ts | 3 +- packages/cli/src/telemetry/index.ts | 4 +- .../cli/src/workflows/workflow.service.ee.ts | 8 +- .../cli/src/workflows/workflow.service.ts | 5 +- .../src/workflows/workflowSharing.service.ts | 16 +- .../cli/src/workflows/workflows.controller.ts | 30 +-- .../cli/test/integration/auth.api.test.ts | 66 ++--- packages/cli/test/integration/auth.mw.test.ts | 7 +- .../integration/commands/reset.cmd.test.ts | 10 +- .../integration/commands/worker.cmd.test.ts | 6 + .../credentials.controller.test.ts | 63 ++--- .../test/integration/credentials.ee.test.ts | 57 ++--- .../cli/test/integration/credentials.test.ts | 26 +- .../environments/SourceControl.test.ts | 4 +- .../cli/test/integration/eventbus.ee.test.ts | 6 +- .../cli/test/integration/eventbus.test.ts | 6 +- .../test/integration/import.service.test.ts | 12 +- .../test/integration/invitations.api.test.ts | 39 ++- .../test/integration/ldap/ldap.api.test.ts | 34 +-- .../cli/test/integration/license.api.test.ts | 7 +- packages/cli/test/integration/me.api.test.ts | 31 +-- .../cli/test/integration/mfa/mfa.api.test.ts | 4 +- .../cli/test/integration/owner.api.test.ts | 14 +- .../integration/passwordReset.api.test.ts | 17 +- .../integration/publicApi/credentials.test.ts | 22 +- .../integration/publicApi/executions.test.ts | 9 +- .../integration/publicApi/users.ee.test.ts | 46 ++-- .../integration/publicApi/workflows.test.ts | 23 +- .../cli/test/integration/role.api.test.ts | 79 ------ .../test/integration/shared/db/credentials.ts | 16 +- .../cli/test/integration/shared/db/roles.ts | 35 --- .../cli/test/integration/shared/db/users.ts | 36 +-- .../test/integration/shared/db/workflows.ts | 15 +- packages/cli/test/integration/shared/types.ts | 1 - .../integration/shared/utils/testServer.ts | 5 - .../test/integration/shared/utils/users.ts | 2 +- .../cli/test/integration/tags.api.test.ts | 4 +- ...sitory.test.ts => user.repository.test.ts} | 18 +- .../cli/test/integration/users.api.test.ts | 86 +++---- .../workflows/workflows.controller.ee.test.ts | 25 +- .../workflows/workflows.controller.test.ts | 3 +- .../cli/test/unit/PermissionChecker.test.ts | 11 +- packages/cli/test/unit/WorkflowRunner.test.ts | 4 +- .../unit/controllers/me.controller.test.ts | 12 +- .../oAuth1Credential.controller.test.ts | 2 +- .../oAuth2Credential.controller.test.ts | 2 +- .../unit/controllers/owner.controller.test.ts | 2 +- .../translation.controller.test.ts | 4 +- .../unit/repositories/role.repository.test.ts | 34 --- .../unit/services/ownership.service.test.ts | 228 +++++++----------- .../test/unit/services/role.service.test.ts | 80 ------ .../test/unit/services/user.service.test.ts | 2 - packages/cli/test/unit/shared/mockObjects.ts | 22 -- packages/editor-ui/src/Interface.ts | 11 +- packages/editor-ui/src/api/users.ts | 2 +- .../src/components/InviteUsersModal.vue | 4 +- .../MainHeader/CollaborationPane.vue | 2 +- .../components/__tests__/BannersStack.test.ts | 13 +- .../__tests__/CollaborationPane.test.ts | 21 +- .../banners/__tests__/V1Banner.spec.ts | 6 +- .../src/rbac/checks/__tests__/hasRole.test.ts | 4 +- packages/editor-ui/src/rbac/checks/hasRole.ts | 2 +- .../rbac/middleware/__tests__/role.test.ts | 12 +- .../editor-ui/src/stores/__tests__/ui.test.ts | 16 +- packages/editor-ui/src/stores/users.store.ts | 6 +- packages/editor-ui/src/utils/userUtils.ts | 12 +- .../editor-ui/src/views/SettingsUsersView.vue | 8 +- 139 files changed, 922 insertions(+), 1684 deletions(-) delete mode 100644 packages/cli/src/controllers/role.controller.ts delete mode 100644 packages/cli/src/databases/entities/Role.ts create mode 100644 packages/cli/src/databases/migrations/common/1705429061930-DropRoleMapping.ts create mode 100644 packages/cli/src/databases/migrations/sqlite/1705429061930-DropRoleMapping.ts delete mode 100644 packages/cli/src/databases/repositories/role.repository.ts delete mode 100644 packages/cli/src/services/role.service.ts delete mode 100644 packages/cli/test/integration/role.api.test.ts delete mode 100644 packages/cli/test/integration/shared/db/roles.ts rename packages/cli/test/integration/{role.repository.test.ts => user.repository.test.ts} (60%) delete mode 100644 packages/cli/test/unit/repositories/role.repository.test.ts delete mode 100644 packages/cli/test/unit/services/role.service.test.ts diff --git a/packages/cli/src/ActiveWebhooks.ts b/packages/cli/src/ActiveWebhooks.ts index d7ded74a6c..6b9717341b 100644 --- a/packages/cli/src/ActiveWebhooks.ts +++ b/packages/cli/src/ActiveWebhooks.ts @@ -84,7 +84,7 @@ export class ActiveWebhooks implements IWebhookManager { const workflowData = await this.workflowRepository.findOne({ where: { id: webhook.workflowId }, - relations: ['shared', 'shared.user', 'shared.user.globalRole'], + relations: ['shared', 'shared.user'], }); if (workflowData === null) { diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 70b0272227..ad19b09d18 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -229,7 +229,7 @@ export class ActiveWorkflowRunner { async clearWebhooks(workflowId: string) { const workflowData = await this.workflowRepository.findOne({ where: { id: workflowId }, - relations: ['shared', 'shared.user', 'shared.user.globalRole'], + relations: ['shared', 'shared.user'], }); if (workflowData === null) { @@ -615,7 +615,7 @@ export class ActiveWorkflowRunner { ); } - const sharing = dbWorkflow.shared.find((shared) => shared.role.name === 'owner'); + const sharing = dbWorkflow.shared.find((shared) => shared.role === 'workflow:owner'); if (!sharing) { throw new WorkflowActivationError(`Workflow ${dbWorkflow.display()} has no owner`); diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index df945ef4fc..95b5a9a161 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -786,15 +786,9 @@ export class CredentialsHelper extends ICredentialsHelper { const credential = await this.sharedCredentialsRepository.findOne({ where: { - role: { - scope: 'credential', - name: 'owner', - }, + role: 'credential:owner', user: { - globalRole: { - scope: 'global', - name: 'owner', - }, + role: 'global:owner', }, credentials: { id: nodeCredential.id, diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index b39a8d1e6a..839a0b187b 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -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 { if (connectionState.connected) return; @@ -130,13 +140,7 @@ export async function init(testConnectionOptions?: ConnectionOptions): Promise { return Math.random().toString(36).slice(-8); }; -/** - * Return the user role to be assigned to LDAP users - */ -export const getLdapUserRole = async (): Promise => { - return await Container.get(RoleService).findGlobalMemberRole(); -}; - /** * Validate the structure of the LDAP configuration schema */ @@ -102,7 +93,7 @@ export const getAuthIdentityByLdapId = async ( idAttributeValue: string, ): Promise => { return await Container.get(AuthIdentityRepository).findOne({ - relations: ['user', 'user.globalRole'], + relations: ['user'], where: { providerId: idAttributeValue, providerType: 'ldap', @@ -113,7 +104,6 @@ export const getAuthIdentityByLdapId = async ( export const getUserByEmail = async (email: string): Promise => { return await Container.get(UserRepository).findOne({ where: { email }, - relations: ['globalRole'], }); }; @@ -164,13 +154,13 @@ export const getLdapUsers = async (): Promise => { export const mapLdapUserToDbUser = ( ldapUser: LdapUser, ldapConfig: LdapConfig, - role?: Role, + toCreate = false, ): [string, User] => { const user = new User(); const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig); Object.assign(user, data); - if (role) { - user.globalRole = role; + if (toCreate) { + user.role = 'global:member'; user.password = randomPassword(); user.disabled = false; } else { @@ -270,10 +260,10 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => { return await Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId)); }; -export const createLdapUserOnLocalDb = async (role: Role, data: Partial, ldapId: string) => { +export const createLdapUserOnLocalDb = async (data: Partial, ldapId: string) => { const user = await Container.get(UserRepository).save({ password: randomPassword(), - globalRole: role, + role: 'global:member', ...data, }); await createLdapAuthIdentity(user, ldapId); diff --git a/packages/cli/src/Ldap/ldap.service.ts b/packages/cli/src/Ldap/ldap.service.ts index 5d3af3fb86..c7eda15e33 100644 --- a/packages/cli/src/Ldap/ldap.service.ts +++ b/packages/cli/src/Ldap/ldap.service.ts @@ -7,7 +7,6 @@ import { ApplicationError, jsonParse } from 'n8n-workflow'; import { Cipher } from 'n8n-core'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory'; import { SettingsRepository } from '@db/repositories/settings.repository'; @@ -30,7 +29,6 @@ import { escapeFilter, formatUrl, getLdapIds, - getLdapUserRole, getLdapUsers, getMappingAttributes, mapLdapUserToDbUser, @@ -346,12 +344,9 @@ export class LdapService { const localAdUsers = await getLdapIds(); - const role = await getLdapUserRole(); - const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess( adUsers, localAdUsers, - role, ); this.logger.debug('LDAP - Users processed', { @@ -407,14 +402,13 @@ export class LdapService { private getUsersToProcess( adUsers: LdapUser[], localAdUsers: string[], - role: Role, ): { usersToCreate: Array<[string, User]>; usersToUpdate: Array<[string, User]>; usersToDisable: string[]; } { return { - usersToCreate: this.getUsersToCreate(adUsers, localAdUsers, role), + usersToCreate: this.getUsersToCreate(adUsers, localAdUsers), usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers), usersToDisable: this.getUsersToDisable(adUsers, localAdUsers), }; @@ -424,11 +418,10 @@ export class LdapService { private getUsersToCreate( remoteAdUsers: LdapUser[], localLdapIds: string[], - role: Role, ): Array<[string, User]> { return remoteAdUsers .filter((adUser) => !localLdapIds.includes(adUser[this.config.ldapIdAttribute] as string)) - .map((adUser) => mapLdapUserToDbUser(adUser, this.config, role)); + .map((adUser) => mapLdapUserToDbUser(adUser, this.config, true)); } /** Get users in LDAP that are already in the database */ diff --git a/packages/cli/src/PublicApi/index.ts b/packages/cli/src/PublicApi/index.ts index 212132b77e..7248b91e72 100644 --- a/packages/cli/src/PublicApi/index.ts +++ b/packages/cli/src/PublicApi/index.ts @@ -98,7 +98,6 @@ async function createApiRouter( const apiKey = req.headers[schema.name.toLowerCase()] as string; const user = await Container.get(UserRepository).findOne({ where: { apiKey }, - relations: ['globalRole'], }); if (!user) return false; diff --git a/packages/cli/src/PublicApi/types.ts b/packages/cli/src/PublicApi/types.ts index fe40fdf63a..4933edda55 100644 --- a/packages/cli/src/PublicApi/types.ts +++ b/packages/cli/src/PublicApi/types.ts @@ -3,8 +3,6 @@ import type { IDataObject, ExecutionStatus } from 'n8n-workflow'; import type { User } from '@db/entities/User'; -import type { Role } from '@db/entities/Role'; - import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { UserManagementMailer } from '@/UserManagement/email'; @@ -25,7 +23,6 @@ export type AuthenticatedRequest< RequestQuery = {}, > = express.Request & { user: User; - globalMemberRole?: Role; mailer?: UserManagementMailer; }; diff --git a/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts b/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts index 2f9f4892b4..9fd2d028ff 100644 --- a/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/audit/audit.handler.ts @@ -5,7 +5,7 @@ import Container from 'typedi'; export = { generateAudit: [ - authorize(['owner', 'admin']), + authorize(['global:owner', 'global:admin']), async (req: AuditRequest.Generate, res: Response): Promise => { try { const { SecurityAuditService } = await import('@/security-audit/SecurityAudit.service'); diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts index a31656be19..1a4275f949 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.handler.ts @@ -23,7 +23,7 @@ import { Container } from 'typedi'; export = { createCredential: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), validCredentialType, validCredentialsProperties, async ( @@ -47,7 +47,7 @@ export = { }, ], deleteCredential: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async ( req: CredentialRequest.Delete, res: express.Response, @@ -55,13 +55,10 @@ export = { const { id: credentialId } = req.params; let credential: CredentialsEntity | undefined; - if (!['owner', 'admin'].includes(req.user.globalRole.name)) { - const shared = await getSharedCredentials(req.user.id, credentialId, [ - 'credentials', - 'role', - ]); + if (!['global:owner', 'global:admin'].includes(req.user.role)) { + const shared = await getSharedCredentials(req.user.id, credentialId); - if (shared?.role.name === 'owner') { + if (shared?.role === 'credential:owner') { credential = shared.credentials; } } else { @@ -78,7 +75,7 @@ export = { ], getCredentialType: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: CredentialTypeRequest.Get, res: express.Response): Promise => { const { credentialTypeName } = req.params; diff --git a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts index 721ea6c8f1..ce6800506d 100644 --- a/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/credentials/credentials.service.ts @@ -9,7 +9,6 @@ import { ExternalHooks } from '@/ExternalHooks'; import type { IDependency, IJsonSchema } from '../../../types'; import type { CredentialRequest } from '@/requests'; import { Container } from 'typedi'; -import { RoleService } from '@/services/role.service'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; @@ -20,14 +19,13 @@ export async function getCredentials(credentialId: string): Promise { return await Container.get(SharedCredentialsRepository).findOne({ where: { userId, credentialsId: credentialId, }, - relations, + relations: ['credentials'], }); } @@ -60,8 +58,6 @@ export async function saveCredential( user: User, encryptedData: ICredentialsDb, ): Promise { - const role = await Container.get(RoleService).findCredentialOwnerRole(); - await Container.get(ExternalHooks).run('credentials.create', [encryptedData]); return await Db.transaction(async (transactionManager) => { @@ -72,7 +68,7 @@ export async function saveCredential( const newSharedCredential = new SharedCredentials(); Object.assign(newSharedCredential, { - role, + role: 'credential:owner', user, credentials: savedCredential, }); diff --git a/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts b/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts index a54a57fbca..3a63fb9aa0 100644 --- a/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/executions/executions.handler.ts @@ -12,7 +12,7 @@ import { ExecutionRepository } from '@db/repositories/execution.repository'; export = { deleteExecution: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: ExecutionRequest.Delete, res: express.Response): Promise => { const sharedWorkflowsIds = await getSharedWorkflowIds(req.user); @@ -44,7 +44,7 @@ export = { }, ], getExecution: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: ExecutionRequest.Get, res: express.Response): Promise => { const sharedWorkflowsIds = await getSharedWorkflowIds(req.user); @@ -75,7 +75,7 @@ export = { }, ], getExecutions: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), validCursor, async (req: ExecutionRequest.GetAll, res: express.Response): Promise => { const { diff --git a/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts b/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts index d19741f82b..66233867de 100644 --- a/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/sourceControl/sourceControl.handler.ts @@ -14,7 +14,7 @@ import { InternalHooks } from '@/InternalHooks'; export = { pull: [ - authorize(['owner', 'admin']), + authorize(['global:owner', 'global:admin']), async ( req: PublicSourceControlRequest.Pull, res: express.Response, diff --git a/packages/cli/src/PublicApi/v1/handlers/users/spec/schemas/user.yml b/packages/cli/src/PublicApi/v1/handlers/users/spec/schemas/user.yml index e0403962ae..532ee98736 100644 --- a/packages/cli/src/PublicApi/v1/handlers/users/spec/schemas/user.yml +++ b/packages/cli/src/PublicApi/v1/handlers/users/spec/schemas/user.yml @@ -36,5 +36,7 @@ properties: description: Last time the user was updated. format: date-time readOnly: true - globalRole: - $ref: './role.yml' + role: + type: string + example: owner + readOnly: true diff --git a/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts b/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts index f9611fa29f..8fd36b1dbb 100644 --- a/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts +++ b/packages/cli/src/PublicApi/v1/handlers/users/users.handler.ee.ts @@ -15,7 +15,7 @@ import { InternalHooks } from '@/InternalHooks'; export = { getUser: [ validLicenseWithUserQuota, - authorize(['owner', 'admin']), + authorize(['global:owner', 'global:admin']), async (req: UserRequest.Get, res: express.Response) => { const { includeRole = false } = req.query; const { id } = req.params; @@ -41,7 +41,7 @@ export = { getUsers: [ validLicenseWithUserQuota, validCursor, - authorize(['owner', 'admin']), + authorize(['global:owner', 'global:admin']), async (req: UserRequest.Get, res: express.Response) => { const { offset = 0, limit = 100, includeRole = false } = req.query; diff --git a/packages/cli/src/PublicApi/v1/handlers/users/users.service.ee.ts b/packages/cli/src/PublicApi/v1/handlers/users/users.service.ee.ts index 94c3880f37..f7bf661816 100644 --- a/packages/cli/src/PublicApi/v1/handlers/users/users.service.ee.ts +++ b/packages/cli/src/PublicApi/v1/handlers/users/users.service.ee.ts @@ -4,24 +4,21 @@ import type { User } from '@db/entities/User'; import pick from 'lodash/pick'; import { validate as uuidValidate } from 'uuid'; -export const getSelectableProperties = (table: 'user' | 'role'): string[] => { - return { - user: ['id', 'email', 'firstName', 'lastName', 'createdAt', 'updatedAt', 'isPending'], - role: ['id', 'name', 'scope', 'createdAt', 'updatedAt'], - }[table]; -}; - export async function getUser(data: { withIdentifier: string; includeRole?: boolean; }): Promise { - return await Container.get(UserRepository).findOne({ - where: { - ...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }), - ...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }), - }, - relations: data?.includeRole ? ['globalRole'] : undefined, - }); + return await Container.get(UserRepository) + .findOne({ + where: { + ...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }), + ...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }), + }, + }) + .then((user) => { + if (user && !data?.includeRole) delete (user as Partial).role; + return user; + }); } export async function getAllUsersAndCount(data: { @@ -31,19 +28,29 @@ export async function getAllUsersAndCount(data: { }): Promise<[User[], number]> { const users = await Container.get(UserRepository).find({ where: {}, - relations: data?.includeRole ? ['globalRole'] : undefined, skip: data.offset, take: data.limit, }); + if (!data?.includeRole) { + users.forEach((user) => { + delete (user as Partial).role; + }); + } const count = await Container.get(UserRepository).count(); return [users, count]; } +const userProperties = [ + 'id', + 'email', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + 'isPending', +]; function pickUserSelectableProperties(user: User, options?: { includeRole: boolean }) { - return pick( - user, - getSelectableProperties('user').concat(options?.includeRole ? ['globalRole'] : []), - ); + return pick(user, userProperties.concat(options?.includeRole ? ['role'] : [])); } export function clean(user: User, options?: { includeRole: boolean }): Partial; diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index 67f9b908a2..bc1dcdc36f 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -23,7 +23,6 @@ import { } from './workflows.service'; import { WorkflowService } from '@/workflows/workflow.service'; import { InternalHooks } from '@/InternalHooks'; -import { RoleService } from '@/services/role.service'; import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee'; import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; import { TagRepository } from '@/databases/repositories/tag.repository'; @@ -31,7 +30,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository export = { createWorkflow: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: WorkflowRequest.Create, res: express.Response): Promise => { const workflow = req.body; @@ -42,9 +41,7 @@ export = { addNodeIds(workflow); - const role = await Container.get(RoleService).findWorkflowOwnerRole(); - - const createdWorkflow = await createWorkflow(workflow, req.user, role); + const createdWorkflow = await createWorkflow(workflow, req.user, 'workflow:owner'); await Container.get(WorkflowHistoryService).saveVersion( req.user, @@ -59,7 +56,7 @@ export = { }, ], deleteWorkflow: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: WorkflowRequest.Get, res: express.Response): Promise => { const { id: workflowId } = req.params; @@ -74,7 +71,7 @@ export = { }, ], getWorkflow: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: WorkflowRequest.Get, res: express.Response): Promise => { const { id } = req.params; @@ -95,7 +92,7 @@ export = { }, ], getWorkflows: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), validCursor, async (req: WorkflowRequest.GetAll, res: express.Response): Promise => { const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query; @@ -104,7 +101,7 @@ export = { ...(active !== undefined && { active }), }; - if (['owner', 'admin'].includes(req.user.globalRole.name)) { + if (['global:owner', 'global:admin'].includes(req.user.role)) { if (tags) { const workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags( parseTagNames(tags), @@ -159,7 +156,7 @@ export = { }, ], updateWorkflow: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: WorkflowRequest.Update, res: express.Response): Promise => { const { id } = req.params; const updateData = new WorkflowEntity(); @@ -221,7 +218,7 @@ export = { }, ], activateWorkflow: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: WorkflowRequest.Activate, res: express.Response): Promise => { const { id } = req.params; @@ -255,7 +252,7 @@ export = { }, ], deactivateWorkflow: [ - authorize(['owner', 'admin', 'member']), + authorize(['global:owner', 'global:admin', 'global:member']), async (req: WorkflowRequest.Activate, res: express.Response): Promise => { const { id } = req.params; diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts index 423c0f4651..8d53a72ea1 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.service.ts @@ -1,10 +1,9 @@ +import { Container } from 'typedi'; import * as Db from '@/Db'; import type { User } from '@db/entities/User'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; -import { SharedWorkflow } from '@db/entities/SharedWorkflow'; -import type { Role } from '@db/entities/Role'; +import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow'; import config from '@/config'; -import Container from 'typedi'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; @@ -13,7 +12,7 @@ function insertIf(condition: boolean, elements: string[]): string[] { } export async function getSharedWorkflowIds(user: User): Promise { - const where = ['owner', 'admin'].includes(user.globalRole.name) ? {} : { userId: user.id }; + const where = ['global:owner', 'global:admin'].includes(user.role) ? {} : { userId: user.id }; const sharedWorkflows = await Container.get(SharedWorkflowRepository).find({ where, select: ['workflowId'], @@ -27,7 +26,7 @@ export async function getSharedWorkflow( ): Promise { return await Container.get(SharedWorkflowRepository).findOne({ where: { - ...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }), + ...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }), ...(workflowId && { workflowId }), }, relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'], @@ -43,7 +42,7 @@ export async function getWorkflowById(id: string): Promise { return await Db.transaction(async (transactionManager) => { const newWorkflow = new WorkflowEntity(); diff --git a/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts index 7335ca4cd8..97501f0466 100644 --- a/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/PublicApi/v1/shared/middlewares/global.middleware.ts @@ -6,20 +6,18 @@ import { Container } from 'typedi'; import type { AuthenticatedRequest, PaginatedRequest } from '../../../types'; import { decodeCursor } from '../services/pagination.service'; import { License } from '@/License'; -import type { RoleNames } from '@/databases/entities/Role'; +import type { GlobalRole } from '@db/entities/User'; const UNLIMITED_USERS_QUOTA = -1; export const authorize = - (authorizedRoles: readonly RoleNames[]) => + (authorizedRoles: readonly GlobalRole[]) => ( req: AuthenticatedRequest, res: express.Response, next: express.NextFunction, ): express.Response | void => { - const { name } = req.user.globalRole; - - if (!authorizedRoles.includes(name)) { + if (!authorizedRoles.includes(req.user.role)) { return res.status(403).json({ message: 'Forbidden' }); } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f9fb609547..7bad362226 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -89,7 +89,6 @@ import { OrchestrationController } from './controllers/orchestration.controller' import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee'; import { InvitationController } from './controllers/invitation.controller'; import { CollaborationService } from './collaboration/collaboration.service'; -import { RoleController } from './controllers/role.controller'; import { BadRequestError } from './errors/response-errors/bad-request.error'; import { OrchestrationService } from '@/services/orchestration.service'; @@ -228,7 +227,6 @@ export class Server extends AbstractServer { VariablesController, InvitationController, VariablesController, - RoleController, ActiveWorkflowsController, WorkflowsController, ExecutionsController, diff --git a/packages/cli/src/UserManagement/PermissionChecker.ts b/packages/cli/src/UserManagement/PermissionChecker.ts index c1bfcae759..7b6261c4d6 100644 --- a/packages/cli/src/UserManagement/PermissionChecker.ts +++ b/packages/cli/src/UserManagement/PermissionChecker.ts @@ -5,7 +5,6 @@ import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow'; import config from '@/config'; import { License } from '@/License'; import { OwnershipService } from '@/services/ownership.service'; -import { RoleService } from '@/services/role.service'; import { UserRepository } from '@db/repositories/user.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; @@ -16,7 +15,6 @@ export class PermissionChecker { private readonly userRepository: UserRepository, private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository, - private readonly roleService: RoleService, private readonly ownershipService: OwnershipService, private readonly license: License, ) {} @@ -37,7 +35,6 @@ export class PermissionChecker { const user = await this.userRepository.findOneOrFail({ where: { id: userId }, - relations: ['globalRole'], }); if (user.hasGlobalScope('workflow:execute')) return; @@ -56,12 +53,8 @@ export class PermissionChecker { workflowUserIds = workflowSharings.map((s) => s.userId); } - const roleId = await this.roleService.findCredentialOwnerRoleId(); - - const credentialSharings = await this.sharedCredentialsRepository.findSharings( - workflowUserIds, - roleId, - ); + const credentialSharings = + await this.sharedCredentialsRepository.findOwnedSharings(workflowUserIds); const accessibleCredIds = credentialSharings.map((s) => s.credentialsId); diff --git a/packages/cli/src/auth/jwt.ts b/packages/cli/src/auth/jwt.ts index cf2a415b75..affc9ea75f 100644 --- a/packages/cli/src/auth/jwt.ts +++ b/packages/cli/src/auth/jwt.ts @@ -56,7 +56,6 @@ export const createPasswordSha = (user: User) => export async function resolveJwtContent(jwtPayload: JwtPayload): Promise { const user = await Container.get(UserRepository).findOne({ where: { id: jwtPayload.id }, - relations: ['globalRole'], }); let passwordHash = null; diff --git a/packages/cli/src/auth/methods/email.ts b/packages/cli/src/auth/methods/email.ts index 9775c1959d..ba839fa4e6 100644 --- a/packages/cli/src/auth/methods/email.ts +++ b/packages/cli/src/auth/methods/email.ts @@ -12,7 +12,7 @@ export const handleEmailLogin = async ( ): Promise => { const user = await Container.get(UserRepository).findOne({ where: { email }, - relations: ['globalRole', 'authIdentities'], + relations: ['authIdentities'], }); if (user?.password && (await Container.get(PasswordUtility).compare(password, user.password))) { diff --git a/packages/cli/src/auth/methods/ldap.ts b/packages/cli/src/auth/methods/ldap.ts index c2e38cff02..964fc9f485 100644 --- a/packages/cli/src/auth/methods/ldap.ts +++ b/packages/cli/src/auth/methods/ldap.ts @@ -4,7 +4,6 @@ import { InternalHooks } from '@/InternalHooks'; import { LdapService } from '@/Ldap/ldap.service'; import { createLdapUserOnLocalDb, - getLdapUserRole, getUserByEmail, getAuthIdentityByLdapId, isLdapEnabled, @@ -50,8 +49,7 @@ export const handleLdapLogin = async ( const identity = await createLdapAuthIdentity(emailUser, ldapId); await updateLdapUserOnLocalDb(identity, ldapAttributesValues); } else { - const role = await getLdapUserRole(); - const user = await createLdapUserOnLocalDb(role, ldapAttributesValues, ldapId); + const user = await createLdapUserOnLocalDb(ldapAttributesValues, ldapId); void Container.get(InternalHooks).onUserSignup(user, { user_type: 'ldap', was_disabled_ldap_user: false, diff --git a/packages/cli/src/commands/db/revert.ts b/packages/cli/src/commands/db/revert.ts index b4c99a5057..967d4a8286 100644 --- a/packages/cli/src/commands/db/revert.ts +++ b/packages/cli/src/commands/db/revert.ts @@ -3,7 +3,7 @@ import type { DataSourceOptions as ConnectionOptions } from 'typeorm'; import { DataSource as Connection } from 'typeorm'; import { Container } from 'typedi'; import { Logger } from '@/Logger'; -import { getConnectionOptions } from '@/Db'; +import { getConnectionOptions, setSchema } from '@/Db'; import type { Migration } from '@db/types'; import { wrapMigration } from '@db/utils/migrationHelpers'; import config from '@/config'; @@ -40,6 +40,7 @@ export class DbRevertMigrationCommand extends Command { this.connection = new Connection(connectionOptions); await this.connection.initialize(); + if (dbType === 'postgresdb') await setSchema(this.connection); await this.connection.undoLastMigration(); await this.connection.destroy(); } diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 067e8c7f4e..95a85fb90c 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -8,13 +8,11 @@ import type { EntityManager } from 'typeorm'; import * as Db from '@/Db'; import type { User } from '@db/entities/User'; import { SharedCredentials } from '@db/entities/SharedCredentials'; -import type { Role } from '@db/entities/Role'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { disableAutoGeneratedIds } from '@db/utils/commandHelpers'; import { BaseCommand } from '../BaseCommand'; import type { ICredentialsEncrypted } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow'; -import { RoleService } from '@/services/role.service'; import { UM_FIX_INSTRUCTION } from '@/constants'; import { UserRepository } from '@db/repositories/user.repository'; @@ -42,8 +40,6 @@ export class ImportCredentialsCommand extends BaseCommand { }), }; - private ownerCredentialRole: Role; - private transactionManager: EntityManager; async init() { @@ -71,7 +67,6 @@ export class ImportCredentialsCommand extends BaseCommand { let totalImported = 0; const cipher = Container.get(Cipher); - await this.initOwnerCredentialRole(); const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); if (flags.separate) { @@ -145,16 +140,6 @@ export class ImportCredentialsCommand extends BaseCommand { ); } - private async initOwnerCredentialRole() { - const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole(); - - if (!ownerCredentialRole) { - throw new ApplicationError(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`); - } - - this.ownerCredentialRole = ownerCredentialRole; - } - private async storeCredential(credential: Partial, user: User) { if (!credential.nodesAccess) { credential.nodesAccess = []; @@ -165,19 +150,14 @@ export class ImportCredentialsCommand extends BaseCommand { { credentialsId: result.identifiers[0].id as string, userId: user.id, - roleId: this.ownerCredentialRole.id, + role: 'credential:owner', }, ['credentialsId', 'userId'], ); } private async getOwner() { - const ownerGlobalRole = await Container.get(RoleService).findGlobalOwnerRole(); - - const owner = - ownerGlobalRole && - (await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole.id })); - + const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); if (!owner) { throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); } diff --git a/packages/cli/src/commands/import/workflow.ts b/packages/cli/src/commands/import/workflow.ts index 28d7ca91a0..21c3d82501 100644 --- a/packages/cli/src/commands/import/workflow.ts +++ b/packages/cli/src/commands/import/workflow.ts @@ -11,7 +11,6 @@ import { generateNanoId } from '@db/utils/generators'; import { UserRepository } from '@db/repositories/user.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import type { IWorkflowToImport } from '@/Interfaces'; -import { RoleService } from '@/services/role.service'; import { ImportService } from '@/services/import.service'; import { BaseCommand } from '../BaseCommand'; @@ -138,12 +137,7 @@ export class ImportWorkflowsCommand extends BaseCommand { } private async getOwner() { - const ownerGlobalRole = await Container.get(RoleService).findGlobalOwnerRole(); - - const owner = - ownerGlobalRole && - (await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole?.id })); - + const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); if (!owner) { throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); } diff --git a/packages/cli/src/commands/user-management/reset.ts b/packages/cli/src/commands/user-management/reset.ts index ef4904c28d..188183e7d4 100644 --- a/packages/cli/src/commands/user-management/reset.ts +++ b/packages/cli/src/commands/user-management/reset.ts @@ -6,7 +6,6 @@ import { SettingsRepository } from '@db/repositories/settings.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { UserRepository } from '@db/repositories/user.repository'; -import { RoleService } from '@/services/role.service'; import { BaseCommand } from '../BaseCommand'; const defaultUserProps = { @@ -14,6 +13,7 @@ const defaultUserProps = { lastName: null, email: null, password: null, + role: 'global:owner', }; export class Reset extends BaseCommand { @@ -24,14 +24,8 @@ export class Reset extends BaseCommand { async run(): Promise { const owner = await this.getInstanceOwner(); - const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole(); - const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole(); - - await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner, workflowOwnerRole); - await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials( - owner, - credentialOwnerRole, - ); + await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner); + await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(owner); await Container.get(UserRepository).deleteAllExcept(owner); await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps)); @@ -45,7 +39,7 @@ export class Reset extends BaseCommand { Container.get(SharedCredentialsRepository).create({ credentials, user: owner, - role: credentialOwnerRole, + role: 'credential:owner', }), ); await Container.get(SharedCredentialsRepository).save(newSharedCredentials); @@ -59,19 +53,17 @@ export class Reset extends BaseCommand { } async getInstanceOwner(): Promise { - const globalRole = await Container.get(RoleService).findGlobalOwnerRole(); - - const owner = await Container.get(UserRepository).findOneBy({ globalRoleId: globalRole.id }); + const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); if (owner) return owner; const user = new User(); - Object.assign(user, { ...defaultUserProps, globalRole }); + Object.assign(user, defaultUserProps); await Container.get(UserRepository).save(user); - return await Container.get(UserRepository).findOneByOrFail({ globalRoleId: globalRole.id }); + return await Container.get(UserRepository).findOneByOrFail({ role: 'global:owner' }); } async catch(error: Error): Promise { diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index e0fce4c2f3..7ba13a2677 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -55,7 +55,7 @@ export class AuthController { const preliminaryUser = await handleEmailLogin(email, password); // if the user is an owner, continue with the login if ( - preliminaryUser?.globalRole?.name === 'owner' || + preliminaryUser?.role === 'global:owner' || preliminaryUser?.settings?.allowSSOManualLogin ) { user = preliminaryUser; @@ -65,7 +65,7 @@ export class AuthController { } } else if (isLdapCurrentAuthenticationMethod()) { const preliminaryUser = await handleEmailLogin(email, password); - if (preliminaryUser?.globalRole?.name === 'owner') { + if (preliminaryUser?.role === 'global:owner') { user = preliminaryUser; usedAuthenticationMethod = 'email'; } else { @@ -138,7 +138,7 @@ export class AuthController { } try { - user = await this.userRepository.findOneOrFail({ where: {}, relations: ['globalRole'] }); + user = await this.userRepository.findOneOrFail({ where: {} }); } catch (error) { throw new InternalServerError( 'No users found in database - did you wipe the users table? Create at least one user.', diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 1129641c0f..8e93d77726 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -1,8 +1,6 @@ import { Request } from 'express'; import { v4 as uuid } from 'uuid'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; -import { RoleRepository } from '@db/repositories/role.repository'; import { SettingsRepository } from '@db/repositories/settings.repository'; import { UserRepository } from '@db/repositories/user.repository'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -39,7 +37,6 @@ const tablesToTruncate = [ 'installed_packages', 'installed_nodes', 'user', - 'role', 'variables', ]; @@ -87,7 +84,6 @@ export class E2EController { constructor( license: License, - private readonly roleRepo: RoleRepository, private readonly settingsRepo: SettingsRepository, private readonly userRepo: UserRepository, private readonly workflowRunner: ActiveWorkflowRunner, @@ -148,7 +144,7 @@ export class E2EController { private async truncateAll() { for (const table of tablesToTruncate) { try { - const { connection } = this.roleRepo.manager; + const { connection } = this.settingsRepo.manager; await connection.query( `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, ); @@ -163,27 +159,12 @@ export class E2EController { members: UserSetupPayload[], admin: UserSetupPayload, ) { - const roles: Array<[Role['name'], Role['scope']]> = [ - ['owner', 'global'], - ['member', 'global'], - ['admin', 'global'], - ['owner', 'workflow'], - ['owner', 'credential'], - ['user', 'credential'], - ['editor', 'workflow'], - ]; - - const [{ id: globalOwnerRoleId }, { id: globalMemberRoleId }, { id: globalAdminRoleId }] = - await this.roleRepo.save( - roles.map(([name, scope], index) => ({ name, scope, id: (index + 1).toString() })), - ); - - const instanceOwner = { + const instanceOwner = this.userRepo.create({ id: uuid(), ...owner, password: await this.passwordUtility.hash(owner.password), - globalRoleId: globalOwnerRoleId, - }; + role: 'global:owner', + }); if (owner?.mfaSecret && owner.mfaRecoveryCodes?.length) { const { encryptedRecoveryCodes, encryptedSecret } = @@ -192,12 +173,12 @@ export class E2EController { instanceOwner.mfaRecoveryCodes = encryptedRecoveryCodes; } - const adminUser = { + const adminUser = this.userRepo.create({ id: uuid(), ...admin, password: await this.passwordUtility.hash(admin.password), - globalRoleId: globalAdminRoleId, - }; + role: 'global:admin', + }); const users = []; @@ -209,7 +190,7 @@ export class E2EController { id: uuid(), ...payload, password: await this.passwordUtility.hash(password), - globalRoleId: globalMemberRoleId, + role: 'global:member', }), ); } diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 864754e1f4..cba846965d 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -1,4 +1,5 @@ import { Response } from 'express'; +import validator from 'validator'; import config from '@/config'; import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators'; @@ -12,12 +13,11 @@ import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers'; import { PasswordUtility } from '@/services/password.utility'; import { PostHogClient } from '@/posthog'; import type { User } from '@/databases/entities/User'; -import validator from 'validator'; +import { UserRepository } from '@db/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { InternalHooks } from '@/InternalHooks'; import { ExternalHooks } from '@/ExternalHooks'; -import { UserRepository } from '@/databases/repositories/user.repository'; @Authorized() @RestController('/invitations') @@ -91,13 +91,13 @@ export class InvitationController { ); } - if (invite.role && !['member', 'admin'].includes(invite.role)) { + if (invite.role && !['global:member', 'global:admin'].includes(invite.role)) { throw new BadRequestError( - `Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'member' or 'admin'.`, + `Cannot invite user with invalid role: ${invite.role}. Please ensure all invitees' roles are either 'global:member' or 'global:admin'.`, ); } - if (invite.role === 'admin' && !this.license.isAdvancedPermissionsLicensed()) { + if (invite.role === 'global:admin' && !this.license.isAdvancedPermissionsLicensed()) { throw new UnauthorizedError( 'Cannot invite admin user without advanced permissions. Please upgrade to a license that includes this feature.', ); @@ -106,7 +106,7 @@ export class InvitationController { const attributes = req.body.map(({ email, role }) => ({ email, - role: role ?? 'member', + role: role ?? 'global:member', })); const { usersInvited, usersCreated } = await this.userService.inviteUsers(req.user, attributes); diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index a7b306d5df..bf7337de78 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -80,7 +80,6 @@ export class MeController { await this.userService.update(userId, payload); const user = await this.userRepository.findOneOrFail({ where: { id: userId }, - relations: ['globalRole'], }); this.logger.info('User updated successfully', { userId }); @@ -235,7 +234,6 @@ export class MeController { const user = await this.userRepository.findOneOrFail({ select: ['settings'], where: { id }, - relations: ['globalRole'], }); return user.settings; diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 24e3874138..fe0484021f 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -15,7 +15,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { InternalHooks } from '@/InternalHooks'; import { UserRepository } from '@/databases/repositories/user.repository'; -@Authorized(['global', 'owner']) +@Authorized('global:owner') @RestController('/owner') export class OwnerController { constructor( @@ -35,7 +35,7 @@ export class OwnerController { @Post('/setup') async setupOwner(req: OwnerRequest.Post, res: Response) { const { email, firstName, lastName, password } = req.body; - const { id: userId, globalRole } = req.user; + const { id: userId } = req.user; if (config.getEnv('userManagement.isInstanceOwnerSetUp')) { this.logger.debug( @@ -65,17 +65,6 @@ export class OwnerController { throw new BadRequestError('First and last names are mandatory'); } - // TODO: This check should be in a middleware outside this class - if (globalRole.scope === 'global' && globalRole.name !== 'owner') { - this.logger.debug( - 'Request to claim instance ownership failed because user shell does not exist or has wrong role!', - { - userId, - }, - ); - throw new BadRequestError('Invalid request'); - } - let owner = req.user; Object.assign(owner, { diff --git a/packages/cli/src/controllers/role.controller.ts b/packages/cli/src/controllers/role.controller.ts deleted file mode 100644 index 4b16ceb771..0000000000 --- a/packages/cli/src/controllers/role.controller.ts +++ /dev/null @@ -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 }; - }); - } -} diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 4709a22d14..2ed94e1345 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -23,7 +23,6 @@ import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials. import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { UserRepository } from '@db/repositories/user.repository'; import { plainToInstance } from 'class-transformer'; -import { RoleService } from '@/services/role.service'; import { UserService } from '@/services/user.service'; import { listQueryMiddleware } from '@/middlewares'; import { Logger } from '@/Logger'; @@ -45,7 +44,6 @@ export class UsersController { private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly userRepository: UserRepository, private readonly activeWorkflowRunner: ActiveWorkflowRunner, - private readonly roleService: RoleService, private readonly userService: UserService, ) {} @@ -70,7 +68,7 @@ export class UsersController { } if (filter?.isOwner) { - for (const user of publicUsers) delete user.globalRole; + for (const user of publicUsers) delete user.role; } // remove computed fields (unselectable) @@ -92,12 +90,7 @@ export class UsersController { async listUsers(req: ListQuery.Request) { const { listQueryOptions } = req; - const globalOwner = await this.roleService.findGlobalOwnerRole(); - - const findManyOptions = await this.userRepository.toFindManyOptions( - listQueryOptions, - globalOwner.id, - ); + const findManyOptions = await this.userRepository.toFindManyOptions(listQueryOptions); const users = await this.userRepository.find(findManyOptions); @@ -118,7 +111,6 @@ export class UsersController { async getUserPasswordResetLink(req: UserRequest.PasswordResetLink) { const user = await this.userRepository.findOneOrFail({ where: { id: req.params.id }, - relations: ['globalRole'], }); if (!user) { throw new NotFoundError('User not found'); @@ -140,7 +132,6 @@ export class UsersController { const user = await this.userRepository.findOneOrFail({ select: ['settings'], where: { id }, - relations: ['globalRole'], }); return user.settings; @@ -194,11 +185,6 @@ export class UsersController { telemetryData.migration_user_id = transferId; } - const [workflowOwnerRole, credentialOwnerRole] = await Promise.all([ - this.roleService.findWorkflowOwnerRole(), - this.roleService.findCredentialOwnerRole(), - ]); - if (transferId) { const transferee = users.find((user) => user.id === transferId); @@ -208,7 +194,7 @@ export class UsersController { .getRepository(SharedWorkflow) .find({ select: ['workflowId'], - where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id }, + where: { userId: userToDelete.id, role: 'workflow:owner' }, }) .then((sharedWorkflows) => sharedWorkflows.map(({ workflowId }) => workflowId)); @@ -223,7 +209,7 @@ export class UsersController { // Transfer ownership of owned workflows await transactionManager.update( SharedWorkflow, - { user: userToDelete, role: workflowOwnerRole }, + { user: userToDelete, role: 'workflow:owner' }, { user: transferee }, ); @@ -234,7 +220,7 @@ export class UsersController { .getRepository(SharedCredentials) .find({ select: ['credentialsId'], - where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id }, + where: { userId: userToDelete.id, role: 'credential:owner' }, }) .then((sharedCredentials) => sharedCredentials.map(({ credentialsId }) => credentialsId)); @@ -249,7 +235,7 @@ export class UsersController { // Transfer ownership of owned credentials await transactionManager.update( SharedCredentials, - { user: userToDelete, role: credentialOwnerRole }, + { user: userToDelete, role: 'credential:owner' }, { user: transferee }, ); @@ -271,11 +257,11 @@ export class UsersController { const [ownedSharedWorkflows, ownedSharedCredentials] = await Promise.all([ this.sharedWorkflowRepository.find({ relations: ['workflow'], - where: { userId: userToDelete.id, roleId: workflowOwnerRole?.id }, + where: { userId: userToDelete.id, role: 'workflow:owner' }, }), this.sharedCredentialsRepository.find({ relations: ['credentials'], - where: { userId: userToDelete.id, roleId: credentialOwnerRole?.id }, + where: { userId: userToDelete.id, role: 'credential:owner' }, }), ]); @@ -318,23 +304,20 @@ export class UsersController { const targetUser = await this.userRepository.findOne({ where: { id: req.params.id }, - relations: ['globalRole'], }); if (targetUser === null) { throw new NotFoundError(NO_USER); } - if (req.user.globalRole.name === 'admin' && targetUser.globalRole.name === 'owner') { + if (req.user.role === 'global:admin' && targetUser.role === 'global:owner') { throw new UnauthorizedError(NO_ADMIN_ON_OWNER); } - if (req.user.globalRole.name === 'owner' && targetUser.globalRole.name === 'owner') { + if (req.user.role === 'global:owner' && targetUser.role === 'global:owner') { throw new UnauthorizedError(NO_OWNER_ON_OWNER); } - const roleToSet = await this.roleService.findCached('global', payload.newRoleName); - - await this.userService.update(targetUser.id, { globalRoleId: roleToSet.id }); + await this.userService.update(targetUser.id, { role: payload.newRoleName }); void this.internalHooks.onUserRoleChange({ user: req.user, diff --git a/packages/cli/src/credentials/credentials.controller.ee.ts b/packages/cli/src/credentials/credentials.controller.ee.ts index 0df219c297..4a8211caf2 100644 --- a/packages/cli/src/credentials/credentials.controller.ee.ts +++ b/packages/cli/src/credentials/credentials.controller.ee.ts @@ -45,7 +45,7 @@ EECredentialsController.get( let credential = await Container.get(CredentialsRepository).findOne({ where: { id: credentialId }, - relations: ['shared', 'shared.role', 'shared.user'], + relations: ['shared', 'shared.user'], }); if (!credential) { @@ -62,7 +62,7 @@ EECredentialsController.get( credential = Container.get(OwnershipService).addOwnedByAndSharedWith(credential); - if (!includeDecryptedData || !userSharing || userSharing.role.name !== 'owner') { + if (!includeDecryptedData || !userSharing || userSharing.role !== 'credential:owner') { const { data: _, ...rest } = credential; return { ...rest }; } @@ -151,10 +151,9 @@ EECredentialsController.put( const ownerIds = ( await EECredentials.getSharings(Db.getConnection().createEntityManager(), credentialId, [ 'shared', - 'shared.role', ]) ) - .filter((e) => e.role.name === 'owner') + .filter((e) => e.role === 'credential:owner') .map((e) => e.userId); let amountRemoved: number | null = null; diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 1e9814b29a..d7406d496f 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -147,7 +147,7 @@ credentialsController.patch( allowGlobalScope: true, globalScope: 'credential:update', }, - ['credentials', 'role'], + ['credentials'], ); if (!sharing) { @@ -163,7 +163,7 @@ credentialsController.patch( ); } - if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:update')) { + if (sharing.role !== 'credential:owner' && !req.user.hasGlobalScope('credential:update')) { Container.get(Logger).info( 'Attempt to update credential blocked due to lack of permissions', { @@ -216,7 +216,7 @@ credentialsController.delete( allowGlobalScope: true, globalScope: 'credential:delete', }, - ['credentials', 'role'], + ['credentials'], ); if (!sharing) { @@ -232,7 +232,7 @@ credentialsController.delete( ); } - if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:delete')) { + if (sharing.role !== 'credential:owner' && !req.user.hasGlobalScope('credential:delete')) { Container.get(Logger).info( 'Attempt to delete credential blocked due to lack of permissions', { diff --git a/packages/cli/src/credentials/credentials.service.ee.ts b/packages/cli/src/credentials/credentials.service.ee.ts index 56ca50953a..9b31d3c4eb 100644 --- a/packages/cli/src/credentials/credentials.service.ee.ts +++ b/packages/cli/src/credentials/credentials.service.ee.ts @@ -1,10 +1,9 @@ +import { Container } from 'typedi'; import type { EntityManager, FindOptionsWhere } from 'typeorm'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { SharedCredentials } from '@db/entities/SharedCredentials'; import type { User } from '@db/entities/User'; import { CredentialsService, type CredentialsGetSharedOptions } from './credentials.service'; -import { RoleService } from '@/services/role.service'; -import Container from 'typedi'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; @@ -15,10 +14,9 @@ export class EECredentialsService extends CredentialsService { ): Promise<{ ownsCredential: boolean; credential?: CredentialsEntity }> { const sharing = await this.getSharing(user, credentialId, { allowGlobalScope: false }, [ 'credentials', - 'role', ]); - if (!sharing || sharing.role.name !== 'owner') return { ownsCredential: false }; + if (!sharing || sharing.role !== 'credential:owner') return { ownsCredential: false }; const { credentials: credential } = sharing; @@ -67,7 +65,6 @@ export class EECredentialsService extends CredentialsService { shareWithIds: string[], ): Promise { const users = await Container.get(UserRepository).getByIds(transaction, shareWithIds); - const role = await Container.get(RoleService).findCredentialUserRole(); const newSharedCredentials = users .filter((user) => !user.isPending) @@ -75,7 +72,7 @@ export class EECredentialsService extends CredentialsService { Container.get(SharedCredentialsRepository).create({ credentialsId: credential.id, userId: user.id, - roleId: role?.id, + role: 'credential:user', }), ); diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index f3f80e5a2b..9533b5a1ad 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -23,7 +23,6 @@ import { ExternalHooks } from '@/ExternalHooks'; import type { User } from '@db/entities/User'; import type { CredentialRequest, ListQuery } from '@/requests'; import { CredentialTypes } from '@/CredentialTypes'; -import { RoleService } from '@/services/role.service'; import { OwnershipService } from '@/services/ownership.service'; import { Logger } from '@/Logger'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; @@ -85,13 +84,8 @@ export class CredentialsService { // global credential permissions. This allows the user to // access credentials they don't own. if (!options.allowGlobalScope || !user.hasGlobalScope(options.globalScope)) { - Object.assign(where, { - userId: user.id, - role: { name: 'owner' }, - }); - if (!relations.includes('role')) { - relations.push('role'); - } + where.userId = user.id; + where.role = 'credential:owner'; } return await Container.get(SharedCredentialsRepository).findOne({ where, relations }); @@ -194,8 +188,6 @@ export class CredentialsService { await Container.get(ExternalHooks).run('credentials.create', [encryptedData]); - const role = await Container.get(RoleService).findCredentialOwnerRole(); - const result = await Db.transaction(async (transactionManager) => { const savedCredential = await transactionManager.save(newCredential); @@ -204,7 +196,7 @@ export class CredentialsService { const newSharedCredential = new SharedCredentials(); Object.assign(newSharedCredential, { - role, + role: 'credential:owner', user, credentials: savedCredential, }); diff --git a/packages/cli/src/databases/dsl/Table.ts b/packages/cli/src/databases/dsl/Table.ts index 5e5fa41f03..94ed4392d0 100644 --- a/packages/cli/src/databases/dsl/Table.ts +++ b/packages/cli/src/databases/dsl/Table.ts @@ -1,5 +1,5 @@ import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typeorm'; -import { Table, TableColumn } from 'typeorm'; +import { Table, TableColumn, TableForeignKey } from 'typeorm'; import LazyPromise from 'p-lazy'; import { Column } from './Column'; import { ApplicationError } from 'n8n-workflow'; @@ -118,6 +118,42 @@ export class DropColumns extends TableOperation { } } +abstract class ForeignKeyOperation extends TableOperation { + protected foreignKey: TableForeignKey; + + constructor( + tableName: string, + columnName: string, + [referencedTableName, referencedColumnName]: [string, string], + prefix: string, + queryRunner: QueryRunner, + customConstraintName?: string, + ) { + super(tableName, prefix, queryRunner); + + this.foreignKey = new TableForeignKey({ + name: customConstraintName, + columnNames: [columnName], + referencedTableName: `${prefix}${referencedTableName}`, + referencedColumnNames: [referencedColumnName], + }); + } +} + +export class AddForeignKey extends ForeignKeyOperation { + async execute(queryRunner: QueryRunner) { + const { tableName, prefix } = this; + return await queryRunner.createForeignKey(`${prefix}${tableName}`, this.foreignKey); + } +} + +export class DropForeignKey extends ForeignKeyOperation { + async execute(queryRunner: QueryRunner) { + const { tableName, prefix } = this; + return await queryRunner.dropForeignKey(`${prefix}${tableName}`, this.foreignKey); + } +} + class ModifyNotNull extends TableOperation { constructor( tableName: string, diff --git a/packages/cli/src/databases/dsl/index.ts b/packages/cli/src/databases/dsl/index.ts index 6eb6df8bc9..2e108c0ef7 100644 --- a/packages/cli/src/databases/dsl/index.ts +++ b/packages/cli/src/databases/dsl/index.ts @@ -1,6 +1,15 @@ import type { QueryRunner } from 'typeorm'; import { Column } from './Column'; -import { AddColumns, AddNotNull, CreateTable, DropColumns, DropNotNull, DropTable } from './Table'; +import { + AddColumns, + AddForeignKey, + AddNotNull, + CreateTable, + DropColumns, + DropForeignKey, + DropNotNull, + DropTable, +} from './Table'; import { CreateIndex, DropIndex } from './Indices'; export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunner) => ({ @@ -26,6 +35,36 @@ export const createSchemaBuilder = (tablePrefix: string, queryRunner: QueryRunne dropIndex: (tableName: string, columnNames: string[], customIndexName?: string) => new DropIndex(tableName, columnNames, tablePrefix, queryRunner, customIndexName), + addForeignKey: ( + tableName: string, + columnName: string, + reference: [string, string], + customConstraintName?: string, + ) => + new AddForeignKey( + tableName, + columnName, + reference, + tablePrefix, + queryRunner, + customConstraintName, + ), + + dropForeignKey: ( + tableName: string, + columnName: string, + reference: [string, string], + customConstraintName?: string, + ) => + new DropForeignKey( + tableName, + columnName, + reference, + tablePrefix, + queryRunner, + customConstraintName, + ), + addNotNull: (tableName: string, columnName: string) => new AddNotNull(tableName, columnName, tablePrefix, queryRunner), dropNotNull: (tableName: string, columnName: string) => diff --git a/packages/cli/src/databases/entities/Role.ts b/packages/cli/src/databases/entities/Role.ts deleted file mode 100644 index 070e8e6be8..0000000000 --- a/packages/cli/src/databases/entities/Role.ts +++ /dev/null @@ -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}`; - } -} diff --git a/packages/cli/src/databases/entities/SharedCredentials.ts b/packages/cli/src/databases/entities/SharedCredentials.ts index 6686e5a3c4..1685732004 100644 --- a/packages/cli/src/databases/entities/SharedCredentials.ts +++ b/packages/cli/src/databases/entities/SharedCredentials.ts @@ -1,16 +1,14 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; import { CredentialsEntity } from './CredentialsEntity'; import { User } from './User'; -import { Role } from './Role'; import { WithTimestamps } from './AbstractEntity'; +export type CredentialSharingRole = 'credential:owner' | 'credential:user'; + @Entity() export class SharedCredentials extends WithTimestamps { - @ManyToOne('Role', 'sharedCredentials', { nullable: false }) - role: Role; - @Column() - roleId: string; + role: CredentialSharingRole; @ManyToOne('User', 'sharedCredentials') user: User; diff --git a/packages/cli/src/databases/entities/SharedWorkflow.ts b/packages/cli/src/databases/entities/SharedWorkflow.ts index 4b181e5ab1..adb94beb5a 100644 --- a/packages/cli/src/databases/entities/SharedWorkflow.ts +++ b/packages/cli/src/databases/entities/SharedWorkflow.ts @@ -1,16 +1,14 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; import { WorkflowEntity } from './WorkflowEntity'; import { User } from './User'; -import { Role } from './Role'; import { WithTimestamps } from './AbstractEntity'; +export type WorkflowSharingRole = 'workflow:owner' | 'workflow:editor' | 'workflow:user'; + @Entity() export class SharedWorkflow extends WithTimestamps { - @ManyToOne('Role', 'sharedWorkflows', { nullable: false }) - role: Role; - @Column() - roleId: string; + role: WorkflowSharingRole; @ManyToOne('User', 'sharedWorkflows') user: User; diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 974315cc08..9a1a96d5f7 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -6,13 +6,11 @@ import { Entity, Index, OneToMany, - ManyToOne, PrimaryGeneratedColumn, BeforeInsert, } from 'typeorm'; import { IsEmail, IsString, Length } from 'class-validator'; import type { IUser, IUserSettings } from 'n8n-workflow'; -import { Role } from './Role'; import type { SharedWorkflow } from './SharedWorkflow'; import type { SharedCredentials } from './SharedCredentials'; import { NoXss } from '../utils/customValidators'; @@ -23,10 +21,13 @@ import type { AuthIdentity } from './AuthIdentity'; import { ownerPermissions, memberPermissions, adminPermissions } from '@/permissions/roles'; import { hasScope, type ScopeOptions, type Scope } from '@n8n/permissions'; -const STATIC_SCOPE_MAP: Record = { - owner: ownerPermissions, - member: memberPermissions, - admin: adminPermissions, +export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member'; +export type AssignableRole = Exclude; + +const STATIC_SCOPE_MAP: Record = { + 'global:owner': ownerPermissions, + 'global:member': memberPermissions, + 'global:admin': adminPermissions, }; @Entity() @@ -72,11 +73,8 @@ export class User extends WithTimestamps implements IUser { }) settings: IUserSettings | null; - @ManyToOne('Role', 'globalForUsers', { nullable: false }) - globalRole: Role; - @Column() - globalRoleId: string; + role: GlobalRole; @OneToMany('AuthIdentity', 'user') authIdentities: AuthIdentity[]; @@ -127,11 +125,11 @@ export class User extends WithTimestamps implements IUser { @AfterLoad() computeIsOwner(): void { - this.isOwner = this.globalRole?.name === 'owner'; + this.isOwner = this.role === 'global:owner'; } get globalScopes() { - return STATIC_SCOPE_MAP[this.globalRole?.name] ?? []; + return STATIC_SCOPE_MAP[this.role] ?? []; } hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): boolean { diff --git a/packages/cli/src/databases/entities/index.ts b/packages/cli/src/databases/entities/index.ts index 9fd3c0b72c..db1f5a5ce7 100644 --- a/packages/cli/src/databases/entities/index.ts +++ b/packages/cli/src/databases/entities/index.ts @@ -6,7 +6,6 @@ import { EventDestinations } from './EventDestinations'; import { ExecutionEntity } from './ExecutionEntity'; import { InstalledNodes } from './InstalledNodes'; import { InstalledPackages } from './InstalledPackages'; -import { Role } from './Role'; import { Settings } from './Settings'; import { SharedCredentials } from './SharedCredentials'; import { SharedWorkflow } from './SharedWorkflow'; @@ -29,7 +28,6 @@ export const entities = { ExecutionEntity, InstalledNodes, InstalledPackages, - Role, Settings, SharedCredentials, SharedWorkflow, diff --git a/packages/cli/src/databases/migrations/common/1705429061930-DropRoleMapping.ts b/packages/cli/src/databases/migrations/common/1705429061930-DropRoleMapping.ts new file mode 100644 index 0000000000..dd91e6f541 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1705429061930-DropRoleMapping.ts @@ -0,0 +1,127 @@ +import type { MigrationContext, ReversibleMigration } from '@db/types'; + +type Table = 'user' | 'shared_workflow' | 'shared_credentials'; + +const idColumns: Record = { + user: 'id', + shared_credentials: 'credentialsId', + shared_workflow: 'workflowId', +}; + +const roleScopes: Record = { + user: 'global', + shared_credentials: 'credential', + shared_workflow: 'workflow', +}; + +const foreignKeySuffixes: Record = { + 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']); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 66effc584b..8ce92158cf 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -51,6 +51,7 @@ import { ExecutionSoftDelete1693491613982 } from '../common/1693491613982-Execut import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata'; import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections'; import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole'; +import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -105,4 +106,5 @@ export const mysqlMigrations: Migration[] = [ AddWorkflowMetadata1695128658538, ModifyWorkflowHistoryNodesAndConnections1695829275184, AddGlobalAdminRole1700571993961, + DropRoleMapping1705429061930, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 6af140bd08..8ae3733976 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -50,6 +50,7 @@ import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWor import { MigrateToTimestampTz1694091729095 } from './1694091729095-MigrateToTimestampTz'; import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections'; import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole'; +import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -103,4 +104,5 @@ export const postgresMigrations: Migration[] = [ MigrateToTimestampTz1694091729095, ModifyWorkflowHistoryNodesAndConnections1695829275184, AddGlobalAdminRole1700571993961, + DropRoleMapping1705429061930, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1705429061930-DropRoleMapping.ts b/packages/cli/src/databases/migrations/sqlite/1705429061930-DropRoleMapping.ts new file mode 100644 index 0000000000..73f559b7aa --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1705429061930-DropRoleMapping.ts @@ -0,0 +1,5 @@ +import { DropRoleMapping1705429061930 as BaseMigration } from '../common/1705429061930-DropRoleMapping'; + +export class DropRoleMapping1705429061930 extends BaseMigration { + transaction = false as const; +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index d92a567bce..7db45788ac 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -48,6 +48,7 @@ import { ExecutionSoftDelete1693491613982 } from './1693491613982-ExecutionSoftD import { AddWorkflowMetadata1695128658538 } from '../common/1695128658538-AddWorkflowMetadata'; import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common/1695829275184-ModifyWorkflowHistoryNodesAndConnections'; import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole'; +import { DropRoleMapping1705429061930 } from './1705429061930-DropRoleMapping'; const sqliteMigrations: Migration[] = [ InitialMigration1588102412422, @@ -99,6 +100,7 @@ const sqliteMigrations: Migration[] = [ AddWorkflowMetadata1695128658538, ModifyWorkflowHistoryNodesAndConnections1695829275184, AddGlobalAdminRole1700571993961, + DropRoleMapping1705429061930, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/databases/repositories/credentials.repository.ts b/packages/cli/src/databases/repositories/credentials.repository.ts index c593410d46..4c6e75cfcd 100644 --- a/packages/cli/src/databases/repositories/credentials.repository.ts +++ b/packages/cli/src/databases/repositories/credentials.repository.ts @@ -45,7 +45,7 @@ export class CredentialsRepository extends Repository { type Select = Array; - const defaultRelations = ['shared', 'shared.role', 'shared.user']; + const defaultRelations = ['shared', 'shared.user']; const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt']; if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations }; @@ -81,7 +81,7 @@ export class CredentialsRepository extends Repository { const findManyOptions: FindManyOptions = { where: { id: In(ids) } }; if (withSharings) { - findManyOptions.relations = ['shared', 'shared.user', 'shared.role']; + findManyOptions.relations = ['shared', 'shared.user']; } return await this.find(findManyOptions); diff --git a/packages/cli/src/databases/repositories/role.repository.ts b/packages/cli/src/databases/repositories/role.repository.ts deleted file mode 100644 index 1785ace18c..0000000000 --- a/packages/cli/src/databases/repositories/role.repository.ts +++ /dev/null @@ -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 { - 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>((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)); - } -} diff --git a/packages/cli/src/databases/repositories/sharedCredentials.repository.ts b/packages/cli/src/databases/repositories/sharedCredentials.repository.ts index 98b00561ff..0a52f52153 100644 --- a/packages/cli/src/databases/repositories/sharedCredentials.repository.ts +++ b/packages/cli/src/databases/repositories/sharedCredentials.repository.ts @@ -1,9 +1,8 @@ import { Service } from 'typedi'; -import type { EntityManager, FindOptionsWhere } from 'typeorm'; +import type { EntityManager } from 'typeorm'; import { DataSource, In, Not, Repository } from 'typeorm'; import { SharedCredentials } from '../entities/SharedCredentials'; import type { User } from '../entities/User'; -import type { Role } from '../entities/Role'; @Service() export class SharedCredentialsRepository extends Repository { @@ -26,15 +25,15 @@ export class SharedCredentialsRepository extends Repository { async findByCredentialIds(credentialIds: string[]) { return await this.find({ - relations: ['credentials', 'role', 'user'], + relations: ['credentials', 'user'], where: { credentialsId: In(credentialIds), }, }); } - async makeOwnerOfAllCredentials(user: User, role: Role) { - return await this.update({ userId: Not(user.id), roleId: role.id }, { user }); + async makeOwnerOfAllCredentials(user: User) { + return await this.update({ userId: Not(user.id), role: 'credential:owner' }, { user }); } /** @@ -42,23 +41,22 @@ export class SharedCredentialsRepository extends Repository { */ async getAccessibleCredentials(userId: string) { const sharings = await this.find({ - relations: ['role'], where: { userId, - role: { name: In(['owner', 'user']), scope: 'credential' }, + role: In(['credential:owner', 'credential:user']), }, }); return sharings.map((s) => s.credentialsId); } - async findSharings(userIds: string[], roleId?: string) { - const where: FindOptionsWhere = { userId: In(userIds) }; - - // If credential sharing is not enabled, get only credentials owned by this user - if (roleId) where.roleId = roleId; - - return await this.find({ where }); + async findOwnedSharings(userIds: string[]) { + return await this.find({ + where: { + userId: In(userIds), + role: 'credential:owner', + }, + }); } async deleteByIds(transaction: EntityManager, sharedCredentialsIds: string[], user?: User) { diff --git a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts index 8ebcc043a5..e3d321cab4 100644 --- a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts +++ b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts @@ -1,10 +1,9 @@ import { Service } from 'typedi'; import { DataSource, Repository, In, Not } from 'typeorm'; -import type { EntityManager, FindOptionsSelect, FindOptionsWhere } from 'typeorm'; -import { SharedWorkflow } from '../entities/SharedWorkflow'; +import type { EntityManager, FindManyOptions, FindOptionsWhere } from 'typeorm'; +import { SharedWorkflow, type WorkflowSharingRole } from '../entities/SharedWorkflow'; import { type User } from '../entities/User'; import type { Scope } from '@n8n/permissions'; -import type { Role } from '../entities/Role'; import type { WorkflowEntity } from '../entities/WorkflowEntity'; @Service() @@ -35,22 +34,29 @@ export class SharedWorkflowRepository extends Repository { async findByWorkflowIds(workflowIds: string[]) { return await this.find({ - relations: ['role', 'user'], + relations: ['user'], where: { - role: { - name: 'owner', - scope: 'workflow', - }, + role: 'workflow:owner', workflowId: In(workflowIds), }, }); } + async findSharingRole( + userId: string, + workflowId: string, + ): Promise { + return await this.findOne({ + select: ['role'], + where: { workflowId, userId }, + }).then((shared) => shared?.role); + } + async findSharing( workflowId: string, user: User, scope: Scope, - { roles, extraRelations }: { roles?: string[]; extraRelations?: string[] } = {}, + { roles, extraRelations }: { roles?: WorkflowSharingRole[]; extraRelations?: string[] } = {}, ) { const where: FindOptionsWhere = { workflow: { id: workflowId }, @@ -61,18 +67,18 @@ export class SharedWorkflowRepository extends Repository { } if (roles) { - where.role = { name: In(roles) }; + where.role = In(roles); } - const relations = ['workflow', 'role']; + const relations = ['workflow']; if (extraRelations) relations.push(...extraRelations); return await this.findOne({ relations, where }); } - async makeOwnerOfAllWorkflows(user: User, role: Role) { - return await this.update({ userId: Not(user.id), roleId: role.id }, { user }); + async makeOwnerOfAllWorkflows(user: User) { + return await this.update({ userId: Not(user.id), role: 'workflow:owner' }, { user }); } async getSharing( @@ -102,14 +108,14 @@ export class SharedWorkflowRepository extends Repository { ): Promise { return await this.find({ where: { - ...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }), + ...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }), ...(options.workflowIds && { workflowId: In(options.workflowIds) }), }, ...(options.relations && { relations: options.relations }), }); } - async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[], roleId: string) { + async share(transaction: EntityManager, workflow: WorkflowEntity, users: User[]) { const newSharedWorkflows = users.reduce((acc, user) => { if (user.isPending) { return acc; @@ -117,7 +123,7 @@ export class SharedWorkflowRepository extends Repository { const entity: Partial = { workflowId: workflow.id, userId: user.id, - roleId, + role: 'workflow:editor', }; acc.push(this.create(entity)); return acc; @@ -126,12 +132,15 @@ export class SharedWorkflowRepository extends Repository { return await transaction.save(newSharedWorkflows); } - async findWithFields(workflowIds: string[], { fields }: { fields: string[] }) { + async findWithFields( + workflowIds: string[], + { select }: Pick, 'select'>, + ) { return await this.find({ where: { workflowId: In(workflowIds), }, - select: fields as FindOptionsSelect, + select, }); } diff --git a/packages/cli/src/databases/repositories/user.repository.ts b/packages/cli/src/databases/repositories/user.repository.ts index ea5465eb44..fd0039bb6c 100644 --- a/packages/cli/src/databases/repositories/user.repository.ts +++ b/packages/cli/src/databases/repositories/user.repository.ts @@ -1,9 +1,9 @@ import { Service } from 'typedi'; import type { EntityManager, FindManyOptions } from 'typeorm'; import { DataSource, In, IsNull, Not, Repository } from 'typeorm'; -import { User } from '../entities/User'; import type { ListQuery } from '@/requests'; +import { type GlobalRole, User } from '../entities/User'; @Service() export class UserRepository extends Repository { constructor(dataSource: DataSource) { @@ -13,7 +13,6 @@ export class UserRepository extends Repository { async findManyByIds(userIds: string[]) { return await this.find({ where: { id: In(userIds) }, - relations: ['globalRole'], }); } @@ -28,7 +27,6 @@ export class UserRepository extends Repository { async findManyByEmail(emails: string[]) { return await this.find({ where: { email: In(emails) }, - relations: ['globalRole'], select: ['email', 'password', 'id'], }); } @@ -43,15 +41,30 @@ export class UserRepository extends Repository { email, password: Not(IsNull()), }, - relations: ['authIdentities', 'globalRole'], + relations: ['authIdentities'], }); } - async toFindManyOptions(listQueryOptions?: ListQuery.Options, globalOwnerRoleId?: string) { + /** Counts the number of users in each role, e.g. `{ admin: 2, member: 6, owner: 1 }` */ + async countUsersByRole() { + const rows = (await this.createQueryBuilder() + .select(['role', 'COUNT(role) as count']) + .groupBy('role') + .execute()) as Array<{ role: GlobalRole; count: string }>; + return rows.reduce( + (acc, row) => { + acc[row.role] = parseInt(row.count, 10); + return acc; + }, + {} as Record, + ); + } + + async toFindManyOptions(listQueryOptions?: ListQuery.Options) { const findManyOptions: FindManyOptions = {}; if (!listQueryOptions) { - findManyOptions.relations = ['globalRole', 'authIdentities']; + findManyOptions.relations = ['authIdentities']; return findManyOptions; } @@ -62,7 +75,7 @@ export class UserRepository extends Repository { if (skip) findManyOptions.skip = skip; if (take && !select) { - findManyOptions.relations = ['globalRole', 'authIdentities']; + findManyOptions.relations = ['authIdentities']; } if (take && select && !select?.id) { @@ -74,11 +87,8 @@ export class UserRepository extends Repository { findManyOptions.where = otherFilters; - if (isOwner !== undefined && globalOwnerRoleId) { - findManyOptions.relations = ['globalRole']; - findManyOptions.where.globalRole = { - id: isOwner ? globalOwnerRoleId : Not(globalOwnerRoleId), - }; + if (isOwner !== undefined) { + findManyOptions.where.role = isOwner ? 'global:owner' : Not('global:owner'); } } diff --git a/packages/cli/src/databases/repositories/workflow.repository.ts b/packages/cli/src/databases/repositories/workflow.repository.ts index 084238fc0b..addec8802c 100644 --- a/packages/cli/src/databases/repositories/workflow.repository.ts +++ b/packages/cli/src/databases/repositories/workflow.repository.ts @@ -35,7 +35,7 @@ export class WorkflowRepository extends Repository { async getAllActive() { return await this.find({ where: { active: true }, - relations: ['shared', 'shared.user', 'shared.user.globalRole', 'shared.role'], + relations: ['shared', 'shared.user'], }); } @@ -50,7 +50,7 @@ export class WorkflowRepository extends Repository { async findById(workflowId: string) { return await this.findOne({ where: { id: workflowId }, - relations: ['shared', 'shared.user', 'shared.user.globalRole', 'shared.role'], + relations: ['shared', 'shared.user'], }); } @@ -135,7 +135,7 @@ export class WorkflowRepository extends Repository { createdAt: true, updatedAt: true, versionId: true, - shared: { userId: true, roleId: true }, + shared: { userId: true, role: true }, }; delete select?.ownedBy; // remove non-entity field, handled after query @@ -152,7 +152,7 @@ export class WorkflowRepository extends Repository { select.tags = { id: true, name: true }; } - if (isOwnedByIncluded) relations.push('shared', 'shared.role', 'shared.user'); + if (isOwnedByIncluded) relations.push('shared', 'shared.user'); if (typeof where.name === 'string' && where.name !== '') { where.name = Like(`%${where.name}%`); diff --git a/packages/cli/src/databases/repositories/workflowStatistics.repository.ts b/packages/cli/src/databases/repositories/workflowStatistics.repository.ts index b08a175b42..67601c00f1 100644 --- a/packages/cli/src/databases/repositories/workflowStatistics.repository.ts +++ b/packages/cli/src/databases/repositories/workflowStatistics.repository.ts @@ -5,7 +5,6 @@ import { StatisticsNames, WorkflowStatistics } from '../entities/WorkflowStatist import type { User } from '@/databases/entities/User'; import { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { SharedWorkflow } from '@/databases/entities/SharedWorkflow'; -import { Role } from '@/databases/entities/Role'; type StatisticsInsertResult = 'insert' | 'failed' | 'alreadyExists'; type StatisticsUpsertResult = StatisticsInsertResult | 'update'; @@ -110,12 +109,11 @@ export class WorkflowStatisticsRepository extends Repository 'shared_workflow', 'shared_workflow.workflowId = workflow_statistics.workflowId', ) - .innerJoin(Role, 'role', 'role.id = shared_workflow.roleId') .where('shared_workflow.userId = :userId', { userId }) .andWhere('workflow.active = :isActive', { isActive: true }) .andWhere('workflow_statistics.name = :name', { name: StatisticsNames.productionSuccess }) .andWhere('workflow_statistics.count >= 5') - .andWhere('role.name = :roleName', { roleName: 'owner' }) + .andWhere('role = :roleName', { roleName: 'workflow:owner' }) .getCount(); } } diff --git a/packages/cli/src/decorators/registerController.ts b/packages/cli/src/decorators/registerController.ts index bb13781445..f6abd260fa 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -36,9 +36,7 @@ export const createAuthMiddleware = if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' }); - const { globalRole } = user; - if (authRole === 'any' || (globalRole.scope === authRole[0] && globalRole.name === authRole[1])) - return next(); + if (authRole === 'any' || authRole === user.role) return next(); res.status(403).json({ status: 'error', message: 'Unauthorized' }); }; diff --git a/packages/cli/src/decorators/types.ts b/packages/cli/src/decorators/types.ts index 919e748686..bbaccf39ab 100644 --- a/packages/cli/src/decorators/types.ts +++ b/packages/cli/src/decorators/types.ts @@ -1,11 +1,11 @@ import type { Request, Response, RequestHandler } from 'express'; -import type { RoleNames, RoleScopes } from '@db/entities/Role'; +import type { GlobalRole } from '@db/entities/User'; import type { BooleanLicenseFeature } from '@/Interfaces'; import type { Scope } from '@n8n/permissions'; export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; -export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none'; +export type AuthRole = GlobalRole | 'any' | 'none'; export type AuthRoleMetadata = Record; export type LicenseMetadata = Record; diff --git a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts index 82b6a41c69..2fe8525996 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts @@ -23,12 +23,10 @@ import { isUniqueConstraintError } from '@/ResponseHelper'; import type { SourceControlWorkflowVersionId } from './types/sourceControlWorkflowVersionId'; import { getCredentialExportPath, getWorkflowExportPath } from './sourceControlHelper.ee'; import type { SourceControlledFile } from './types/sourceControlledFile'; -import { RoleService } from '@/services/role.service'; import { VariablesService } from '../variables/variables.service.ee'; import { TagRepository } from '@db/repositories/tag.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { UserRepository } from '@db/repositories/user.repository'; -import { UM_FIX_INSTRUCTION } from '@/constants'; import { Logger } from '@/Logger'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; @@ -59,36 +57,6 @@ export class SourceControlImportService { ); } - private async getOwnerGlobalRole() { - const globalOwnerRole = await Container.get(RoleService).findGlobalOwnerRole(); - - if (!globalOwnerRole) { - throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); - } - - return globalOwnerRole; - } - - private async getCredentialOwnerRole() { - const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole(); - - if (!credentialOwnerRole) { - throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`); - } - - return credentialOwnerRole; - } - - private async getWorkflowOwnerRole() { - const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole(); - - if (!workflowOwnerRole) { - throw new ApplicationError(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`); - } - - return workflowOwnerRole; - } - public async getRemoteVersionIdsFromFiles(): Promise { const remoteWorkflowFiles = await glob('*.json', { cwd: this.workflowExportFolder, @@ -222,7 +190,6 @@ export class SourceControlImportService { } public async importWorkflowFromWorkFolder(candidates: SourceControlledFile[], userId: string) { - const ownerWorkflowRole = await this.getWorkflowOwnerRole(); const workflowRunner = this.activeWorkflowRunner; const candidateIds = candidates.map((c) => c.id); const existingWorkflows = await Container.get(WorkflowRepository).findByIds(candidateIds, { @@ -230,7 +197,7 @@ export class SourceControlImportService { }); const allSharedWorkflows = await Container.get(SharedWorkflowRepository).findWithFields( candidateIds, - { fields: ['workflowId', 'roleId', 'userId'] }, + { select: ['workflowId', 'role', 'userId'] }, ); const cachedOwnerIds = new Map(); const importWorkflowsResult = await Promise.all( @@ -273,35 +240,29 @@ export class SourceControlImportService { } const existingSharedWorkflowOwnerByRoleId = allSharedWorkflows.find( - (e) => - e.workflowId === importedWorkflow.id && - e.roleId.toString() === ownerWorkflowRole.id.toString(), + (e) => e.workflowId === importedWorkflow.id && e.role === 'workflow:owner', ); const existingSharedWorkflowOwnerByUserId = allSharedWorkflows.find( - (e) => - e.workflowId === importedWorkflow.id && - e.roleId.toString() === workflowOwnerId.toString(), + (e) => e.workflowId === importedWorkflow.id && e.role === 'workflow:owner', ); if (!existingSharedWorkflowOwnerByUserId && !existingSharedWorkflowOwnerByRoleId) { // no owner exists yet, so create one await Container.get(SharedWorkflowRepository).insert({ workflowId: importedWorkflow.id, userId: workflowOwnerId, - roleId: ownerWorkflowRole.id, + role: 'workflow:owner', }); } else if (existingSharedWorkflowOwnerByRoleId) { // skip, because the workflow already has a global owner } else if (existingSharedWorkflowOwnerByUserId && !existingSharedWorkflowOwnerByRoleId) { - // if the worklflow has a non-global owner that is referenced by the owner file, + // if the workflow has a non-global owner that is referenced by the owner file, // and no existing global owner, update the owner to the user referenced in the owner file await Container.get(SharedWorkflowRepository).update( { workflowId: importedWorkflow.id, userId: workflowOwnerId, }, - { - roleId: ownerWorkflowRole.id, - }, + { role: 'workflow:owner' }, ); } if (existingWorkflow?.active) { @@ -343,13 +304,11 @@ export class SourceControlImportService { }, select: ['id', 'name', 'type', 'data'], }); - const ownerCredentialRole = await this.getCredentialOwnerRole(); - const ownerGlobalRole = await this.getOwnerGlobalRole(); const existingSharedCredentials = await Container.get(SharedCredentialsRepository).find({ - select: ['userId', 'credentialsId', 'roleId'], + select: ['userId', 'credentialsId', 'role'], where: { credentialsId: In(candidateIds), - roleId: In([ownerCredentialRole.id, ownerGlobalRole.id]), + role: 'credential:owner', }, }); let importCredentialsResult: Array<{ id: string; name: string; type: string }> = []; @@ -382,7 +341,7 @@ export class SourceControlImportService { const newSharedCredential = new SharedCredentials(); newSharedCredential.credentialsId = newCredentialObject.id as string; newSharedCredential.userId = userId; - newSharedCredential.roleId = ownerCredentialRole.id; + newSharedCredential.role = 'credential:owner'; await Container.get(SharedCredentialsRepository).upsert({ ...newSharedCredential }, [ 'credentialsId', diff --git a/packages/cli/src/executions/execution.service.ee.ts b/packages/cli/src/executions/execution.service.ee.ts index 7dd65c682f..29e2c5b8f3 100644 --- a/packages/cli/src/executions/execution.service.ee.ts +++ b/packages/cli/src/executions/execution.service.ee.ts @@ -22,7 +22,7 @@ export class EnterpriseExecutionsService { if (!execution) return; - const relations = ['shared', 'shared.user', 'shared.role']; + const relations = ['shared', 'shared.user']; const workflow = (await this.workflowRepository.get( { id: execution.workflowId }, diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index 5808b92d01..aa9c214800 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -27,7 +27,7 @@ export class ExecutionsController { private async getAccessibleWorkflowIds(user: User) { return this.license.isSharingEnabled() ? await this.workflowSharingService.getSharedWorkflowIds(user) - : await this.workflowSharingService.getSharedWorkflowIds(user, ['owner']); + : await this.workflowSharingService.getSharedWorkflowIds(user, ['workflow:owner']); } @Get('/') diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index ef89d276b3..27c2ebb110 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -14,8 +14,7 @@ import type { import { IsBoolean, IsEmail, IsIn, IsOptional, IsString, Length } from 'class-validator'; import { NoXss } from '@db/utils/customValidators'; import type { PublicUser, SecretsProvider, SecretsProviderState } from '@/Interfaces'; -import type { Role, RoleNames } from '@db/entities/Role'; -import type { User } from '@db/entities/User'; +import { AssignableRole, type User } from '@db/entities/User'; import type { UserManagementMailer } from '@/UserManagement/email'; import type { Variables } from '@db/entities/Variables'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; @@ -48,8 +47,8 @@ export class UserSettingsUpdatePayload { } export class UserRoleChangePayload { - @IsIn(['member', 'admin']) - newRoleName: Exclude; + @IsIn(['global:admin', 'global:member']) + newRoleName: AssignableRole; } export type AuthlessRequest< @@ -67,7 +66,6 @@ export type AuthenticatedRequest< > = Omit, 'user'> & { user: User; mailer?: UserManagementMailer; - globalMemberRole?: Role; }; // ---------------------------------- @@ -225,7 +223,7 @@ export declare namespace UserRequest { export type Invite = AuthenticatedRequest< {}, {}, - Array<{ email: string; role?: 'member' | 'admin' }> + Array<{ email: string; role?: AssignableRole }> >; export type InviteResponse = { diff --git a/packages/cli/src/services/import.service.ts b/packages/cli/src/services/import.service.ts index df9050e82a..32f6894f9b 100644 --- a/packages/cli/src/services/import.service.ts +++ b/packages/cli/src/services/import.service.ts @@ -4,16 +4,13 @@ import { type INode, type INodeCredentialsDetails } from 'n8n-workflow'; import { Logger } from '@/Logger'; import * as Db from '@/Db'; -import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { TagRepository } from '@/databases/repositories/tag.repository'; -import { SharedWorkflow } from '@/databases/entities/SharedWorkflow'; -import { RoleService } from '@/services/role.service'; +import { CredentialsRepository } from '@db/repositories/credentials.repository'; +import { TagRepository } from '@db/repositories/tag.repository'; +import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { replaceInvalidCredentials } from '@/WorkflowHelpers'; -import { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; -import { WorkflowTagMapping } from '@/databases/entities/WorkflowTagMapping'; - -import type { TagEntity } from '@/databases/entities/TagEntity'; -import type { Role } from '@/databases/entities/Role'; +import { WorkflowEntity } from '@db/entities/WorkflowEntity'; +import { WorkflowTagMapping } from '@db/entities/WorkflowTagMapping'; +import type { TagEntity } from '@db/entities/TagEntity'; import type { ICredentialsDb } from '@/Interfaces'; @Service() @@ -22,19 +19,15 @@ export class ImportService { private dbTags: TagEntity[] = []; - private workflowOwnerRole: Role; - constructor( private readonly logger: Logger, private readonly credentialsRepository: CredentialsRepository, private readonly tagRepository: TagRepository, - private readonly roleService: RoleService, ) {} async initRecords() { this.dbCredentials = await this.credentialsRepository.find(); this.dbTags = await this.tagRepository.find(); - this.workflowOwnerRole = await this.roleService.findWorkflowOwnerRole(); } async importWorkflows(workflows: WorkflowEntity[], userId: string) { @@ -64,7 +57,7 @@ export class ImportService { const workflowId = upsertResult.identifiers.at(0)?.id as string; - await tx.upsert(SharedWorkflow, { workflowId, userId, roleId: this.workflowOwnerRole.id }, [ + await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [ 'workflowId', 'userId', ]); diff --git a/packages/cli/src/services/ownership.service.ts b/packages/cli/src/services/ownership.service.ts index 2dfb4e29c3..10c8da6334 100644 --- a/packages/cli/src/services/ownership.service.ts +++ b/packages/cli/src/services/ownership.service.ts @@ -2,17 +2,14 @@ import { Service } from 'typedi'; import { CacheService } from '@/services/cache/cache.service'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import type { User } from '@db/entities/User'; -import { RoleService } from './role.service'; -import { UserRepository } from '@/databases/repositories/user.repository'; +import { UserRepository } from '@db/repositories/user.repository'; import type { ListQuery } from '@/requests'; -import { ApplicationError } from 'n8n-workflow'; @Service() export class OwnershipService { constructor( private cacheService: CacheService, private userRepository: UserRepository, - private roleService: RoleService, private sharedWorkflowRepository: SharedWorkflowRepository, ) {} @@ -27,13 +24,9 @@ export class OwnershipService { if (cachedValue) return this.userRepository.create(cachedValue); - const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole(); - - if (!workflowOwnerRole) throw new ApplicationError('Failed to find workflow owner role'); - const sharedWorkflow = await this.sharedWorkflowRepository.findOneOrFail({ - where: { workflowId, roleId: workflowOwnerRole.id }, - relations: ['user', 'user.globalRole'], + where: { workflowId, role: 'workflow:owner' }, + relations: ['user'], }); void this.cacheService.setHash('workflow-ownership', { [workflowId]: sharedWorkflow.user }); @@ -61,7 +54,7 @@ export class OwnershipService { shared?.forEach(({ user, role }) => { const { id, email, firstName, lastName } = user; - if (role.name === 'owner') { + if (role === 'credential:owner' || role === 'workflow:owner') { entity.ownedBy = { id, email, firstName, lastName }; } else { entity.sharedWith.push({ id, email, firstName, lastName }); @@ -72,11 +65,8 @@ export class OwnershipService { } async getInstanceOwner() { - const globalOwnerRole = await this.roleService.findGlobalOwnerRole(); - return await this.userRepository.findOneOrFail({ - where: { globalRoleId: globalOwnerRole.id }, - relations: ['globalRole'], + where: { role: 'global:owner' }, }); } } diff --git a/packages/cli/src/services/role.service.ts b/packages/cli/src/services/role.service.ts deleted file mode 100644 index 3deaeebfc2..0000000000 --- a/packages/cli/src/services/role.service.ts +++ /dev/null @@ -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; - } -} diff --git a/packages/cli/src/services/user.service.ts b/packages/cli/src/services/user.service.ts index 8a0b5e1772..6c753788fa 100644 --- a/packages/cli/src/services/user.service.ts +++ b/packages/cli/src/services/user.service.ts @@ -1,5 +1,5 @@ import { Container, Service } from 'typedi'; -import { User } from '@db/entities/User'; +import { type AssignableRole, User } from '@db/entities/User'; import type { IUserSettings } from 'n8n-workflow'; import { UserRepository } from '@db/repositories/user.repository'; import type { PublicUser } from '@/Interfaces'; @@ -10,7 +10,6 @@ import { Logger } from '@/Logger'; import { createPasswordSha } from '@/auth/jwt'; import { UserManagementMailer } from '@/UserManagement/email'; import { InternalHooks } from '@/InternalHooks'; -import { RoleService } from '@/services/role.service'; import { UrlService } from '@/services/url.service'; import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import type { UserRequest } from '@/requests'; @@ -23,7 +22,6 @@ export class UserService { private readonly userRepository: UserRepository, private readonly jwtService: JwtService, private readonly mailer: UserManagementMailer, - private readonly roleService: RoleService, private readonly urlService: UrlService, ) {} @@ -73,7 +71,7 @@ export class UserService { const user = await this.userRepository.findOne({ where: { id: decodedToken.sub }, - relations: ['authIdentities', 'globalRole'], + relations: ['authIdentities'], }); if (!user) { @@ -162,7 +160,7 @@ export class UserService { private async sendEmails( owner: User, toInviteUsers: { [key: string]: string }, - role: 'member' | 'admin', + role: AssignableRole, ) { const domain = this.urlService.getInstanceBaseUrl(); @@ -224,9 +222,7 @@ export class UserService { ); } - async inviteUsers(owner: User, attributes: Array<{ email: string; role: 'member' | 'admin' }>) { - const memberRole = await this.roleService.findGlobalMemberRole(); - const adminRole = await this.roleService.findGlobalAdminRole(); + async inviteUsers(owner: User, attributes: Array<{ email: string; role: AssignableRole }>) { const emails = attributes.map(({ email }) => email); const existingUsers = await this.userRepository.findManyByEmail(emails); @@ -250,10 +246,7 @@ export class UserService { async (transactionManager) => await Promise.all( toCreateUsers.map(async ({ email, role }) => { - const newUser = Object.assign(new User(), { - email, - globalRole: role === 'member' ? memberRole : adminRole, - }); + const newUser = transactionManager.create(User, { email, role }); const savedUser = await transactionManager.save(newUser); createdUsers.set(email, savedUser.id); return savedUser; diff --git a/packages/cli/src/services/userOnboarding.service.ts b/packages/cli/src/services/userOnboarding.service.ts index f1d23cf1bc..ab8dbb98c1 100644 --- a/packages/cli/src/services/userOnboarding.service.ts +++ b/packages/cli/src/services/userOnboarding.service.ts @@ -4,7 +4,6 @@ import { In } from 'typeorm'; import type { User } from '@db/entities/User'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import { RoleService } from '@/services/role.service'; import { UserService } from '@/services/user.service'; @Service() @@ -12,7 +11,6 @@ export class UserOnboardingService { constructor( private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly workflowRepository: WorkflowRepository, - private readonly roleService: RoleService, private readonly userService: UserService, ) {} @@ -24,12 +22,11 @@ export class UserOnboardingService { let belowThreshold = true; const skippedTypes = ['n8n-nodes-base.start', 'n8n-nodes-base.stickyNote']; - const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole(); const ownedWorkflowsIds = await this.sharedWorkflowRepository .find({ where: { userId: user.id, - roleId: workflowOwnerRole?.id, + role: 'workflow:owner', }, select: ['workflowId'], }) diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index 2a20c25c7d..b3e16861f1 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -174,7 +174,7 @@ export class SamlService { const lowerCasedEmail = attributes.email.toLowerCase(); const user = await Container.get(UserRepository).findOne({ where: { email: lowerCasedEmail }, - relations: ['globalRole', 'authIdentities'], + relations: ['authIdentities'], }); if (user) { // Login path for existing users that are fully set up and that have a SAML authIdentity set up diff --git a/packages/cli/src/sso/saml/samlHelpers.ts b/packages/cli/src/sso/saml/samlHelpers.ts index 87603231a1..799277eba8 100644 --- a/packages/cli/src/sso/saml/samlHelpers.ts +++ b/packages/cli/src/sso/saml/samlHelpers.ts @@ -17,7 +17,6 @@ import { } from '../ssoHelpers'; import { getServiceProviderConfigTestReturnUrl } from './serviceProvider.ee'; import type { SamlConfiguration } from './types/requests'; -import { RoleService } from '@/services/role.service'; import { UserRepository } from '@db/repositories/user.repository'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; @@ -104,7 +103,7 @@ export async function createUserFromSamlAttributes(attributes: SamlUserAttribute user.email = lowerCasedEmail; user.firstName = attributes.firstName; user.lastName = attributes.lastName; - user.globalRole = await Container.get(RoleService).findGlobalMemberRole(); + user.role = 'global:member'; // generates a password that is not used or known to the user user.password = await Container.get(PasswordUtility).hash(generatePassword()); authIdentity.providerId = attributes.userPrincipalName; diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index 1ead60a225..373fc20150 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -11,7 +11,7 @@ import { License } from '@/License'; import { N8N_VERSION } from '@/constants'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { SourceControlPreferencesService } from '../environments/sourceControl/sourceControlPreferences.service.ee'; -import { RoleRepository } from '@/databases/repositories/role.repository'; +import { UserRepository } from '@db/repositories/user.repository'; type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success'; @@ -111,7 +111,7 @@ export class Telemetry { plan_name_current: this.license.getPlanName(), quota: this.license.getTriggerLimit(), usage: await this.workflowRepository.getActiveTriggerCount(), - role_count: await Container.get(RoleRepository).countUsersByRole(), + role_count: await Container.get(UserRepository).countUsersByRole(), source_control_set_up: Container.get(SourceControlPreferencesService).isSourceControlSetup(), branchName: sourceControlPreferences.branchName, read_only_instance: sourceControlPreferences.branchReadOnly, diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index 161e580a83..5c96808451 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -34,10 +34,10 @@ export class EnterpriseWorkflowService { user, workflowId, { allowGlobalScope: false }, - ['workflow', 'role'], + ['workflow'], ); - if (!sharing || sharing.role.name !== 'owner') return { ownsWorkflow: false }; + if (!sharing || sharing.role !== 'workflow:owner') return { ownsWorkflow: false }; const { workflow } = sharing; @@ -54,7 +54,7 @@ export class EnterpriseWorkflowService { workflow.shared?.forEach(({ user, role }) => { const { id, email, firstName, lastName } = user; - if (role.name === 'owner') { + if (role === 'workflow:owner') { workflow.ownedBy = { id, email, firstName, lastName }; return; } @@ -101,7 +101,7 @@ export class EnterpriseWorkflowService { }; credential.shared?.forEach(({ user, role }) => { const { id, email, firstName, lastName } = user; - if (role.name === 'owner') { + if (role === 'credential:owner') { workflowCredential.ownedBy = { id, email, firstName, lastName }; } else { workflowCredential.sharedWith?.push({ id, email, firstName, lastName }); diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index ddae49cc3e..3833510b75 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -8,6 +8,7 @@ import { BinaryDataService } from 'n8n-core'; import config from '@/config'; import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; +import type { WorkflowSharingRole } from '@db/entities/SharedWorkflow'; import { ExecutionRepository } from '@db/repositories/execution.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository'; @@ -60,7 +61,7 @@ export class WorkflowService { workflowId: string, tagIds?: string[], forceSave?: boolean, - roles?: string[], + roles?: WorkflowSharingRole[], ): Promise { const shared = await this.sharedWorkflowRepository.findSharing( workflowId, @@ -250,7 +251,7 @@ export class WorkflowService { workflowId, user, 'workflow:delete', - { roles: ['owner'] }, + { roles: ['workflow:owner'] }, ); if (!sharedWorkflow) { diff --git a/packages/cli/src/workflows/workflowSharing.service.ts b/packages/cli/src/workflows/workflowSharing.service.ts index a322b2c327..78f325c719 100644 --- a/packages/cli/src/workflows/workflowSharing.service.ts +++ b/packages/cli/src/workflows/workflowSharing.service.ts @@ -1,31 +1,25 @@ import { Service } from 'typedi'; import { In, type FindOptionsWhere } from 'typeorm'; -import type { RoleNames } from '@db/entities/Role'; -import type { SharedWorkflow } from '@db/entities/SharedWorkflow'; +import type { SharedWorkflow, WorkflowSharingRole } from '@db/entities/SharedWorkflow'; import type { User } from '@db/entities/User'; -import { RoleRepository } from '@db/repositories/role.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; @Service() export class WorkflowSharingService { - constructor( - private readonly roleRepository: RoleRepository, - private readonly sharedWorkflowRepository: SharedWorkflowRepository, - ) {} + constructor(private readonly sharedWorkflowRepository: SharedWorkflowRepository) {} /** * Get the IDs of the workflows that have been shared with the user. * Returns all IDs if user has the 'workflow:read' scope. */ - async getSharedWorkflowIds(user: User, roleNames?: RoleNames[]): Promise { + async getSharedWorkflowIds(user: User, roles?: WorkflowSharingRole[]): Promise { const where: FindOptionsWhere = {}; if (!user.hasGlobalScope('workflow:read')) { where.userId = user.id; } - if (roleNames?.length) { - const roleIds = await this.roleRepository.getIdsInScopeWorkflowByNames(roleNames); - where.roleId = In(roleIds); + if (roles?.length) { + where.role = In(roles); } const sharedWorkflows = await this.sharedWorkflowRepository.find({ where, diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index a21df79ca5..912457760a 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -10,8 +10,7 @@ import * as WorkflowHelpers from '@/WorkflowHelpers'; import type { IWorkflowResponse } from '@/Interfaces'; import config from '@/config'; import { Authorized, Delete, Get, Patch, Post, Put, RestController } from '@/decorators'; -import type { RoleNames } from '@db/entities/Role'; -import { SharedWorkflow } from '@db/entities/SharedWorkflow'; +import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { TagRepository } from '@db/repositories/tag.repository'; @@ -23,7 +22,6 @@ import { ListQuery } from '@/requests'; import { WorkflowService } from './workflow.service'; import { License } from '@/License'; import { InternalHooks } from '@/InternalHooks'; -import { RoleService } from '@/services/role.service'; import * as utils from '@/utils'; import { listQueryMiddleware } from '@/middlewares'; import { TagService } from '@/services/tag.service'; @@ -53,7 +51,6 @@ export class WorkflowsController { private readonly externalHooks: ExternalHooks, private readonly tagRepository: TagRepository, private readonly enterpriseWorkflowService: EnterpriseWorkflowService, - private readonly roleService: RoleService, private readonly workflowHistoryService: WorkflowHistoryService, private readonly tagService: TagService, private readonly namingService: NamingService, @@ -116,12 +113,10 @@ export class WorkflowsController { await Db.transaction(async (transactionManager) => { savedWorkflow = await transactionManager.save(newWorkflow); - const role = await this.roleService.findWorkflowOwnerRole(); - const newSharedWorkflow = new SharedWorkflow(); Object.assign(newSharedWorkflow, { - role, + role: 'workflow:owner', user: req.user, workflow: savedWorkflow, }); @@ -151,7 +146,9 @@ export class WorkflowsController { @Get('/', { middlewares: listQueryMiddleware }) async getAll(req: ListQuery.Request, res: express.Response) { try { - const roles: RoleNames[] = this.license.isSharingEnabled() ? [] : ['owner']; + const roles: WorkflowSharingRole[] = this.license.isSharingEnabled() + ? [] + : ['workflow:owner']; const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds( req.user, roles, @@ -223,7 +220,7 @@ export class WorkflowsController { const { id: workflowId } = req.params; if (this.license.isSharingEnabled()) { - const relations = ['shared', 'shared.user', 'shared.role']; + const relations = ['shared', 'shared.user']; if (!config.getEnv('workflowTagsDisabled')) { relations.push('tags'); } @@ -281,7 +278,8 @@ export class WorkflowsController { const { tags, ...rest } = req.body; Object.assign(updateData, rest); - if (this.license.isSharingEnabled()) { + const isSharingEnabled = this.license.isSharingEnabled(); + if (isSharingEnabled) { updateData = await this.enterpriseWorkflowService.preventTampering( updateData, workflowId, @@ -294,8 +292,8 @@ export class WorkflowsController { updateData, workflowId, tags, - this.license.isSharingEnabled() ? forceSave : true, - this.license.isSharingEnabled() ? undefined : ['owner'], + isSharingEnabled ? forceSave : true, + isSharingEnabled ? undefined : ['workflow:owner'], ); return updatedWorkflow; @@ -378,10 +376,10 @@ export class WorkflowsController { await this.workflowRepository.getSharings( Db.getConnection().createEntityManager(), workflowId, - ['shared', 'shared.role'], + ['shared'], ) ) - .filter((e) => e.role.name === 'owner') + .filter((e) => e.role === 'workflow:owner') .map((e) => e.userId); let newShareeIds: string[] = []; @@ -399,9 +397,7 @@ export class WorkflowsController { if (newShareeIds.length) { const users = await this.userRepository.getByIds(trx, newShareeIds); - const role = await this.roleService.findWorkflowEditorRole(); - - await this.sharedWorkflowRepository.share(trx, workflow!, users, role.id); + await this.sharedWorkflowRepository.share(trx, workflow!, users); } }); diff --git a/packages/cli/test/integration/auth.api.test.ts b/packages/cli/test/integration/auth.api.test.ts index ab3f3a8dd3..768acc467d 100644 --- a/packages/cli/test/integration/auth.api.test.ts +++ b/packages/cli/test/integration/auth.api.test.ts @@ -3,19 +3,15 @@ import { Container } from 'typedi'; import validator from 'validator'; import config from '@/config'; import { AUTH_COOKIE_NAME } from '@/constants'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { randomValidPassword } from './shared/random'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; -import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles'; import { createUser, createUserShell } from './shared/db/users'; import { UserRepository } from '@db/repositories/user.repository'; import { MfaService } from '@/Mfa/mfa.service'; -let globalOwnerRole: Role; -let globalMemberRole: Role; let owner: User; let authOwnerAgent: SuperAgentTest; const ownerPassword = randomValidPassword(); @@ -26,8 +22,6 @@ const license = testServer.license; let mfaService: MfaService; beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); mfaService = Container.get(MfaService); }); @@ -41,7 +35,7 @@ describe('POST /login', () => { beforeEach(async () => { owner = await createUser({ password: ownerPassword, - globalRole: globalOwnerRole, + role: 'global:owner', }); }); @@ -60,7 +54,7 @@ describe('POST /login', () => { lastName, password, personalizationAnswers, - globalRole, + role, apiKey, globalScopes, mfaSecret, @@ -74,9 +68,7 @@ describe('POST /login', () => { expect(password).toBeUndefined(); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); expect(globalScopes).toBeDefined(); expect(mfaRecoveryCodes).toBeUndefined(); @@ -107,7 +99,7 @@ describe('POST /login', () => { lastName, password, personalizationAnswers, - globalRole, + role, apiKey, mfaRecoveryCodes, mfaSecret, @@ -120,9 +112,7 @@ describe('POST /login', () => { expect(password).toBeUndefined(); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); expect(mfaRecoveryCodes).toBeUndefined(); expect(mfaSecret).toBeUndefined(); @@ -149,7 +139,7 @@ describe('POST /login', () => { license.setQuota('quota:users', 0); const ownerUser = await createUser({ password: randomValidPassword(), - globalRole: globalOwnerRole, + role: 'global:owner', }); const response = await testServer.authAgentFor(ownerUser).get('/login'); @@ -168,7 +158,7 @@ describe('GET /login', () => { }); test('should return cookie if UM is disabled and no cookie is already set', async () => { - await createUserShell(globalOwnerRole); + await createUserShell('global:owner'); await utils.setInstanceOwnerSetUp(false); const response = await testServer.authlessAgent.get('/login'); @@ -191,7 +181,7 @@ describe('GET /login', () => { }); test('should return logged-in owner shell', async () => { - const ownerShell = await createUserShell(globalOwnerRole); + const ownerShell = await createUserShell('global:owner'); const response = await testServer.authAgentFor(ownerShell).get('/login'); @@ -204,7 +194,7 @@ describe('GET /login', () => { lastName, password, personalizationAnswers, - globalRole, + role, apiKey, globalScopes, } = response.body.data; @@ -216,9 +206,7 @@ describe('GET /login', () => { expect(password).toBeUndefined(); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); expect(globalScopes).toBeDefined(); expect(globalScopes).toContain('workflow:read'); @@ -228,7 +216,7 @@ describe('GET /login', () => { }); test('should return logged-in member shell', async () => { - const memberShell = await createUserShell(globalMemberRole); + const memberShell = await createUserShell('global:member'); const response = await testServer.authAgentFor(memberShell).get('/login'); @@ -241,7 +229,7 @@ describe('GET /login', () => { lastName, password, personalizationAnswers, - globalRole, + role, apiKey, globalScopes, } = response.body.data; @@ -253,9 +241,7 @@ describe('GET /login', () => { expect(password).toBeUndefined(); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('member'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:member'); expect(apiKey).toBeUndefined(); expect(globalScopes).toBeDefined(); expect(globalScopes).not.toContain('workflow:read'); @@ -265,7 +251,7 @@ describe('GET /login', () => { }); test('should return logged-in owner', async () => { - const owner = await createUser({ globalRole: globalOwnerRole }); + const owner = await createUser({ role: 'global:owner' }); const response = await testServer.authAgentFor(owner).get('/login'); @@ -278,7 +264,7 @@ describe('GET /login', () => { lastName, password, personalizationAnswers, - globalRole, + role, apiKey, globalScopes, } = response.body.data; @@ -290,9 +276,7 @@ describe('GET /login', () => { expect(password).toBeUndefined(); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); expect(globalScopes).toBeDefined(); expect(globalScopes).toContain('workflow:read'); @@ -302,7 +286,7 @@ describe('GET /login', () => { }); test('should return logged-in member', async () => { - const member = await createUser({ globalRole: globalMemberRole }); + const member = await createUser({ role: 'global:member' }); const response = await testServer.authAgentFor(member).get('/login'); @@ -315,7 +299,7 @@ describe('GET /login', () => { lastName, password, personalizationAnswers, - globalRole, + role, apiKey, globalScopes, } = response.body.data; @@ -327,9 +311,7 @@ describe('GET /login', () => { expect(password).toBeUndefined(); expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); - expect(globalRole).toBeDefined(); - expect(globalRole.name).toBe('member'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:member'); expect(apiKey).toBeUndefined(); expect(globalScopes).toBeDefined(); expect(globalScopes).not.toContain('workflow:read'); @@ -343,13 +325,13 @@ describe('GET /resolve-signup-token', () => { beforeEach(async () => { owner = await createUser({ password: ownerPassword, - globalRole: globalOwnerRole, + role: 'global:owner', }); authOwnerAgent = testServer.authAgentFor(owner); }); test('should validate invite token', async () => { - const memberShell = await createUserShell(globalMemberRole); + const memberShell = await createUserShell('global:member'); const response = await authOwnerAgent .get('/resolve-signup-token') @@ -369,7 +351,7 @@ describe('GET /resolve-signup-token', () => { test('should return 403 if user quota reached', async () => { license.setQuota('quota:users', 0); - const memberShell = await createUserShell(globalMemberRole); + const memberShell = await createUserShell('global:member'); const response = await authOwnerAgent .get('/resolve-signup-token') @@ -380,7 +362,7 @@ describe('GET /resolve-signup-token', () => { }); test('should fail with invalid inputs', async () => { - const { id: inviteeId } = await createUser({ globalRole: globalMemberRole }); + const { id: inviteeId } = await createUser({ role: 'global:member' }); const first = await authOwnerAgent.get('/resolve-signup-token').query({ inviterId: owner.id }); @@ -412,7 +394,7 @@ describe('GET /resolve-signup-token', () => { describe('POST /logout', () => { test('should log user out', async () => { - const owner = await createUser({ globalRole: globalOwnerRole }); + const owner = await createUser({ role: 'global:owner' }); const response = await testServer.authAgentFor(owner).post('/logout'); diff --git a/packages/cli/test/integration/auth.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index f958a630d8..7a117e95be 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -1,8 +1,8 @@ +import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; + import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils/'; -import { getGlobalMemberRole } from './shared/db/roles'; import { createUser } from './shared/db/users'; -import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { mockInstance } from '../shared/mocking'; describe('Auth Middleware', () => { @@ -42,8 +42,7 @@ describe('Auth Middleware', () => { describe('Routes requiring Authorization', () => { let authMemberAgent: SuperAgentTest; beforeAll(async () => { - const globalMemberRole = await getGlobalMemberRole(); - const member = await createUser({ globalRole: globalMemberRole }); + const member = await createUser({ role: 'global:member' }); authMemberAgent = testServer.authAgentFor(member); }); diff --git a/packages/cli/test/integration/commands/reset.cmd.test.ts b/packages/cli/test/integration/commands/reset.cmd.test.ts index 6ffc229bb7..fd32fee1fc 100644 --- a/packages/cli/test/integration/commands/reset.cmd.test.ts +++ b/packages/cli/test/integration/commands/reset.cmd.test.ts @@ -1,5 +1,4 @@ import { Reset } from '@/commands/user-management/reset'; -import type { Role } from '@db/entities/Role'; import { InternalHooks } from '@/InternalHooks'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { NodeTypes } from '@/NodeTypes'; @@ -8,18 +7,13 @@ import { UserRepository } from '@db/repositories/user.repository'; import { mockInstance } from '../../shared/mocking'; import * as testDb from '../shared/testDb'; -import { getGlobalOwnerRole } from '../shared/db/roles'; import { createUser } from '../shared/db/users'; -let globalOwnerRole: Role; - beforeAll(async () => { mockInstance(InternalHooks); mockInstance(LoadNodesAndCredentials); mockInstance(NodeTypes); await testDb.init(); - - globalOwnerRole = await getGlobalOwnerRole(); }); beforeEach(async () => { @@ -32,11 +26,11 @@ afterAll(async () => { // eslint-disable-next-line n8n-local-rules/no-skipped-tests test.skip('user-management:reset should reset DB to default user state', async () => { - await createUser({ globalRole: globalOwnerRole }); + await createUser({ role: 'global:owner' }); await Reset.run(); - const user = await Container.get(UserRepository).findOneBy({ globalRoleId: globalOwnerRole.id }); + const user = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); if (!user) { fail('No owner found after DB reset to default user state'); diff --git a/packages/cli/test/integration/commands/worker.cmd.test.ts b/packages/cli/test/integration/commands/worker.cmd.test.ts index 604b1b297d..4bbb5c5ecb 100644 --- a/packages/cli/test/integration/commands/worker.cmd.test.ts +++ b/packages/cli/test/integration/commands/worker.cmd.test.ts @@ -18,6 +18,7 @@ import { OrchestrationHandlerWorkerService } from '@/services/orchestration/work import { OrchestrationWorkerService } from '@/services/orchestration/worker/orchestration.worker.service'; import { OrchestrationService } from '@/services/orchestration.service'; +import * as testDb from '../shared/testDb'; import { mockInstance } from '../../shared/mocking'; const oclifConfig = new Config({ root: __dirname }); @@ -39,6 +40,11 @@ beforeAll(async () => { mockInstance(RedisServicePubSubPublisher); mockInstance(RedisServicePubSubSubscriber); mockInstance(OrchestrationService); + await testDb.init(); +}); + +afterAll(async () => { + await testDb.terminate(); }); test('worker initializes all its components', async () => { diff --git a/packages/cli/test/integration/credentials.controller.test.ts b/packages/cli/test/integration/credentials.controller.test.ts index 3c122f2b4e..806d30eb95 100644 --- a/packages/cli/test/integration/credentials.controller.test.ts +++ b/packages/cli/test/integration/credentials.controller.test.ts @@ -5,7 +5,6 @@ import { setupTestServer } from './shared/utils/'; import { randomCredentialPayload as payload } from './shared/random'; import { saveCredential } from './shared/db/credentials'; import { createMember, createOwner } from './shared/db/users'; -import { getCredentialOwnerRole } from './shared/db/roles'; const { any } = expect; @@ -26,10 +25,14 @@ type GetAllResponse = { body: { data: ListQuery.Credentials.WithOwnedByAndShared describe('GET /credentials', () => { describe('should return', () => { test('all credentials for owner', async () => { - const role = await getCredentialOwnerRole(); - - const { id: id1 } = await saveCredential(payload(), { user: owner, role }); - const { id: id2 } = await saveCredential(payload(), { user: member, role }); + const { id: id1 } = await saveCredential(payload(), { + user: owner, + role: 'credential:owner', + }); + const { id: id2 } = await saveCredential(payload(), { + user: member, + role: 'credential:owner', + }); const response: GetAllResponse = await testServer .authAgentFor(owner) @@ -47,13 +50,11 @@ describe('GET /credentials', () => { }); test('only own credentials for member', async () => { - const role = await getCredentialOwnerRole(); - const firstMember = member; const secondMember = await createMember(); - const c1 = await saveCredential(payload(), { user: firstMember, role }); - const c2 = await saveCredential(payload(), { user: secondMember, role }); + const c1 = await saveCredential(payload(), { user: firstMember, role: 'credential:owner' }); + const c2 = await saveCredential(payload(), { user: secondMember, role: 'credential:owner' }); const response: GetAllResponse = await testServer .authAgentFor(firstMember) @@ -72,8 +73,7 @@ describe('GET /credentials', () => { describe('filter', () => { test('should filter credentials by field: name - full match', async () => { - const role = await getCredentialOwnerRole(); - const savedCred = await saveCredential(payload(), { user: owner, role }); + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response: GetAllResponse = await testServer .authAgentFor(owner) @@ -97,8 +97,7 @@ describe('GET /credentials', () => { }); test('should filter credentials by field: name - partial match', async () => { - const role = await getCredentialOwnerRole(); - const savedCred = await saveCredential(payload(), { user: owner, role }); + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const partialName = savedCred.name.slice(3); @@ -124,9 +123,7 @@ describe('GET /credentials', () => { }); test('should filter credentials by field: type - full match', async () => { - const role = await getCredentialOwnerRole(); - - const savedCred = await saveCredential(payload(), { user: owner, role }); + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response: GetAllResponse = await testServer .authAgentFor(owner) @@ -150,9 +147,7 @@ describe('GET /credentials', () => { }); test('should filter credentials by field: type - partial match', async () => { - const role = await getCredentialOwnerRole(); - - const savedCred = await saveCredential(payload(), { user: owner, role }); + const savedCred = await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const partialType = savedCred.type.slice(3); @@ -180,10 +175,8 @@ describe('GET /credentials', () => { describe('select', () => { test('should select credential field: id', async () => { - const role = await getCredentialOwnerRole(); - - await saveCredential(payload(), { user: owner, role }); - await saveCredential(payload(), { user: owner, role }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response: GetAllResponse = await testServer .authAgentFor(owner) @@ -197,10 +190,8 @@ describe('GET /credentials', () => { }); test('should select credential field: name', async () => { - const role = await getCredentialOwnerRole(); - - await saveCredential(payload(), { user: owner, role }); - await saveCredential(payload(), { user: owner, role }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response: GetAllResponse = await testServer .authAgentFor(owner) @@ -214,10 +205,8 @@ describe('GET /credentials', () => { }); test('should select credential field: type', async () => { - const role = await getCredentialOwnerRole(); - - await saveCredential(payload(), { user: owner, role }); - await saveCredential(payload(), { user: owner, role }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response: GetAllResponse = await testServer .authAgentFor(owner) @@ -233,10 +222,8 @@ describe('GET /credentials', () => { describe('take', () => { test('should return n credentials or less, without skip', async () => { - const role = await getCredentialOwnerRole(); - - await saveCredential(payload(), { user: owner, role }); - await saveCredential(payload(), { user: owner, role }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response = await testServer .authAgentFor(owner) @@ -260,10 +247,8 @@ describe('GET /credentials', () => { }); test('should return n credentials or less, with skip', async () => { - const role = await getCredentialOwnerRole(); - - await saveCredential(payload(), { user: owner, role }); - await saveCredential(payload(), { user: owner, role }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); + await saveCredential(payload(), { user: owner, role: 'credential:owner' }); const response = await testServer .authAgentFor(owner) diff --git a/packages/cli/test/integration/credentials.ee.test.ts b/packages/cli/test/integration/credentials.ee.test.ts index 146fe2f4a5..056d0f5329 100644 --- a/packages/cli/test/integration/credentials.ee.test.ts +++ b/packages/cli/test/integration/credentials.ee.test.ts @@ -1,28 +1,26 @@ +import { Container } from 'typedi'; import type { SuperAgentTest } from 'supertest'; import { In } from 'typeorm'; import type { IUser } from 'n8n-workflow'; import type { ListQuery } from '@/requests'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; +import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; +import { License } from '@/License'; import { randomCredentialPayload } from './shared/random'; import * as testDb from './shared/testDb'; import type { SaveCredentialFunction } from './shared/types'; import * as utils from './shared/utils/'; import { affixRoleToSaveCredential, shareCredentialWithUsers } from './shared/db/credentials'; -import { getCredentialOwnerRole, getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles'; import { createManyUsers, createUser, createUserShell } from './shared/db/users'; -import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import Container from 'typedi'; -import { License } from '@/License'; -import { mockInstance } from '../shared/mocking'; import { UserManagementMailer } from '@/UserManagement/email'; +import { mockInstance } from '../shared/mocking'; + const sharingSpy = jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(true); const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] }); -let globalMemberRole: Role; let owner: User; let member: User; let anotherMember: User; @@ -32,18 +30,14 @@ let saveCredential: SaveCredentialFunction; const mailer = mockInstance(UserManagementMailer); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); - const credentialOwnerRole = await getCredentialOwnerRole(); - - owner = await createUser({ globalRole: globalOwnerRole }); - member = await createUser({ globalRole: globalMemberRole }); - anotherMember = await createUser({ globalRole: globalMemberRole }); + owner = await createUser({ role: 'global:owner' }); + member = await createUser({ role: 'global:member' }); + anotherMember = await createUser({ role: 'global:member' }); authOwnerAgent = testServer.authAgentFor(owner); authAnotherMemberAgent = testServer.authAgentFor(anotherMember); - saveCredential = affixRoleToSaveCredential(credentialOwnerRole); + saveCredential = affixRoleToSaveCredential('credential:owner'); }); beforeEach(async () => { @@ -92,7 +86,7 @@ describe('router should switch based on flag', () => { describe('GET /credentials', () => { test('should return all creds for owner', async () => { const [member1, member2, member3] = await createManyUsers(3, { - globalRole: globalMemberRole, + role: 'global:member', }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); @@ -156,7 +150,7 @@ describe('GET /credentials', () => { test('should return only relevant creds for member', async () => { const [member1, member2] = await createManyUsers(2, { - globalRole: globalMemberRole, + role: 'global:member', }); await saveCredential(randomCredentialPayload(), { user: member2 }); @@ -232,7 +226,7 @@ describe('GET /credentials/:id', () => { test('should retrieve non-owned cred for owner', async () => { const [member1, member2] = await createManyUsers(2, { - globalRole: globalMemberRole, + role: 'global:member', }); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); @@ -271,7 +265,7 @@ describe('GET /credentials/:id', () => { test('should retrieve owned cred for member', async () => { const [member1, member2, member3] = await createManyUsers(3, { - globalRole: globalMemberRole, + role: 'global:member', }); const authMemberAgent = testServer.authAgentFor(member1); const savedCredential = await saveCredential(randomCredentialPayload(), { user: member1 }); @@ -339,7 +333,7 @@ describe('PUT /credentials/:id/share', () => { const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const [member1, member2, member3, member4, member5] = await createManyUsers(5, { - globalRole: globalMemberRole, + role: 'global:member', }); const shareWithIds = [member1.id, member2.id, member3.id]; @@ -353,7 +347,6 @@ describe('PUT /credentials/:id/share', () => { expect(response.body.data).toBeUndefined(); const sharedCredentials = await Container.get(SharedCredentialsRepository).find({ - relations: ['role'], where: { credentialsId: savedCredential.id }, }); @@ -362,13 +355,11 @@ describe('PUT /credentials/:id/share', () => { sharedCredentials.forEach((sharedCredential) => { if (sharedCredential.userId === owner.id) { - expect(sharedCredential.role.name).toBe('owner'); - expect(sharedCredential.role.scope).toBe('credential'); + expect(sharedCredential.role).toBe('credential:owner'); return; } expect(shareWithIds).toContain(sharedCredential.userId); - expect(sharedCredential.role.name).toBe('user'); - expect(sharedCredential.role.scope).toBe('credential'); + expect(sharedCredential.role).toBe('credential:user'); }); expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1); @@ -376,7 +367,7 @@ describe('PUT /credentials/:id/share', () => { test('should share the credential with the provided userIds', async () => { const [member1, member2, member3] = await createManyUsers(3, { - globalRole: globalMemberRole, + role: 'global:member', }); const memberIds = [member1.id, member2.id, member3.id]; const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); @@ -390,25 +381,21 @@ describe('PUT /credentials/:id/share', () => { // check that sharings got correctly set in DB const sharedCredentials = await Container.get(SharedCredentialsRepository).find({ - relations: ['role'], where: { credentialsId: savedCredential.id, userId: In([...memberIds]) }, }); expect(sharedCredentials.length).toBe(memberIds.length); sharedCredentials.forEach((sharedCredential) => { - expect(sharedCredential.role.name).toBe('user'); - expect(sharedCredential.role.scope).toBe('credential'); + expect(sharedCredential.role).toBe('credential:user'); }); // check that owner still exists const ownerSharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({ - relations: ['role'], where: { credentialsId: savedCredential.id, userId: owner.id }, }); - expect(ownerSharedCredential.role.name).toBe('owner'); - expect(ownerSharedCredential.role.scope).toBe('credential'); + expect(ownerSharedCredential.role).toBe('credential:owner'); expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1); }); @@ -456,7 +443,7 @@ describe('PUT /credentials/:id/share', () => { test('should respond 403 for non-owned credentials for non-shared members sharing', async () => { const savedCredential = await saveCredential(randomCredentialPayload(), { user: member }); - const tempUser = await createUser({ globalRole: globalMemberRole }); + const tempUser = await createUser({ role: 'global:member' }); const response = await authAnotherMemberAgent .put(`/credentials/${savedCredential.id}/share`) @@ -487,7 +474,7 @@ describe('PUT /credentials/:id/share', () => { }); test('should ignore pending sharee', async () => { - const memberShell = await createUserShell(globalMemberRole); + const memberShell = await createUserShell('global:member'); const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const response = await authOwnerAgent @@ -538,7 +525,7 @@ describe('PUT /credentials/:id/share', () => { const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner }); const [member1, member2] = await createManyUsers(2, { - globalRole: globalMemberRole, + role: 'global:member', }); await shareCredentialWithUsers(savedCredential, [member1, member2]); diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials.test.ts index 992ea6fb71..f665d3f7b5 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials.test.ts @@ -1,28 +1,24 @@ +import { Container } from 'typedi'; import type { SuperAgentTest } from 'supertest'; import config from '@/config'; import type { ListQuery } from '@/requests'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; +import { CredentialsRepository } from '@db/repositories/credentials.repository'; +import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; +import { License } from '@/License'; import { randomCredentialPayload, randomName, randomString } from './shared/random'; import * as testDb from './shared/testDb'; import type { SaveCredentialFunction } from './shared/types'; import * as utils from './shared/utils/'; import { affixRoleToSaveCredential, shareCredentialWithUsers } from './shared/db/credentials'; -import { getCredentialOwnerRole, getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles'; import { createManyUsers, createUser } from './shared/db/users'; -import { CredentialsRepository } from '@db/repositories/credentials.repository'; -import Container from 'typedi'; -import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -import { License } from '@/License'; // mock that credentialsSharing is not enabled jest.spyOn(License.prototype, 'isSharingEnabled').mockReturnValue(false); const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] }); -let globalOwnerRole: Role; -let globalMemberRole: Role; let owner: User; let member: User; let secondMember: User; @@ -31,15 +27,11 @@ let authMemberAgent: SuperAgentTest; let saveCredential: SaveCredentialFunction; beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); - const credentialOwnerRole = await getCredentialOwnerRole(); + owner = await createUser({ role: 'global:owner' }); + member = await createUser({ role: 'global:member' }); + secondMember = await createUser({ role: 'global:member' }); - owner = await createUser({ globalRole: globalOwnerRole }); - member = await createUser({ globalRole: globalMemberRole }); - secondMember = await createUser({ globalRole: globalMemberRole }); - - saveCredential = affixRoleToSaveCredential(credentialOwnerRole); + saveCredential = affixRoleToSaveCredential('credential:owner'); authOwnerAgent = testServer.authAgentFor(owner); authMemberAgent = testServer.authAgentFor(member); @@ -74,7 +66,7 @@ describe('GET /credentials', () => { test('should return only own creds for member', async () => { const [member1, member2] = await createManyUsers(2, { - globalRole: globalMemberRole, + role: 'global:member', }); const [savedCredential1] = await Promise.all([ diff --git a/packages/cli/test/integration/environments/SourceControl.test.ts b/packages/cli/test/integration/environments/SourceControl.test.ts index 384c02d446..f1da8d0672 100644 --- a/packages/cli/test/integration/environments/SourceControl.test.ts +++ b/packages/cli/test/integration/environments/SourceControl.test.ts @@ -8,7 +8,6 @@ import { SourceControlService } from '@/environments/sourceControl/sourceControl import type { SourceControlledFile } from '@/environments/sourceControl/types/sourceControlledFile'; import * as utils from '../shared/utils/'; -import { getGlobalOwnerRole } from '../shared/db/roles'; import { createUser } from '../shared/db/users'; let authOwnerAgent: SuperAgentTest; @@ -20,8 +19,7 @@ const testServer = utils.setupTestServer({ }); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - owner = await createUser({ globalRole: globalOwnerRole }); + owner = await createUser({ role: 'global:owner' }); authOwnerAgent = testServer.authAgentFor(owner); Container.get(SourceControlPreferencesService).isSourceControlConnected = () => true; diff --git a/packages/cli/test/integration/eventbus.ee.test.ts b/packages/cli/test/integration/eventbus.ee.test.ts index 0e37936ccb..25e761914f 100644 --- a/packages/cli/test/integration/eventbus.ee.test.ts +++ b/packages/cli/test/integration/eventbus.ee.test.ts @@ -3,7 +3,6 @@ import axios from 'axios'; import syslog from 'syslog-client'; import { v4 as uuid } from 'uuid'; import type { SuperAgentTest } from 'supertest'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import type { MessageEventBusDestinationSentryOptions, @@ -26,7 +25,6 @@ import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessag import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode'; import * as utils from './shared/utils'; -import { getGlobalOwnerRole } from './shared/db/roles'; import { createUser } from './shared/db/users'; jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); @@ -35,7 +33,6 @@ const mockedAxios = axios as jest.Mocked; jest.mock('syslog-client'); const mockedSyslog = syslog as jest.Mocked; -let globalOwnerRole: Role; let owner: User; let authOwnerAgent: SuperAgentTest; @@ -85,8 +82,7 @@ const testServer = utils.setupTestServer({ }); beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); - owner = await createUser({ globalRole: globalOwnerRole }); + owner = await createUser({ role: 'global:owner' }); authOwnerAgent = testServer.authAgentFor(owner); mockedSyslog.createClient.mockImplementation(() => new syslog.Client()); diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index b2c62ba11c..9f6581d049 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -1,8 +1,6 @@ import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils/'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; -import { getGlobalOwnerRole } from './shared/db/roles'; import { createUser } from './shared/db/users'; /** @@ -11,7 +9,6 @@ import { createUser } from './shared/db/users'; * The tests in this file are only checking endpoint permissions. */ -let globalOwnerRole: Role; let owner: User; let authOwnerAgent: SuperAgentTest; @@ -21,8 +18,7 @@ const testServer = utils.setupTestServer({ }); beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); - owner = await createUser({ globalRole: globalOwnerRole }); + owner = await createUser({ role: 'global:owner' }); authOwnerAgent = testServer.authAgentFor(owner); }); diff --git a/packages/cli/test/integration/import.service.test.ts b/packages/cli/test/integration/import.service.test.ts index 07b7f1a600..4809e58138 100644 --- a/packages/cli/test/integration/import.service.test.ts +++ b/packages/cli/test/integration/import.service.test.ts @@ -6,7 +6,6 @@ import type { INode } from 'n8n-workflow'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { TagRepository } from '@/databases/repositories/tag.repository'; import { ImportService } from '@/services/import.service'; -import { RoleService } from '@/services/role.service'; import { TagEntity } from '@/databases/entities/TagEntity'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository'; @@ -34,12 +33,7 @@ describe('ImportService', () => { credentialsRepository.find.mockResolvedValue([]); - importService = new ImportService( - mock(), - credentialsRepository, - tagRepository, - Container.get(RoleService), - ); + importService = new ImportService(mock(), credentialsRepository, tagRepository); }); afterEach(async () => { @@ -67,10 +61,8 @@ describe('ImportService', () => { await importService.importWorkflows([workflowToImport], owner.id); - const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole(); - const dbSharing = await Container.get(SharedWorkflowRepository).findOneOrFail({ - where: { workflowId: workflowToImport.id, userId: owner.id, roleId: workflowOwnerRole.id }, + where: { workflowId: workflowToImport.id, userId: owner.id, role: 'workflow:owner' }, }); expect(dbSharing.userId).toBe(owner.id); diff --git a/packages/cli/test/integration/invitations.api.test.ts b/packages/cli/test/integration/invitations.api.test.ts index 2ac4d4c1d6..4ca48712ea 100644 --- a/packages/cli/test/integration/invitations.api.test.ts +++ b/packages/cli/test/integration/invitations.api.test.ts @@ -17,7 +17,6 @@ import { } from './shared/random'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; -import { getGlobalAdminRole, getGlobalMemberRole } from './shared/db/roles'; import { createMember, createOwner, createUser, createUserShell } from './shared/db/users'; import { ExternalHooks } from '@/ExternalHooks'; import { InternalHooks } from '@/InternalHooks'; @@ -56,8 +55,7 @@ describe('POST /invitations/:id/accept', () => { }); test('should fill out a member shell', async () => { - const globalMemberRole = await getGlobalMemberRole(); - const memberShell = await createUserShell(globalMemberRole); + const memberShell = await createUserShell('global:member'); const memberData = { inviterId: owner.id, @@ -78,7 +76,7 @@ describe('POST /invitations/:id/accept', () => { lastName, personalizationAnswers, password, - globalRole, + role, isPending, apiKey, globalScopes, @@ -91,8 +89,7 @@ describe('POST /invitations/:id/accept', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole.scope).toBe('global'); - expect(globalRole.name).toBe('member'); + expect(role).toBe('global:member'); expect(apiKey).not.toBeDefined(); expect(globalScopes).toBeDefined(); expect(globalScopes).not.toHaveLength(0); @@ -110,8 +107,7 @@ describe('POST /invitations/:id/accept', () => { }); test('should fill out an admin shell', async () => { - const globalAdminRole = await getGlobalAdminRole(); - const adminShell = await createUserShell(globalAdminRole); + const adminShell = await createUserShell('global:admin'); const memberData = { inviterId: owner.id, @@ -132,7 +128,7 @@ describe('POST /invitations/:id/accept', () => { lastName, personalizationAnswers, password, - globalRole, + role, isPending, apiKey, globalScopes, @@ -145,8 +141,7 @@ describe('POST /invitations/:id/accept', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole.scope).toBe('global'); - expect(globalRole.name).toBe('admin'); + expect(role).toBe('global:admin'); expect(apiKey).not.toBeDefined(); expect(globalScopes).toBeDefined(); expect(globalScopes).not.toHaveLength(0); @@ -166,11 +161,9 @@ describe('POST /invitations/:id/accept', () => { test('should fail with invalid payloads', async () => { const memberShellEmail = randomEmail(); - const globalMemberRole = await getGlobalMemberRole(); - const memberShell = await Container.get(UserRepository).save({ email: memberShellEmail, - globalRole: globalMemberRole, + role: 'global:member', }); const invalidPayloads = [ @@ -219,8 +212,7 @@ describe('POST /invitations/:id/accept', () => { }); test('should fail with already accepted invite', async () => { - const globalMemberRole = await getGlobalMemberRole(); - const member = await createUser({ globalRole: globalMemberRole }); + const member = await createUser({ role: 'global:member' }); const memberData = { inviterId: owner.id, @@ -334,7 +326,7 @@ describe('POST /invitations', () => { const response = await ownerAgent .post('/invitations') - .send([{ email: randomEmail(), role: 'admin' }]) + .send([{ email: randomEmail(), role: 'global:admin' }]) .expect(200); const [result] = response.body.data as UserInvitationResponse[]; @@ -349,11 +341,11 @@ describe('POST /invitations', () => { test('should reinvite member', async () => { mailer.invite.mockResolvedValue({ emailSent: false }); - await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'member' }]); + await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'global:member' }]); await ownerAgent .post('/invitations') - .send([{ email: randomEmail(), role: 'member' }]) + .send([{ email: randomEmail(), role: 'global:member' }]) .expect(200); }); @@ -361,11 +353,11 @@ describe('POST /invitations', () => { license.isAdvancedPermissionsLicensed.mockReturnValue(true); mailer.invite.mockResolvedValue({ emailSent: false }); - await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'admin' }]); + await ownerAgent.post('/invitations').send([{ email: randomEmail(), role: 'global:admin' }]); await ownerAgent .post('/invitations') - .send([{ email: randomEmail(), role: 'admin' }]) + .send([{ email: randomEmail(), role: 'global:admin' }]) .expect(200); }); @@ -375,7 +367,7 @@ describe('POST /invitations', () => { await ownerAgent .post('/invitations') - .send([{ email: randomEmail(), role: 'admin' }]) + .send([{ email: randomEmail(), role: 'global:admin' }]) .expect(403); }); @@ -384,8 +376,7 @@ describe('POST /invitations', () => { mailer.invite.mockResolvedValue({ emailSent: true }); - const globalMemberRole = await getGlobalMemberRole(); - const memberShell = await createUserShell(globalMemberRole); + const memberShell = await createUserShell('global:member'); const newUser = randomEmail(); diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index 96c7115ecc..5969fa4b7f 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -6,7 +6,6 @@ import { jsonParse } from 'n8n-workflow'; import { Cipher } from 'n8n-core'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/Ldap/constants'; import { LdapService } from '@/Ldap/ldap.service'; @@ -18,7 +17,6 @@ import { randomEmail, randomName, uniqueId } from './../shared/random'; import * as testDb from './../shared/testDb'; import * as utils from '../shared/utils/'; -import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles'; import { createLdapUser, createUser, getAllUsers, getLdapIdentities } from '../shared/db/users'; import { UserRepository } from '@db/repositories/user.repository'; import { SettingsRepository } from '@db/repositories/settings.repository'; @@ -26,7 +24,6 @@ import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProvider jest.mock('@/telemetry'); -let globalMemberRole: Role; let owner: User; let authOwnerAgent: SuperAgentTest; @@ -50,14 +47,7 @@ const testServer = utils.setupTestServer({ }); beforeAll(async () => { - const [globalOwnerRole, fetchedGlobalMemberRole] = await Promise.all([ - getGlobalOwnerRole(), - getGlobalMemberRole(), - ]); - - globalMemberRole = fetchedGlobalMemberRole; - - owner = await createUser({ globalRole: globalOwnerRole, password: 'password' }); + owner = await createUser({ role: 'global:owner', password: 'password' }); authOwnerAgent = testServer.authAgentFor(owner); defaultLdapConfig.bindingAdminPassword = Container.get(Cipher).encrypt( @@ -97,7 +87,7 @@ const createLdapConfig = async (attributes: Partial = {}): Promise { - const member = await createUser({ globalRole: globalMemberRole }); + const member = await createUser({ role: 'global:member' }); const authAgent = testServer.authAgentFor(member); await authAgent.get('/ldap/config').expect(403); await authAgent.put('/ldap/config').expect(403); @@ -169,7 +159,7 @@ describe('PUT /ldap/config', () => { const ldapConfig = await createLdapConfig(); Container.get(LdapService).setConfig(ldapConfig); - const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId()); + const member = await createLdapUser({ role: 'global:member' }, uniqueId()); const configuration = ldapConfig; @@ -282,7 +272,7 @@ describe('POST /ldap/sync', () => { const ldapUserId = uniqueId(); const member = await createLdapUser( - { globalRole: globalMemberRole, email: ldapUserEmail }, + { role: 'global:member', email: ldapUserEmail }, ldapUserId, ); @@ -311,7 +301,7 @@ describe('POST /ldap/sync', () => { const ldapUserId = uniqueId(); const member = await createLdapUser( - { globalRole: globalMemberRole, email: ldapUserEmail }, + { role: 'global:member', email: ldapUserEmail }, ldapUserId, ); @@ -394,7 +384,7 @@ describe('POST /ldap/sync', () => { await createLdapUser( { - globalRole: globalMemberRole, + role: 'global:member', email: ldapUser.mail, firstName: ldapUser.givenName, lastName: randomName(), @@ -427,7 +417,7 @@ describe('POST /ldap/sync', () => { await createLdapUser( { - globalRole: globalMemberRole, + role: 'global:member', email: ldapUser.mail, firstName: ldapUser.givenName, lastName: ldapUser.sn, @@ -456,7 +446,7 @@ describe('POST /ldap/sync', () => { }); test('should remove user instance access once the user is disabled during synchronization', async () => { - const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId()); + const member = await createLdapUser({ role: 'global:member' }, uniqueId()); jest.spyOn(LdapService.prototype, 'searchWithAdminBinding').mockResolvedValue([]); @@ -543,7 +533,7 @@ describe('POST /login', () => { await createLdapUser( { - globalRole: globalMemberRole, + role: 'global:member', email: ldapUser.mail, firstName: 'firstname', lastName: 'lastname', @@ -577,7 +567,7 @@ describe('POST /login', () => { }; await createUser({ - globalRole: globalMemberRole, + role: 'global:member', email: ldapUser.mail, firstName: ldapUser.givenName, lastName: 'lastname', @@ -592,7 +582,7 @@ describe('Instance owner should able to delete LDAP users', () => { const ldapConfig = await createLdapConfig(); Container.get(LdapService).setConfig(ldapConfig); - const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId()); + const member = await createLdapUser({ role: 'global:member' }, uniqueId()); await authOwnerAgent.post(`/users/${member.id}`); }); @@ -601,7 +591,7 @@ describe('Instance owner should able to delete LDAP users', () => { const ldapConfig = await createLdapConfig(); Container.get(LdapService).setConfig(ldapConfig); - const member = await createLdapUser({ globalRole: globalMemberRole }, uniqueId()); + const member = await createLdapUser({ role: 'global:member' }, uniqueId()); // delete the LDAP member and transfer its workflows/credentials to instance owner await authOwnerAgent.post(`/users/${member.id}?transferId=${owner.id}`); diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index 1430beb064..3d1fc4cda8 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -5,7 +5,6 @@ import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; import { License } from '@/License'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; -import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles'; import { createUserShell } from './shared/db/users'; const MOCK_SERVER_URL = 'https://server.com/v1'; @@ -19,10 +18,8 @@ let authMemberAgent: SuperAgentTest; const testServer = utils.setupTestServer({ endpointGroups: ['license'] }); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - const globalMemberRole = await getGlobalMemberRole(); - owner = await createUserShell(globalOwnerRole); - member = await createUserShell(globalMemberRole); + owner = await createUserShell('global:owner'); + member = await createUserShell('global:member'); authOwnerAgent = testServer.authAgentFor(owner); authMemberAgent = testServer.authAgentFor(member); diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index 552e0d649c..61dde8c92f 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -1,7 +1,6 @@ import type { SuperAgentTest } from 'supertest'; import { IsNull } from 'typeorm'; import validator from 'validator'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { @@ -13,21 +12,12 @@ import { } from './shared/random'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; -import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles'; import { addApiKey, createUser, createUserShell } from './shared/db/users'; import Container from 'typedi'; import { UserRepository } from '@db/repositories/user.repository'; const testServer = utils.setupTestServer({ endpointGroups: ['me'] }); -let globalOwnerRole: Role; -let globalMemberRole: Role; - -beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); -}); - beforeEach(async () => { await testDb.truncate(['User']); }); @@ -37,7 +27,7 @@ describe('Owner shell', () => { let authOwnerShellAgent: SuperAgentTest; beforeEach(async () => { - ownerShell = await createUserShell(globalOwnerRole); + ownerShell = await createUserShell('global:owner'); await addApiKey(ownerShell); authOwnerShellAgent = testServer.authAgentFor(ownerShell); }); @@ -54,7 +44,7 @@ describe('Owner shell', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, apiKey, @@ -67,8 +57,7 @@ describe('Owner shell', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); const storedOwnerShell = await Container.get(UserRepository).findOneByOrFail({ id }); @@ -177,7 +166,7 @@ describe('Member', () => { beforeEach(async () => { member = await createUser({ password: memberPassword, - globalRole: globalMemberRole, + role: 'global:member', apiKey: randomApiKey(), }); authMemberAgent = testServer.authAgentFor(member); @@ -197,7 +186,7 @@ describe('Member', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, apiKey, @@ -210,8 +199,7 @@ describe('Member', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole.name).toBe('member'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:member'); expect(apiKey).toBeUndefined(); const storedMember = await Container.get(UserRepository).findOneByOrFail({ id }); @@ -317,7 +305,7 @@ describe('Owner', () => { }); test('PATCH /me should succeed with valid inputs', async () => { - const owner = await createUser({ globalRole: globalOwnerRole }); + const owner = await createUser({ role: 'global:owner' }); const authOwnerAgent = testServer.authAgentFor(owner); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { @@ -331,7 +319,7 @@ describe('Owner', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, apiKey, @@ -344,8 +332,7 @@ describe('Owner', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); const storedOwner = await Container.get(UserRepository).findOneByOrFail({ id }); diff --git a/packages/cli/test/integration/mfa/mfa.api.test.ts b/packages/cli/test/integration/mfa/mfa.api.test.ts index 1f9a5318ea..2e7df144ca 100644 --- a/packages/cli/test/integration/mfa/mfa.api.test.ts +++ b/packages/cli/test/integration/mfa/mfa.api.test.ts @@ -1,6 +1,5 @@ import Container from 'typedi'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { randomPassword } from '@/Ldap/helpers'; import { TOTPService } from '@/Mfa/totp.service'; @@ -13,7 +12,6 @@ import { UserRepository } from '@db/repositories/user.repository'; jest.mock('@/telemetry'); -let globalOwnerRole: Role; let owner: User; const testServer = utils.setupTestServer({ @@ -23,7 +21,7 @@ const testServer = utils.setupTestServer({ beforeEach(async () => { await testDb.truncate(['User']); - owner = await createUser({ globalRole: globalOwnerRole }); + owner = await createUser({ role: 'global:owner' }); config.set('userManagement.disabled', false); }); diff --git a/packages/cli/test/integration/owner.api.test.ts b/packages/cli/test/integration/owner.api.test.ts index 2d044ee43a..cbe8a84c91 100644 --- a/packages/cli/test/integration/owner.api.test.ts +++ b/packages/cli/test/integration/owner.api.test.ts @@ -2,7 +2,6 @@ import validator from 'validator'; import type { SuperAgentTest } from 'supertest'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { randomEmail, @@ -12,23 +11,17 @@ import { } from './shared/random'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils/'; -import { getGlobalOwnerRole } from './shared/db/roles'; import { createUserShell } from './shared/db/users'; import { UserRepository } from '@db/repositories/user.repository'; import Container from 'typedi'; const testServer = utils.setupTestServer({ endpointGroups: ['owner'] }); -let globalOwnerRole: Role; let ownerShell: User; let authOwnerShellAgent: SuperAgentTest; -beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); -}); - beforeEach(async () => { - ownerShell = await createUserShell(globalOwnerRole); + ownerShell = await createUserShell('global:owner'); authOwnerShellAgent = testServer.authAgentFor(ownerShell); config.set('userManagement.isInstanceOwnerSetUp', false); }); @@ -56,7 +49,7 @@ describe('POST /owner/setup', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, apiKey, @@ -70,8 +63,7 @@ describe('POST /owner/setup', () => { expect(personalizationAnswers).toBeNull(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole.name).toBe('owner'); - expect(globalRole.scope).toBe('global'); + expect(role).toBe('global:owner'); expect(apiKey).toBeUndefined(); expect(globalScopes).not.toHaveLength(0); diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index cf263a890d..996fc0ab77 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -5,7 +5,6 @@ import { mock } from 'jest-mock-extended'; import { License } from '@/License'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { setCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { ExternalHooks } from '@/ExternalHooks'; @@ -24,14 +23,11 @@ import { randomValidPassword, } from './shared/random'; import * as testDb from './shared/testDb'; -import { getGlobalMemberRole, getGlobalOwnerRole } from './shared/db/roles'; import { createUser } from './shared/db/users'; import { PasswordUtility } from '@/services/password.utility'; config.set('userManagement.jwtSecret', randomString(5, 10)); -let globalOwnerRole: Role; -let globalMemberRole: Role; let owner: User; let member: User; @@ -41,15 +37,10 @@ const testServer = setupTestServer({ endpointGroups: ['passwordReset'] }); const jwtService = Container.get(JwtService); let userService: UserService; -beforeAll(async () => { - globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); -}); - beforeEach(async () => { await testDb.truncate(['User']); - owner = await createUser({ globalRole: globalOwnerRole }); - member = await createUser({ globalRole: globalMemberRole }); + owner = await createUser({ role: 'global:owner' }); + member = await createUser({ role: 'global:member' }); externalHooks.run.mockReset(); jest.replaceProperty(mailer, 'isEmailSetUp', true); userService = Container.get(UserService); @@ -59,7 +50,7 @@ describe('POST /forgot-password', () => { test('should send password reset email', async () => { const member = await createUser({ email: 'test@test.com', - globalRole: globalMemberRole, + role: 'global:member', }); await Promise.all( @@ -85,7 +76,7 @@ describe('POST /forgot-password', () => { await setCurrentAuthenticationMethod('saml'); const member = await createUser({ email: 'test@test.com', - globalRole: globalMemberRole, + role: 'global:member', }); await testServer.authlessAgent diff --git a/packages/cli/test/integration/publicApi/credentials.test.ts b/packages/cli/test/integration/publicApi/credentials.test.ts index b754208dec..6ce0723874 100644 --- a/packages/cli/test/integration/publicApi/credentials.test.ts +++ b/packages/cli/test/integration/publicApi/credentials.test.ts @@ -1,5 +1,4 @@ import type { SuperAgentTest } from 'supertest'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { randomApiKey, randomName, randomString } from '../shared/random'; @@ -7,14 +6,11 @@ import * as utils from '../shared/utils/'; import type { CredentialPayload, SaveCredentialFunction } from '../shared/types'; import * as testDb from '../shared/testDb'; import { affixRoleToSaveCredential } from '../shared/db/credentials'; -import { getAllRoles } from '../shared/db/roles'; import { addApiKey, createUser, createUserShell } from '../shared/db/users'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import Container from 'typedi'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; -let globalMemberRole: Role; -let credentialOwnerRole: Role; let owner: User; let member: User; let authOwnerAgent: SuperAgentTest; @@ -25,19 +21,13 @@ let saveCredential: SaveCredentialFunction; const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] }); beforeAll(async () => { - const [globalOwnerRole, fetchedGlobalMemberRole, _, fetchedCredentialOwnerRole] = - await getAllRoles(); - - globalMemberRole = fetchedGlobalMemberRole; - credentialOwnerRole = fetchedCredentialOwnerRole; - - owner = await addApiKey(await createUserShell(globalOwnerRole)); - member = await createUser({ globalRole: globalMemberRole, apiKey: randomApiKey() }); + owner = await addApiKey(await createUserShell('global:owner')); + member = await createUser({ role: 'global:member', apiKey: randomApiKey() }); authOwnerAgent = testServer.publicApiAgentFor(owner); authMemberAgent = testServer.publicApiAgentFor(member); - saveCredential = affixRoleToSaveCredential(credentialOwnerRole); + saveCredential = affixRoleToSaveCredential('credential:owner'); await utils.initCredentialsTypes(); }); @@ -73,11 +63,11 @@ describe('POST /credentials', () => { expect(credential.data).not.toBe(payload.data); const sharedCredential = await Container.get(SharedCredentialsRepository).findOneOrFail({ - relations: ['user', 'credentials', 'role'], + relations: ['user', 'credentials'], where: { credentialsId: credential.id, userId: owner.id }, }); - expect(sharedCredential.role).toEqual(credentialOwnerRole); + expect(sharedCredential.role).toEqual('credential:owner'); expect(sharedCredential.credentials.name).toBe(payload.name); }); @@ -156,7 +146,7 @@ describe('DELETE /credentials/:id', () => { test('should delete owned cred for member but leave others untouched', async () => { const anotherMember = await createUser({ - globalRole: globalMemberRole, + role: 'global:member', apiKey: randomApiKey(), }); diff --git a/packages/cli/test/integration/publicApi/executions.test.ts b/packages/cli/test/integration/publicApi/executions.test.ts index 32c069bd0f..012519df66 100644 --- a/packages/cli/test/integration/publicApi/executions.test.ts +++ b/packages/cli/test/integration/publicApi/executions.test.ts @@ -5,7 +5,6 @@ import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { randomApiKey } from '../shared/random'; import * as utils from '../shared/utils/'; import * as testDb from '../shared/testDb'; -import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles'; import { createUser } from '../shared/db/users'; import { createManyWorkflows, @@ -30,11 +29,9 @@ let workflowRunner: ActiveWorkflowRunner; const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] }); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - const globalUserRole = await getGlobalMemberRole(); - owner = await createUser({ globalRole: globalOwnerRole, apiKey: randomApiKey() }); - user1 = await createUser({ globalRole: globalUserRole, apiKey: randomApiKey() }); - user2 = await createUser({ globalRole: globalUserRole, apiKey: randomApiKey() }); + owner = await createUser({ role: 'global:owner', apiKey: randomApiKey() }); + user1 = await createUser({ role: 'global:member', apiKey: randomApiKey() }); + user2 = await createUser({ role: 'global:member', apiKey: randomApiKey() }); // TODO: mock BinaryDataService instead await utils.initBinaryDataService(); diff --git a/packages/cli/test/integration/publicApi/users.ee.test.ts b/packages/cli/test/integration/publicApi/users.ee.test.ts index 40709e8926..8dfae84625 100644 --- a/packages/cli/test/integration/publicApi/users.ee.test.ts +++ b/packages/cli/test/integration/publicApi/users.ee.test.ts @@ -2,14 +2,12 @@ import type { SuperAgentTest } from 'supertest'; import validator from 'validator'; import { v4 as uuid } from 'uuid'; -import type { Role } from '@db/entities/Role'; import { License } from '@/License'; import { mockInstance } from '../../shared/mocking'; import { randomApiKey } from '../shared/random'; import * as utils from '../shared/utils/'; import * as testDb from '../shared/testDb'; -import { getGlobalMemberRole, getGlobalOwnerRole } from '../shared/db/roles'; import { createUser, createUserShell } from '../shared/db/users'; mockInstance(License, { @@ -18,16 +16,6 @@ mockInstance(License, { const testServer = utils.setupTestServer({ endpointGroups: ['publicApi'] }); -let globalOwnerRole: Role; -let globalMemberRole: Role; - -beforeAll(async () => { - [globalOwnerRole, globalMemberRole] = await Promise.all([ - getGlobalOwnerRole(), - getGlobalMemberRole(), - ]); -}); - beforeEach(async () => { await testDb.truncate(['SharedCredentials', 'SharedWorkflow', 'Workflow', 'Credentials', 'User']); }); @@ -35,14 +23,14 @@ beforeEach(async () => { describe('With license unlimited quota:users', () => { describe('GET /users', () => { test('should fail due to missing API Key', async () => { - const owner = await createUser({ globalRole: globalOwnerRole }); + const owner = await createUser({ role: 'global:owner' }); const authOwnerAgent = testServer.publicApiAgentFor(owner); await authOwnerAgent.get('/users').expect(401); }); test('should fail due to invalid API Key', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); owner.apiKey = 'invalid-key'; @@ -58,7 +46,7 @@ describe('With license unlimited quota:users', () => { test('should return all users', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); @@ -77,7 +65,7 @@ describe('With license unlimited quota:users', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, createdAt, @@ -91,7 +79,7 @@ describe('With license unlimited quota:users', () => { expect(personalizationAnswers).toBeUndefined(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole).toBeUndefined(); + expect(role).toBeUndefined(); expect(createdAt).toBeDefined(); expect(updatedAt).toBeDefined(); } @@ -100,14 +88,14 @@ describe('With license unlimited quota:users', () => { describe('GET /users/:id', () => { test('should fail due to missing API Key', async () => { - const owner = await createUser({ globalRole: globalOwnerRole }); + const owner = await createUser({ role: 'global:owner' }); const authOwnerAgent = testServer.publicApiAgentFor(owner); await authOwnerAgent.get(`/users/${owner.id}`).expect(401); }); test('should fail due to invalid API Key', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); owner.apiKey = 'invalid-key'; @@ -122,7 +110,7 @@ describe('With license unlimited quota:users', () => { }); test('should return 404 for non-existing id ', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); const authOwnerAgent = testServer.publicApiAgentFor(owner); @@ -131,11 +119,11 @@ describe('With license unlimited quota:users', () => { test('should return a pending user', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); - const { id: memberId } = await createUserShell(globalMemberRole); + const { id: memberId } = await createUserShell('global:member'); const authOwnerAgent = testServer.publicApiAgentFor(owner); const response = await authOwnerAgent.get(`/users/${memberId}`).expect(200); @@ -146,7 +134,7 @@ describe('With license unlimited quota:users', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, createdAt, @@ -159,7 +147,7 @@ describe('With license unlimited quota:users', () => { expect(lastName).toBeDefined(); expect(personalizationAnswers).toBeUndefined(); expect(password).toBeUndefined(); - expect(globalRole).toBeUndefined(); + expect(role).toBeUndefined(); expect(createdAt).toBeDefined(); expect(isPending).toBeDefined(); expect(isPending).toBeTruthy(); @@ -170,7 +158,7 @@ describe('With license unlimited quota:users', () => { describe('GET /users/:email', () => { test('with non-existing email should return 404', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); const authOwnerAgent = testServer.publicApiAgentFor(owner); @@ -179,7 +167,7 @@ describe('With license unlimited quota:users', () => { test('should return a user', async () => { const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); @@ -192,7 +180,7 @@ describe('With license unlimited quota:users', () => { firstName, lastName, personalizationAnswers, - globalRole, + role, password, isPending, createdAt, @@ -206,7 +194,7 @@ describe('With license unlimited quota:users', () => { expect(personalizationAnswers).toBeUndefined(); expect(password).toBeUndefined(); expect(isPending).toBe(false); - expect(globalRole).toBeUndefined(); + expect(role).toBeUndefined(); expect(createdAt).toBeDefined(); expect(updatedAt).toBeDefined(); }); @@ -220,7 +208,7 @@ describe('With license without quota:users', () => { mockInstance(License, { getUsersLimit: jest.fn().mockReturnValue(null) }); const owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); authOwnerAgent = testServer.publicApiAgentFor(owner); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 7912a1fd14..53aa198f09 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -2,25 +2,22 @@ import type { SuperAgentTest } from 'supertest'; import Container from 'typedi'; import type { INode } from 'n8n-workflow'; import { STARTING_NODES } from '@/constants'; -import type { Role } from '@db/entities/Role'; import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; +import { Push } from '@/push'; +import { ExecutionService } from '@/executions/execution.service'; import { randomApiKey } from '../shared/random'; import * as utils from '../shared/utils/'; import * as testDb from '../shared/testDb'; -import { getAllRoles } from '../shared/db/roles'; import { createUser } from '../shared/db/users'; import { createWorkflow, createWorkflowWithTrigger } from '../shared/db/workflows'; import { createTag } from '../shared/db/tags'; import { mockInstance } from '../../shared/mocking'; -import { Push } from '@/push'; -import { ExecutionService } from '@/executions/execution.service'; -let workflowOwnerRole: Role; let owner: User; let member: User; let authOwnerAgent: SuperAgentTest; @@ -34,17 +31,13 @@ mockInstance(Push); mockInstance(ExecutionService); beforeAll(async () => { - const [globalOwnerRole, globalMemberRole, fetchedWorkflowOwnerRole] = await getAllRoles(); - - workflowOwnerRole = fetchedWorkflowOwnerRole; - owner = await createUser({ - globalRole: globalOwnerRole, + role: 'global:owner', apiKey: randomApiKey(), }); member = await createUser({ - globalRole: globalMemberRole, + role: 'global:member', apiKey: randomApiKey(), }); @@ -693,12 +686,12 @@ describe('POST /workflows', () => { userId: member.id, workflowId: response.body.id, }, - relations: ['workflow', 'role'], + relations: ['workflow'], }); expect(sharedWorkflow?.workflow.name).toBe(name); expect(sharedWorkflow?.workflow.createdAt.toISOString()).toBe(createdAt); - expect(sharedWorkflow?.role).toEqual(workflowOwnerRole); + expect(sharedWorkflow?.role).toEqual('workflow:owner'); }); test('should create workflow history version when licensed', async () => { @@ -1110,13 +1103,13 @@ describe('PUT /workflows/:id', () => { userId: member.id, workflowId: response.body.id, }, - relations: ['workflow', 'role'], + relations: ['workflow'], }); expect(sharedWorkflow?.workflow.name).toBe(payload.name); expect(sharedWorkflow?.workflow.updatedAt.getTime()).toBeGreaterThan( workflow.updatedAt.getTime(), ); - expect(sharedWorkflow?.role).toEqual(workflowOwnerRole); + expect(sharedWorkflow?.role).toEqual('workflow:owner'); }); }); diff --git a/packages/cli/test/integration/role.api.test.ts b/packages/cli/test/integration/role.api.test.ts deleted file mode 100644 index 024f0d6950..0000000000 --- a/packages/cli/test/integration/role.api.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as utils from './shared/utils/'; -import * as testDb from './shared/testDb'; -import { createAdmin, createMember, createOwner } from './shared/db/users'; - -import type { SuperAgentTest } from 'supertest'; -import type { User } from '@db/entities/User'; - -const testServer = utils.setupTestServer({ - endpointGroups: ['role'], - enabledFeatures: ['feat:advancedPermissions'], -}); - -const license = testServer.license; - -describe('GET /roles', () => { - let owner: User; - let admin: User; - let member: User; - - let ownerAgent: SuperAgentTest; - let adminAgent: SuperAgentTest; - let memberAgent: SuperAgentTest; - - let toAgent: Record = {}; - - beforeAll(async () => { - await testDb.truncate(['User']); - - owner = await createOwner(); - admin = await createAdmin(); - member = await createMember(); - - ownerAgent = testServer.authAgentFor(owner); - adminAgent = testServer.authAgentFor(admin); - memberAgent = testServer.authAgentFor(member); - - toAgent = { - owner: ownerAgent, - admin: adminAgent, - member: memberAgent, - }; - }); - - describe('with advanced permissions licensed', () => { - test.each(['owner', 'admin', 'member'])('should return all roles to %s', async (user) => { - license.enable('feat:advancedPermissions'); - - const response = await toAgent[user].get('/roles').expect(200); - - expect(response.body.data).toEqual([ - { scope: 'global', name: 'owner', isAvailable: true }, - { scope: 'global', name: 'member', isAvailable: true }, - { scope: 'global', name: 'admin', isAvailable: true }, - { scope: 'workflow', name: 'owner', isAvailable: true }, - { scope: 'credential', name: 'owner', isAvailable: true }, - { scope: 'credential', name: 'user', isAvailable: true }, - { scope: 'workflow', name: 'editor', isAvailable: true }, - ]); - }); - }); - - describe('with advanced permissions not licensed', () => { - test.each(['owner', 'admin', 'member'])('should return all roles to %s', async (user) => { - license.disable('feat:advancedPermissions'); - - const response = await toAgent[user].get('/roles').expect(200); - - expect(response.body.data).toEqual([ - { scope: 'global', name: 'owner', isAvailable: true }, - { scope: 'global', name: 'member', isAvailable: true }, - { scope: 'global', name: 'admin', isAvailable: false }, - { scope: 'workflow', name: 'owner', isAvailable: true }, - { scope: 'credential', name: 'owner', isAvailable: true }, - { scope: 'credential', name: 'user', isAvailable: true }, - { scope: 'workflow', name: 'editor', isAvailable: true }, - ]); - }); - }); -}); diff --git a/packages/cli/test/integration/shared/db/credentials.ts b/packages/cli/test/integration/shared/db/credentials.ts index f92507ce8b..2208e08936 100644 --- a/packages/cli/test/integration/shared/db/credentials.ts +++ b/packages/cli/test/integration/shared/db/credentials.ts @@ -1,12 +1,11 @@ +import { Container } from 'typedi'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { User } from '@db/entities/User'; -import type { Role } from '@db/entities/Role'; -import type { ICredentialsDb } from '@/Interfaces'; -import { RoleService } from '@/services/role.service'; -import type { CredentialPayload } from '../types'; -import Container from 'typedi'; import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; +import type { CredentialSharingRole } from '@db/entities/SharedCredentials'; +import type { ICredentialsDb } from '@/Interfaces'; +import type { CredentialPayload } from '../types'; async function encryptCredentialData(credential: CredentialsEntity) { const { createCredentialsFromCredentialsEntity } = await import('@/CredentialsHelper'); @@ -48,7 +47,7 @@ export async function createCredentials(attributes: Partial = */ export async function saveCredential( credentialPayload: CredentialPayload, - { user, role }: { user: User; role: Role }, + { user, role }: { user: User; role: CredentialSharingRole }, ) { const newCredential = new CredentialsEntity(); @@ -72,18 +71,17 @@ export async function saveCredential( } export async function shareCredentialWithUsers(credential: CredentialsEntity, users: User[]) { - const role = await Container.get(RoleService).findCredentialUserRole(); const newSharedCredentials = users.map((user) => Container.get(SharedCredentialsRepository).create({ userId: user.id, credentialsId: credential.id, - roleId: role?.id, + role: 'credential:user', }), ); return await Container.get(SharedCredentialsRepository).save(newSharedCredentials); } -export function affixRoleToSaveCredential(role: Role) { +export function affixRoleToSaveCredential(role: CredentialSharingRole) { return async (credentialPayload: CredentialPayload, { user }: { user: User }) => await saveCredential(credentialPayload, { user, role }); } diff --git a/packages/cli/test/integration/shared/db/roles.ts b/packages/cli/test/integration/shared/db/roles.ts deleted file mode 100644 index a525489093..0000000000 --- a/packages/cli/test/integration/shared/db/roles.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Container from 'typedi'; -import { RoleService } from '@/services/role.service'; - -export async function getGlobalOwnerRole() { - return await Container.get(RoleService).findGlobalOwnerRole(); -} - -export async function getGlobalMemberRole() { - return await Container.get(RoleService).findGlobalMemberRole(); -} - -export async function getGlobalAdminRole() { - return await Container.get(RoleService).findGlobalAdminRole(); -} - -export async function getWorkflowOwnerRole() { - return await Container.get(RoleService).findWorkflowOwnerRole(); -} - -export async function getWorkflowEditorRole() { - return await Container.get(RoleService).findWorkflowEditorRole(); -} - -export async function getCredentialOwnerRole() { - return await Container.get(RoleService).findCredentialOwnerRole(); -} - -export async function getAllRoles() { - return await Promise.all([ - getGlobalOwnerRole(), - getGlobalMemberRole(), - getWorkflowOwnerRole(), - getCredentialOwnerRole(), - ]); -} diff --git a/packages/cli/test/integration/shared/db/users.ts b/packages/cli/test/integration/shared/db/users.ts index c4e95cd9c3..27defb2184 100644 --- a/packages/cli/test/integration/shared/db/users.ts +++ b/packages/cli/test/integration/shared/db/users.ts @@ -1,28 +1,25 @@ import Container from 'typedi'; import { hash } from 'bcryptjs'; import { AuthIdentity } from '@db/entities/AuthIdentity'; -import type { Role } from '@db/entities/Role'; -import type { User } from '@db/entities/User'; +import type { GlobalRole, User } from '@db/entities/User'; import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository'; import { UserRepository } from '@db/repositories/user.repository'; import { TOTPService } from '@/Mfa/totp.service'; import { MfaService } from '@/Mfa/mfa.service'; import { randomApiKey, randomEmail, randomName, randomValidPassword } from '../random'; -import { getGlobalAdminRole, getGlobalMemberRole, getGlobalOwnerRole } from './roles'; /** * Store a user in the DB, defaulting to a `member`. */ export async function createUser(attributes: Partial = {}): Promise { - const { email, password, firstName, lastName, globalRole, ...rest } = attributes; + const { email, password, firstName, lastName, role, ...rest } = attributes; const user = Container.get(UserRepository).create({ email: email ?? randomEmail(), password: await hash(password ?? randomValidPassword(), 10), firstName: firstName ?? randomName(), lastName: lastName ?? randomName(), - globalRoleId: (globalRole ?? (await getGlobalMemberRole())).id, - globalRole, + role: role ?? 'global:member', ...rest, }); user.computeIsOwner(); @@ -70,25 +67,21 @@ export async function createUserWithMfaEnabled( } export async function createOwner() { - return await createUser({ globalRole: await getGlobalOwnerRole() }); + return await createUser({ role: 'global:owner' }); } export async function createMember() { - return await createUser({ globalRole: await getGlobalMemberRole() }); + return await createUser({ role: 'global:member' }); } export async function createAdmin() { - return await createUser({ globalRole: await getGlobalAdminRole() }); + return await createUser({ role: 'global:admin' }); } -export async function createUserShell(globalRole: Role): Promise { - if (globalRole.scope !== 'global') { - throw new Error(`Invalid role received: ${JSON.stringify(globalRole)}`); - } +export async function createUserShell(role: GlobalRole): Promise { + const shell: Partial = { role }; - const shell: Partial = { globalRoleId: globalRole.id }; - - if (globalRole.name !== 'owner') { + if (role !== 'global:owner') { shell.email = randomEmail(); } @@ -102,10 +95,7 @@ export async function createManyUsers( amount: number, attributes: Partial = {}, ): Promise { - let { email, password, firstName, lastName, globalRole, ...rest } = attributes; - if (!globalRole) { - globalRole = await getGlobalMemberRole(); - } + let { email, password, firstName, lastName, role, ...rest } = attributes; const users = await Promise.all( [...Array(amount)].map(async () => @@ -114,7 +104,7 @@ export async function createManyUsers( password: await hash(password ?? randomValidPassword(), 10), firstName: firstName ?? randomName(), lastName: lastName ?? randomName(), - globalRole, + role: role ?? 'global:member', ...rest, }), ), @@ -130,13 +120,13 @@ export async function addApiKey(user: User): Promise { export const getAllUsers = async () => await Container.get(UserRepository).find({ - relations: ['globalRole', 'authIdentities'], + relations: ['authIdentities'], }); export const getUserById = async (id: string) => await Container.get(UserRepository).findOneOrFail({ where: { id }, - relations: ['globalRole', 'authIdentities'], + relations: ['authIdentities'], }); export const getLdapIdentities = async () => diff --git a/packages/cli/test/integration/shared/db/workflows.ts b/packages/cli/test/integration/shared/db/workflows.ts index 6356d92dd8..5603db7ab9 100644 --- a/packages/cli/test/integration/shared/db/workflows.ts +++ b/packages/cli/test/integration/shared/db/workflows.ts @@ -1,10 +1,12 @@ import Container from 'typedi'; +import type { DeepPartial } from 'typeorm'; import { v4 as uuid } from 'uuid'; + import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; -import { getWorkflowEditorRole, getWorkflowOwnerRole } from './roles'; +import type { SharedWorkflow } from '@db/entities/SharedWorkflow'; export async function createManyWorkflows( amount: number, @@ -49,18 +51,17 @@ export async function createWorkflow(attributes: Partial = {}, u await Container.get(SharedWorkflowRepository).save({ user, workflow, - role: await getWorkflowOwnerRole(), + role: 'workflow:owner', }); } return workflow; } export async function shareWorkflowWithUsers(workflow: WorkflowEntity, users: User[]) { - const role = await getWorkflowEditorRole(); - const sharedWorkflows = users.map((user) => ({ - user, - workflow, - role, + const sharedWorkflows: Array> = users.map((user) => ({ + userId: user.id, + workflowId: workflow.id, + role: 'workflow:editor', })); return await Container.get(SharedWorkflowRepository).save(sharedWorkflows); } diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index 6670fc1ff9..8f4b764eeb 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -31,7 +31,6 @@ type EndpointGroup = | 'executions' | 'workflowHistory' | 'binaryData' - | 'role' | 'invitations' | 'debug'; diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index 3d9e2cd2bf..aea2602bdd 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -249,11 +249,6 @@ export const setupTestServer = ({ registerController(app, BinaryDataController); break; - case 'role': - const { RoleController } = await import('@/controllers/role.controller'); - registerController(app, RoleController); - break; - case 'debug': const { DebugController } = await import('@/controllers/debug.controller'); registerController(app, DebugController); diff --git a/packages/cli/test/integration/shared/utils/users.ts b/packages/cli/test/integration/shared/utils/users.ts index c2b4d6be9b..c655f754b2 100644 --- a/packages/cli/test/integration/shared/utils/users.ts +++ b/packages/cli/test/integration/shared/utils/users.ts @@ -13,7 +13,7 @@ export const validateUser = (user: PublicUser) => { expect(user.settings).toBe(null); expect(user.personalizationAnswers).toBeNull(); expect(user.password).toBeUndefined(); - expect(user.globalRole).toBeDefined(); + expect(user.role).toBeDefined(); }; export const assertInviteUserSuccessResponse = (data: UserInvitationResponse) => { diff --git a/packages/cli/test/integration/tags.api.test.ts b/packages/cli/test/integration/tags.api.test.ts index bc44fdf00d..73be97a1c2 100644 --- a/packages/cli/test/integration/tags.api.test.ts +++ b/packages/cli/test/integration/tags.api.test.ts @@ -3,15 +3,13 @@ import * as testDb from './shared/testDb'; import type { SuperAgentTest } from 'supertest'; import { TagRepository } from '@db/repositories/tag.repository'; import Container from 'typedi'; -import { getGlobalOwnerRole } from './shared/db/roles'; import { createUserShell } from './shared/db/users'; let authOwnerAgent: SuperAgentTest; const testServer = utils.setupTestServer({ endpointGroups: ['tags'] }); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - const ownerShell = await createUserShell(globalOwnerRole); + const ownerShell = await createUserShell('global:owner'); authOwnerAgent = testServer.authAgentFor(ownerShell); }); diff --git a/packages/cli/test/integration/role.repository.test.ts b/packages/cli/test/integration/user.repository.test.ts similarity index 60% rename from packages/cli/test/integration/role.repository.test.ts rename to packages/cli/test/integration/user.repository.test.ts index 04e645928d..6929326b95 100644 --- a/packages/cli/test/integration/role.repository.test.ts +++ b/packages/cli/test/integration/user.repository.test.ts @@ -1,15 +1,15 @@ +import Container from 'typedi'; +import { UserRepository } from '@db/repositories/user.repository'; import { createAdmin, createMember, createOwner } from './shared/db/users'; import * as testDb from './shared/testDb'; -import { RoleRepository } from '@/databases/repositories/role.repository'; -import Container from 'typedi'; -describe('RoleRepository', () => { - let roleRepository: RoleRepository; +describe('UserRepository', () => { + let userRepository: UserRepository; beforeAll(async () => { await testDb.init(); - roleRepository = Container.get(RoleRepository); + userRepository = Container.get(UserRepository); await testDb.truncate(['User']); }); @@ -29,9 +29,13 @@ describe('RoleRepository', () => { createMember(), ]); - const usersByRole = await roleRepository.countUsersByRole(); + const usersByRole = await userRepository.countUsersByRole(); - expect(usersByRole).toStrictEqual({ admin: 2, member: 3, owner: 1 }); + expect(usersByRole).toStrictEqual({ + 'global:admin': 2, + 'global:member': 3, + 'global:owner': 1, + }); }); }); }); diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 4b108e6753..fefb0161b1 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -1,12 +1,14 @@ import Container from 'typedi'; -import { UserRepository } from '@db/repositories/user.repository'; +import type { SuperAgentTest } from 'supertest'; import { UsersController } from '@/controllers/users.controller'; +import type { User } from '@db/entities/User'; +import { UserRepository } from '@db/repositories/user.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; +import { ExecutionService } from '@/executions/execution.service'; import { getCredentialById, saveCredential } from './shared/db/credentials'; -import { getCredentialOwnerRole, getWorkflowOwnerRole } from './shared/db/roles'; import { createAdmin, createMember, createOwner, getUserById } from './shared/db/users'; import { createWorkflow, getWorkflowById } from './shared/db/workflows'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; @@ -14,11 +16,6 @@ import { validateUser } from './shared/utils/users'; import { randomName } from './shared/random'; import * as utils from './shared/utils/'; import * as testDb from './shared/testDb'; - -import type { SuperAgentTest } from 'supertest'; -import type { Role } from '@db/entities/Role'; -import type { User } from '@db/entities/User'; -import { ExecutionService } from '@/executions/execution.service'; import { mockInstance } from '../shared/mocking'; mockInstance(ExecutionService); @@ -216,7 +213,6 @@ describe('GET /users', () => { /** * Some list query options require auxiliary fields: * - * - `isOwner` requires `globalRole` * - `select` with `take` requires `id` (for pagination) */ test('should support options that require auxiliary fields', async () => { @@ -235,8 +231,6 @@ describe('DELETE /users/:id', () => { let owner: User; let member: User; let ownerAgent: SuperAgentTest; - let workflowOwnerRole: Role; - let credentialOwnerRole: Role; beforeAll(async () => { await testDb.truncate(['User']); @@ -244,9 +238,6 @@ describe('DELETE /users/:id', () => { owner = await createOwner(); member = await createMember(); ownerAgent = testServer.authAgentFor(owner); - - workflowOwnerRole = await getWorkflowOwnerRole(); - credentialOwnerRole = await getCredentialOwnerRole(); }); test('should delete user and their resources', async () => { @@ -254,7 +245,7 @@ describe('DELETE /users/:id', () => { const savedCredential = await saveCredential( { name: randomName(), type: '', data: {}, nodesAccess: [] }, - { user: member, role: credentialOwnerRole }, + { user: member, role: 'credential:owner' }, ); const response = await ownerAgent.delete(`/users/${member.id}`); @@ -266,12 +257,12 @@ describe('DELETE /users/:id', () => { const sharedWorkflow = await Container.get(SharedWorkflowRepository).findOne({ relations: ['user'], - where: { userId: member.id, roleId: workflowOwnerRole.id }, + where: { userId: member.id, role: 'workflow:owner' }, }); const sharedCredential = await Container.get(SharedCredentialsRepository).findOne({ relations: ['user'], - where: { userId: member.id, roleId: credentialOwnerRole.id }, + where: { userId: member.id, role: 'credential:owner' }, }); const workflow = await getWorkflowById(savedWorkflow.id); @@ -298,7 +289,7 @@ describe('DELETE /users/:id', () => { { name: randomName(), type: '', data: {}, nodesAccess: [] }, { user: member, - role: credentialOwnerRole, + role: 'credential:owner', }, ), ]); @@ -386,7 +377,7 @@ describe('PATCH /users/:id/role', () => { describe('unauthenticated user', () => { test('should receive 401', async () => { const response = await authlessAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(401); @@ -396,13 +387,13 @@ describe('PATCH /users/:id/role', () => { describe('Invalid payload should return 400 when newRoleName', () => { test.each([ ['is missing', {}], - ['is `owner`', { newRoleName: 'owner' }], - ['is an array', { newRoleName: ['owner'] }], + ['is `owner`', { newRoleName: 'global:owner' }], + ['is an array', { newRoleName: ['global:owner'] }], ])('%s', async (_, payload) => { const response = await adminAgent.patch(`/users/${member.id}/role`).send(payload); expect(response.statusCode).toBe(400); expect(response.body.message).toBe( - 'newRoleName must be one of the following values: member, admin', + 'newRoleName must be one of the following values: global:admin, global:member', ); }); }); @@ -410,7 +401,7 @@ describe('PATCH /users/:id/role', () => { describe('member', () => { test('should fail to demote owner to member', async () => { const response = await memberAgent.patch(`/users/${owner.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(403); @@ -419,7 +410,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to demote owner to admin', async () => { const response = await memberAgent.patch(`/users/${owner.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -428,7 +419,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to demote admin to member', async () => { const response = await memberAgent.patch(`/users/${admin.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(403); @@ -437,7 +428,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to promote other member to owner', async () => { const response = await memberAgent.patch(`/users/${otherMember.id}/role`).send({ - newRoleName: 'owner', + newRoleName: 'global:owner', }); expect(response.statusCode).toBe(403); @@ -446,7 +437,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to promote other member to admin', async () => { const response = await memberAgent.patch(`/users/${otherMember.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -455,7 +446,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to promote self to admin', async () => { const response = await memberAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -464,7 +455,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to promote self to owner', async () => { const response = await memberAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'owner', + newRoleName: 'global:owner', }); expect(response.statusCode).toBe(403); @@ -477,7 +468,7 @@ describe('PATCH /users/:id/role', () => { const response = await adminAgent .patch('/users/c2317ff3-7a9f-4fd4-ad2b-7331f6359260/role') .send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(404); @@ -486,7 +477,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to demote owner to admin', async () => { const response = await adminAgent.patch(`/users/${owner.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -495,7 +486,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to demote owner to member', async () => { const response = await adminAgent.patch(`/users/${owner.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(403); @@ -506,7 +497,7 @@ describe('PATCH /users/:id/role', () => { testServer.license.disable('feat:advancedPermissions'); const response = await adminAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -515,7 +506,7 @@ describe('PATCH /users/:id/role', () => { test('should be able to demote admin to member', async () => { const response = await adminAgent.patch(`/users/${otherAdmin.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(200); @@ -523,8 +514,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(otherAdmin.id); - expect(user.globalRole.scope).toBe('global'); - expect(user.globalRole.name).toBe('member'); + expect(user.role).toBe('global:member'); // restore other admin @@ -534,7 +524,7 @@ describe('PATCH /users/:id/role', () => { test('should be able to demote self to member', async () => { const response = await adminAgent.patch(`/users/${admin.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(200); @@ -542,8 +532,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.globalRole.scope).toBe('global'); - expect(user.globalRole.name).toBe('member'); + expect(user.role).toBe('global:member'); // restore admin @@ -553,7 +542,7 @@ describe('PATCH /users/:id/role', () => { test('should be able to promote member to admin if licensed', async () => { const response = await adminAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(200); @@ -561,8 +550,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.globalRole.scope).toBe('global'); - expect(user.globalRole.name).toBe('admin'); + expect(user.role).toBe('global:admin'); // restore member @@ -574,7 +562,7 @@ describe('PATCH /users/:id/role', () => { describe('owner', () => { test('should fail to demote self to admin', async () => { const response = await ownerAgent.patch(`/users/${owner.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -583,7 +571,7 @@ describe('PATCH /users/:id/role', () => { test('should fail to demote self to member', async () => { const response = await ownerAgent.patch(`/users/${owner.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(403); @@ -594,7 +582,7 @@ describe('PATCH /users/:id/role', () => { testServer.license.disable('feat:advancedPermissions'); const response = await ownerAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(403); @@ -603,7 +591,7 @@ describe('PATCH /users/:id/role', () => { test('should be able to promote member to admin if licensed', async () => { const response = await ownerAgent.patch(`/users/${member.id}/role`).send({ - newRoleName: 'admin', + newRoleName: 'global:admin', }); expect(response.statusCode).toBe(200); @@ -611,8 +599,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.globalRole.scope).toBe('global'); - expect(user.globalRole.name).toBe('admin'); + expect(user.role).toBe('global:admin'); // restore member @@ -622,7 +609,7 @@ describe('PATCH /users/:id/role', () => { test('should be able to demote admin to member', async () => { const response = await ownerAgent.patch(`/users/${admin.id}/role`).send({ - newRoleName: 'member', + newRoleName: 'global:member', }); expect(response.statusCode).toBe(200); @@ -630,8 +617,7 @@ describe('PATCH /users/:id/role', () => { const user = await getUserById(admin.id); - expect(user.globalRole.scope).toBe('global'); - expect(user.globalRole.name).toBe('member'); + expect(user.role).toBe('global:member'); // restore admin diff --git a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts index bf7472c69a..222ef6ebf7 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.ee.test.ts @@ -3,7 +3,6 @@ import type { SuperAgentTest } from 'supertest'; import { v4 as uuid } from 'uuid'; import type { INode } from 'n8n-workflow'; -import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -17,17 +16,11 @@ import type { SaveCredentialFunction } from '../shared/types'; import { makeWorkflow } from '../shared/utils/'; import { randomCredentialPayload } from '../shared/random'; import { affixRoleToSaveCredential, shareCredentialWithUsers } from '../shared/db/credentials'; -import { - getCredentialOwnerRole, - getGlobalMemberRole, - getGlobalOwnerRole, -} from '../shared/db/roles'; import { createUser } from '../shared/db/users'; import { createWorkflow, getWorkflowSharing, shareWorkflowWithUsers } from '../shared/db/workflows'; import { License } from '@/License'; import { UserManagementMailer } from '@/UserManagement/email'; -let globalMemberRole: Role; let owner: User; let member: User; let anotherMember: User; @@ -48,19 +41,15 @@ const license = testServer.license; const mailer = mockInstance(UserManagementMailer); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - globalMemberRole = await getGlobalMemberRole(); - const credentialOwnerRole = await getCredentialOwnerRole(); - - owner = await createUser({ globalRole: globalOwnerRole }); - member = await createUser({ globalRole: globalMemberRole }); - anotherMember = await createUser({ globalRole: globalMemberRole }); + owner = await createUser({ role: 'global:owner' }); + member = await createUser({ role: 'global:member' }); + anotherMember = await createUser({ role: 'global:member' }); authOwnerAgent = testServer.authAgentFor(owner); authMemberAgent = testServer.authAgentFor(member); authAnotherMemberAgent = testServer.authAgentFor(anotherMember); - saveCredential = affixRoleToSaveCredential(credentialOwnerRole); + saveCredential = affixRoleToSaveCredential('credential:owner'); await utils.initNodeTypes(); }); @@ -226,7 +215,7 @@ describe('PUT /workflows/:id', () => { test('PUT /workflows/:id/share should not allow sharing by another non-shared member', async () => { const workflow = await createWorkflow({}, member); - const tempUser = await createUser({ globalRole: globalMemberRole }); + const tempUser = await createUser({ role: 'global:member' }); const response = await authAnotherMemberAgent .put(`/workflows/${workflow.id}/share`) @@ -1003,7 +992,7 @@ describe('PATCH /workflows/:id - validate interim updates', () => { describe('getSharedWorkflowIds', () => { it('should show all workflows to owners', async () => { - owner.globalRole = await getGlobalOwnerRole(); + owner.role = 'global:owner'; const workflow1 = await createWorkflow({}, member); const workflow2 = await createWorkflow({}, anotherMember); const sharedWorkflowIds = @@ -1014,7 +1003,7 @@ describe('getSharedWorkflowIds', () => { }); it('should show shared workflows to users', async () => { - member.globalRole = await getGlobalMemberRole(); + member.role = 'global:member'; const workflow1 = await createWorkflow({}, anotherMember); const workflow2 = await createWorkflow({}, anotherMember); const workflow3 = await createWorkflow({}, anotherMember); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 345a466084..b02c351b4d 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -6,7 +6,6 @@ import type { INode, IPinData } from 'n8n-workflow'; import type { User } from '@db/entities/User'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; -import { RoleService } from '@/services/role.service'; import type { ListQuery } from '@/requests'; import { WorkflowHistoryRepository } from '@db/repositories/workflowHistory.repository'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -180,7 +179,7 @@ describe('GET /workflows', () => { test('should return workflows', async () => { const credential = await saveCredential(randomCredentialPayload(), { user: owner, - role: await Container.get(RoleService).findCredentialOwnerRole(), + role: 'credential:owner', }); const nodes: INode[] = [ diff --git a/packages/cli/test/unit/PermissionChecker.test.ts b/packages/cli/test/unit/PermissionChecker.test.ts index 7c68fd3d94..5eb4b6e0ea 100644 --- a/packages/cli/test/unit/PermissionChecker.test.ts +++ b/packages/cli/test/unit/PermissionChecker.test.ts @@ -4,7 +4,6 @@ import type { WorkflowSettings } from 'n8n-workflow'; import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; import config from '@/config'; -import type { Role } from '@db/entities/Role'; import { User } from '@db/entities/User'; import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; @@ -27,7 +26,6 @@ import * as testDb from '../integration/shared/testDb'; import type { SaveCredentialFunction } from '../integration/shared/types'; import { mockNodeTypesData } from './Helpers'; import { affixRoleToSaveCredential } from '../integration/shared/db/credentials'; -import { getCredentialOwnerRole, getWorkflowOwnerRole } from '../integration/shared/db/roles'; import { createOwner, createUser } from '../integration/shared/db/users'; export const toTargetCallErrorMsg = (subworkflowId: string) => @@ -71,8 +69,6 @@ export function createSubworkflow({ }); } -let credentialOwnerRole: Role; -let workflowOwnerRole: Role; let saveCredential: SaveCredentialFunction; const mockNodeTypes = mockInstance(NodeTypes); @@ -85,10 +81,7 @@ let permissionChecker: PermissionChecker; beforeAll(async () => { await testDb.init(); - credentialOwnerRole = await getCredentialOwnerRole(); - workflowOwnerRole = await getWorkflowOwnerRole(); - - saveCredential = affixRoleToSaveCredential(credentialOwnerRole); + saveCredential = affixRoleToSaveCredential('credential:owner'); permissionChecker = Container.get(PermissionChecker); }); @@ -251,7 +244,7 @@ describe('check()', () => { await Container.get(SharedWorkflowRepository).save({ workflow: workflowEntity, user: member, - role: workflowOwnerRole, + role: 'workflow:owner', }); const workflow = new Workflow(workflowDetails); diff --git a/packages/cli/test/unit/WorkflowRunner.test.ts b/packages/cli/test/unit/WorkflowRunner.test.ts index 876ae1ac1c..0317793459 100644 --- a/packages/cli/test/unit/WorkflowRunner.test.ts +++ b/packages/cli/test/unit/WorkflowRunner.test.ts @@ -8,7 +8,6 @@ import config from '@/config'; import { mockInstance } from '../shared/mocking'; import * as testDb from '../integration/shared/testDb'; import { setupTestServer } from '../integration/shared/utils'; -import { getGlobalOwnerRole } from '../integration/shared/db/roles'; import { createUser } from '../integration/shared/db/users'; import { createWorkflow } from '../integration/shared/db/workflows'; import { createExecution } from '../integration/shared/db/executions'; @@ -25,8 +24,7 @@ const watchers = new Watchers(); const watchedWorkflowExecuteAfter = jest.spyOn(watchers, 'workflowExecuteAfter'); beforeAll(async () => { - const globalOwnerRole = await getGlobalOwnerRole(); - owner = await createUser({ globalRole: globalOwnerRole }); + owner = await createUser({ role: 'global:owner' }); mockInstance(Push); Container.set(Push, new Push()); diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index aaaea5a86a..3359e592aa 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -44,7 +44,7 @@ describe('MeController', () => { id: '123', password: 'password', authIdentities: [], - globalRoleId: '1', + role: 'global:owner', }); const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const req = mock({ user, body: reqBody }); @@ -79,7 +79,7 @@ describe('MeController', () => { id: '123', password: 'password', authIdentities: [], - globalRoleId: '1', + role: 'global:owner', }); const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const req = mock({ user, body: reqBody }); @@ -88,7 +88,7 @@ describe('MeController', () => { jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); // Add invalid data to the request payload - Object.assign(reqBody, { id: '0', globalRoleId: '42' }); + Object.assign(reqBody, { id: '0', role: '42' }); await controller.updateCurrentUser(req, res); @@ -99,7 +99,7 @@ describe('MeController', () => { expect(updatedUser.firstName).toBe(reqBody.firstName); expect(updatedUser.lastName).toBe(reqBody.lastName); expect(updatedUser.id).not.toBe('0'); - expect(updatedUser.globalRoleId).not.toBe('42'); + expect(updatedUser.role).not.toBe('42'); }); it('should throw BadRequestError if beforeUpdate hook throws BadRequestError', async () => { @@ -107,11 +107,11 @@ describe('MeController', () => { id: '123', password: 'password', authIdentities: [], - globalRoleId: '1', + role: 'global:owner', }); const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const req = mock({ user, body: reqBody }); - userService.findOneOrFail.mockResolvedValue(user); + // userService.findOneOrFail.mockResolvedValue(user); externalHooks.run.mockImplementationOnce(async (hookName) => { if (hookName === 'user.profile.beforeUpdate') { diff --git a/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts b/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts index 0ea169261c..59d9b8f636 100644 --- a/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts +++ b/packages/cli/test/unit/controllers/oAuth1Credential.controller.test.ts @@ -34,7 +34,7 @@ describe('OAuth1CredentialController', () => { id: '123', password: 'password', authIdentities: [], - globalRoleId: '1', + role: 'global:owner', }); const credential = mock({ id: '1', diff --git a/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts b/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts index 3498f6a532..9acbe305be 100644 --- a/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts +++ b/packages/cli/test/unit/controllers/oAuth2Credential.controller.test.ts @@ -38,7 +38,7 @@ describe('OAuth2CredentialController', () => { id: '123', password: 'password', authIdentities: [], - globalRoleId: '1', + role: 'global:owner', }); const credential = mock({ id: '1', diff --git a/packages/cli/test/unit/controllers/owner.controller.test.ts b/packages/cli/test/unit/controllers/owner.controller.test.ts index 90c6e02eb6..97191a7a69 100644 --- a/packages/cli/test/unit/controllers/owner.controller.test.ts +++ b/packages/cli/test/unit/controllers/owner.controller.test.ts @@ -76,7 +76,7 @@ describe('OwnerController', () => { it('should setup the instance owner successfully', async () => { const user = mock({ id: 'userId', - globalRole: { scope: 'global', name: 'owner' }, + role: 'global:owner', authIdentities: [], }); const req = mock({ diff --git a/packages/cli/test/unit/controllers/translation.controller.test.ts b/packages/cli/test/unit/controllers/translation.controller.test.ts index b56fdf2a58..e34237cd69 100644 --- a/packages/cli/test/unit/controllers/translation.controller.test.ts +++ b/packages/cli/test/unit/controllers/translation.controller.test.ts @@ -1,5 +1,4 @@ import { mock } from 'jest-mock-extended'; -import type { ICredentialTypes } from 'n8n-workflow'; import config from '@/config'; import type { TranslationRequest } from '@/controllers/translation.controller'; import { @@ -7,10 +6,11 @@ import { CREDENTIAL_TRANSLATIONS_DIR, } from '@/controllers/translation.controller'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import type { CredentialTypes } from '@/CredentialTypes'; describe('TranslationController', () => { const configGetSpy = jest.spyOn(config, 'getEnv'); - const credentialTypes = mock(); + const credentialTypes = mock(); const controller = new TranslationController(credentialTypes); describe('getCredentialTranslation', () => { diff --git a/packages/cli/test/unit/repositories/role.repository.test.ts b/packages/cli/test/unit/repositories/role.repository.test.ts deleted file mode 100644 index 40d95207ac..0000000000 --- a/packages/cli/test/unit/repositories/role.repository.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Container } from 'typedi'; -import { DataSource, EntityManager } from 'typeorm'; -import { mock } from 'jest-mock-extended'; -import type { RoleNames, RoleScopes } from '@db/entities/Role'; -import { Role } from '@db/entities/Role'; -import { RoleRepository } from '@db/repositories/role.repository'; -import { mockInstance } from '../../shared/mocking'; -import { randomInteger } from '../../integration/shared/random'; - -describe('RoleRepository', () => { - const entityManager = mockInstance(EntityManager); - const dataSource = mockInstance(DataSource, { manager: entityManager }); - dataSource.getMetadata.mockReturnValue(mock()); - Object.assign(entityManager, { connection: dataSource }); - const roleRepository = Container.get(RoleRepository); - - describe('findRole', () => { - test('should return the role when present', async () => { - entityManager.findOne.mockResolvedValueOnce(createRole('global', 'owner')); - const role = await roleRepository.findRole('global', 'owner'); - expect(role?.name).toEqual('owner'); - expect(role?.scope).toEqual('global'); - }); - - test('should return null otherwise', async () => { - entityManager.findOne.mockResolvedValueOnce(null); - const role = await roleRepository.findRole('global', 'owner'); - expect(role).toEqual(null); - }); - }); - - const createRole = (scope: RoleScopes, name: RoleNames) => - Object.assign(new Role(), { name, scope, id: `${randomInteger()}` }); -}); diff --git a/packages/cli/test/unit/services/ownership.service.test.ts b/packages/cli/test/unit/services/ownership.service.test.ts index 20be942c2f..3fed4b8ce7 100644 --- a/packages/cli/test/unit/services/ownership.service.test.ts +++ b/packages/cli/test/unit/services/ownership.service.test.ts @@ -1,181 +1,135 @@ import { OwnershipService } from '@/services/ownership.service'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; -import { Role } from '@db/entities/Role'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { User } from '@db/entities/User'; -import { RoleService } from '@/services/role.service'; import type { SharedCredentials } from '@db/entities/SharedCredentials'; import { mockInstance } from '../../shared/mocking'; import { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; import { UserRepository } from '@/databases/repositories/user.repository'; import { mock } from 'jest-mock-extended'; -import { - mockCredRole, - mockCredential, - mockUser, - mockInstanceOwnerRole, - wfOwnerRole, -} from '../shared/mockObjects'; +import { mockCredential, mockUser } from '../shared/mockObjects'; describe('OwnershipService', () => { - const roleService = mockInstance(RoleService); const userRepository = mockInstance(UserRepository); const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository); - - const ownershipService = new OwnershipService( - mock(), - userRepository, - roleService, - sharedWorkflowRepository, - ); + const ownershipService = new OwnershipService(mock(), userRepository, sharedWorkflowRepository); beforeEach(() => { jest.clearAllMocks(); }); - describe('OwnershipService', () => { - const roleService = mockInstance(RoleService); - const userRepository = mockInstance(UserRepository); - const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository); + describe('getWorkflowOwner()', () => { + test('should retrieve a workflow owner', async () => { + const mockOwner = new User(); + const mockNonOwner = new User(); - const ownershipService = new OwnershipService( - mock(), - userRepository, - roleService, - sharedWorkflowRepository, - ); + const sharedWorkflow = Object.assign(new SharedWorkflow(), { + role: 'workflow:owner', + user: mockOwner, + }); - beforeEach(() => { - jest.clearAllMocks(); + sharedWorkflowRepository.findOneOrFail.mockResolvedValueOnce(sharedWorkflow); + + const returnedOwner = await ownershipService.getWorkflowOwnerCached('some-workflow-id'); + + expect(returnedOwner).toBe(mockOwner); + expect(returnedOwner).not.toBe(mockNonOwner); }); - describe('getWorkflowOwner()', () => { - test('should retrieve a workflow owner', async () => { - roleService.findWorkflowOwnerRole.mockResolvedValueOnce(wfOwnerRole()); + test('should throw if no workflow owner found', async () => { + sharedWorkflowRepository.findOneOrFail.mockRejectedValue(new Error()); - const mockOwner = new User(); - const mockNonOwner = new User(); + await expect(ownershipService.getWorkflowOwnerCached('some-workflow-id')).rejects.toThrow(); + }); + }); - const sharedWorkflow = Object.assign(new SharedWorkflow(), { - role: new Role(), - user: mockOwner, - }); + describe('addOwnedByAndSharedWith()', () => { + test('should add `ownedBy` and `sharedWith` to credential', async () => { + const owner = mockUser(); + const editor = mockUser(); - sharedWorkflowRepository.findOneOrFail.mockResolvedValueOnce(sharedWorkflow); + const credential = mockCredential(); - const returnedOwner = await ownershipService.getWorkflowOwnerCached('some-workflow-id'); + credential.shared = [ + { role: 'credential:owner', user: owner }, + { role: 'credential:editor', user: editor }, + ] as SharedCredentials[]; - expect(returnedOwner).toBe(mockOwner); - expect(returnedOwner).not.toBe(mockNonOwner); + const { ownedBy, sharedWith } = ownershipService.addOwnedByAndSharedWith(credential); + + expect(ownedBy).toStrictEqual({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, }); - test('should throw if no workflow owner role found', async () => { - roleService.findWorkflowOwnerRole.mockRejectedValueOnce(new Error()); - - await expect(ownershipService.getWorkflowOwnerCached('some-workflow-id')).rejects.toThrow(); - }); - - test('should throw if no workflow owner found', async () => { - roleService.findWorkflowOwnerRole.mockResolvedValueOnce(wfOwnerRole()); - - sharedWorkflowRepository.findOneOrFail.mockRejectedValue(new Error()); - - await expect(ownershipService.getWorkflowOwnerCached('some-workflow-id')).rejects.toThrow(); - }); + expect(sharedWith).toStrictEqual([ + { + id: editor.id, + email: editor.email, + firstName: editor.firstName, + lastName: editor.lastName, + }, + ]); }); - describe('addOwnedByAndSharedWith()', () => { - test('should add `ownedBy` and `sharedWith` to credential', async () => { - const owner = mockUser(); - const editor = mockUser(); + test('should add `ownedBy` and `sharedWith` to workflow', async () => { + const owner = mockUser(); + const editor = mockUser(); - const credential = mockCredential(); + const workflow = new WorkflowEntity(); - credential.shared = [ - { role: mockCredRole('owner'), user: owner }, - { role: mockCredRole('editor'), user: editor }, - ] as SharedCredentials[]; + workflow.shared = [ + { role: 'workflow:owner', user: owner }, + { role: 'workflow:editor', user: editor }, + ] as SharedWorkflow[]; - const { ownedBy, sharedWith } = ownershipService.addOwnedByAndSharedWith(credential); + const { ownedBy, sharedWith } = ownershipService.addOwnedByAndSharedWith(workflow); - expect(ownedBy).toStrictEqual({ - id: owner.id, - email: owner.email, - firstName: owner.firstName, - lastName: owner.lastName, - }); - - expect(sharedWith).toStrictEqual([ - { - id: editor.id, - email: editor.email, - firstName: editor.firstName, - lastName: editor.lastName, - }, - ]); + expect(ownedBy).toStrictEqual({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, }); - test('should add `ownedBy` and `sharedWith` to workflow', async () => { - const owner = mockUser(); - const editor = mockUser(); - - const workflow = new WorkflowEntity(); - - workflow.shared = [ - { role: mockCredRole('owner'), user: owner }, - { role: mockCredRole('editor'), user: editor }, - ] as SharedWorkflow[]; - - const { ownedBy, sharedWith } = ownershipService.addOwnedByAndSharedWith(workflow); - - expect(ownedBy).toStrictEqual({ - id: owner.id, - email: owner.email, - firstName: owner.firstName, - lastName: owner.lastName, - }); - - expect(sharedWith).toStrictEqual([ - { - id: editor.id, - email: editor.email, - firstName: editor.firstName, - lastName: editor.lastName, - }, - ]); - }); - - test('should produce an empty sharedWith if no sharee', async () => { - const owner = mockUser(); - - const credential = mockCredential(); - - credential.shared = [{ role: mockCredRole('owner'), user: owner }] as SharedCredentials[]; - - const { ownedBy, sharedWith } = ownershipService.addOwnedByAndSharedWith(credential); - - expect(ownedBy).toStrictEqual({ - id: owner.id, - email: owner.email, - firstName: owner.firstName, - lastName: owner.lastName, - }); - - expect(sharedWith).toHaveLength(0); - }); + expect(sharedWith).toStrictEqual([ + { + id: editor.id, + email: editor.email, + firstName: editor.firstName, + lastName: editor.lastName, + }, + ]); }); - describe('getInstanceOwner()', () => { - test('should find owner using global owner role ID', async () => { - const instanceOwnerRole = mockInstanceOwnerRole(); - roleService.findGlobalOwnerRole.mockResolvedValue(instanceOwnerRole); + test('should produce an empty sharedWith if no sharee', async () => { + const owner = mockUser(); - await ownershipService.getInstanceOwner(); + const credential = mockCredential(); - expect(userRepository.findOneOrFail).toHaveBeenCalledWith({ - where: { globalRoleId: instanceOwnerRole.id }, - relations: ['globalRole'], - }); + credential.shared = [{ role: 'credential:owner', user: owner }] as SharedCredentials[]; + + const { ownedBy, sharedWith } = ownershipService.addOwnedByAndSharedWith(credential); + + expect(ownedBy).toStrictEqual({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, + }); + + expect(sharedWith).toHaveLength(0); + }); + }); + + describe('getInstanceOwner()', () => { + test('should find owner using global owner role ID', async () => { + await ownershipService.getInstanceOwner(); + + expect(userRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { role: 'global:owner' }, }); }); }); diff --git a/packages/cli/test/unit/services/role.service.test.ts b/packages/cli/test/unit/services/role.service.test.ts deleted file mode 100644 index 19c26ae4af..0000000000 --- a/packages/cli/test/unit/services/role.service.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; -import type { RoleNames, RoleScopes } from '@db/entities/Role'; -import { Role } from '@db/entities/Role'; -import { RoleService } from '@/services/role.service'; -import { RoleRepository } from '@db/repositories/role.repository'; -import { CacheService } from '@/services/cache/cache.service'; -import { SharedWorkflow } from '@db/entities/SharedWorkflow'; -import { mockInstance } from '../../shared/mocking'; -import { chooseRandomly } from '../../integration/shared/random'; -import config from '@/config'; - -const ROLE_PROPS: Array<{ name: RoleNames; scope: RoleScopes }> = [ - { name: 'owner', scope: 'global' }, - { name: 'member', scope: 'global' }, - { name: 'owner', scope: 'workflow' }, - { name: 'owner', scope: 'credential' }, - { name: 'user', scope: 'credential' }, - { name: 'editor', scope: 'workflow' }, -]; - -export const uppercaseInitial = (str: string) => str[0].toUpperCase() + str.slice(1); - -describe('RoleService', () => { - const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository); - const roleRepository = mockInstance(RoleRepository); - const cacheService = mockInstance(CacheService); - const roleService = new RoleService(roleRepository, sharedWorkflowRepository, cacheService); - - const userId = '1'; - const workflowId = '42'; - - const { name, scope } = chooseRandomly(ROLE_PROPS); - - const display = { - name: uppercaseInitial(name), - scope: uppercaseInitial(scope), - }; - - beforeEach(() => { - config.load(config.default); - jest.clearAllMocks(); - }); - - [true, false].forEach((cacheEnabled) => { - const tag = ['cache', cacheEnabled ? 'enabled' : 'disabled'].join(' '); - - describe(`find${display.scope}${display.name}Role() [${tag}]`, () => { - test(`should return the ${scope} ${name} role if found`, async () => { - config.set('cache.enabled', cacheEnabled); - - const role = roleRepository.create({ name, scope }); - roleRepository.findRole.mockResolvedValueOnce(role); - const returnedRole = await roleRepository.findRole(scope, name); - - expect(returnedRole).toBe(role); - }); - }); - - describe(`findRoleByUserAndWorkflow() [${tag}]`, () => { - test('should return the role if a shared workflow is found', async () => { - config.set('cache.enabled', cacheEnabled); - - const sharedWorkflow = Object.assign(new SharedWorkflow(), { role: new Role() }); - sharedWorkflowRepository.findOne.mockResolvedValueOnce(sharedWorkflow); - const returnedRole = await roleService.findRoleByUserAndWorkflow(userId, workflowId); - - expect(returnedRole).toBe(sharedWorkflow.role); - }); - - test('should return undefined if no shared workflow is found', async () => { - config.set('cache.enabled', cacheEnabled); - - sharedWorkflowRepository.findOne.mockResolvedValueOnce(null); - const returnedRole = await roleService.findRoleByUserAndWorkflow(userId, workflowId); - - expect(returnedRole).toBeUndefined(); - }); - }); - }); -}); diff --git a/packages/cli/test/unit/services/user.service.test.ts b/packages/cli/test/unit/services/user.service.test.ts index 56eb26194b..dca81305af 100644 --- a/packages/cli/test/unit/services/user.service.test.ts +++ b/packages/cli/test/unit/services/user.service.test.ts @@ -6,14 +6,12 @@ import { User } from '@db/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; import { UserService } from '@/services/user.service'; import { mockInstance } from '../../shared/mocking'; -import { RoleService } from '@/services/role.service'; import { v4 as uuid } from 'uuid'; describe('UserService', () => { config.set('userManagement.jwtSecret', 'random-secret'); mockInstance(Logger); - mockInstance(RoleService); const userRepository = mockInstance(UserRepository); const userService = Container.get(UserService); diff --git a/packages/cli/test/unit/shared/mockObjects.ts b/packages/cli/test/unit/shared/mockObjects.ts index dee4d97150..baa6cf4740 100644 --- a/packages/cli/test/unit/shared/mockObjects.ts +++ b/packages/cli/test/unit/shared/mockObjects.ts @@ -1,5 +1,4 @@ import { User } from '@db/entities/User'; -import { Role } from '@db/entities/Role'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { @@ -9,20 +8,6 @@ import { randomName, } from '../../integration/shared/random'; -export const wfOwnerRole = () => - Object.assign(new Role(), { - scope: 'workflow', - name: 'owner', - id: randomInteger(), - }); - -export const mockCredRole = (name: 'owner' | 'editor'): Role => - Object.assign(new Role(), { - scope: 'credentials', - name, - id: randomInteger(), - }); - export const mockCredential = (): CredentialsEntity => Object.assign(new CredentialsEntity(), randomCredentialPayload()); @@ -33,10 +18,3 @@ export const mockUser = (): User => firstName: randomName(), lastName: randomName(), }); - -export const mockInstanceOwnerRole = () => - Object.assign(new Role(), { - scope: 'global', - name: 'owner', - id: randomInteger(), - }); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 3861dbfce3..8084fdbd08 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -685,9 +685,9 @@ export type IPersonalizationSurveyVersions = | IPersonalizationSurveyAnswersV2 | IPersonalizationSurveyAnswersV3; -export type IRole = 'default' | 'owner' | 'member' | 'admin'; +export type IRole = 'default' | 'global:owner' | 'global:member' | 'global:admin'; -export type InvitableRoleName = 'member' | 'admin'; +export type InvitableRoleName = 'global:member' | 'global:admin'; export interface IUserResponse { id: string; @@ -695,11 +695,7 @@ export interface IUserResponse { lastName?: string; email?: string; createdAt?: string; - globalRole?: { - name: IRole; - id: string; - createdAt: Date; - }; + role?: IRole; globalScopes?: Scope[]; personalizationAnswers?: IPersonalizationSurveyVersions | null; isPending: boolean; @@ -720,7 +716,6 @@ export interface IUser extends IUserResponse { fullName?: string; createdAt?: string; mfaEnabled: boolean; - globalRoleId?: number; } export interface IVersionNotificationSettings { diff --git a/packages/editor-ui/src/api/users.ts b/packages/editor-ui/src/api/users.ts index d462475d92..49c532d9b8 100644 --- a/packages/editor-ui/src/api/users.ts +++ b/packages/editor-ui/src/api/users.ts @@ -157,7 +157,7 @@ export async function submitPersonalizationSurvey( export interface UpdateGlobalRolePayload { id: string; - newRoleName: Exclude; + newRoleName: Exclude; } export async function updateGlobalRole( diff --git a/packages/editor-ui/src/components/InviteUsersModal.vue b/packages/editor-ui/src/components/InviteUsersModal.vue index 18a56e32c8..ec9033f415 100644 --- a/packages/editor-ui/src/components/InviteUsersModal.vue +++ b/packages/editor-ui/src/components/InviteUsersModal.vue @@ -109,7 +109,7 @@ export default defineComponent({ formBus: createEventBus(), modalBus: createEventBus(), emails: '', - role: 'member', + role: ROLE.Member, showInviteUrls: null as IInviteResponse[] | null, loading: false, INVITE_USER_MODAL_KEY, @@ -135,7 +135,7 @@ export default defineComponent({ }, { name: 'role', - initialValue: 'member', + initialValue: ROLE.Member, properties: { label: this.$locale.baseText('auth.role'), required: true, diff --git a/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue b/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue index ca258918c4..541079cf5e 100644 --- a/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue +++ b/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue @@ -16,7 +16,7 @@ const activeUsersSorted = computed(() => { const currentWorkflowUsers = (collaborationStore.getUsersForCurrentWorkflow ?? []).map( (userInfo) => userInfo.user, ); - const owner = currentWorkflowUsers.find((user) => user.globalRoleId === 1); + const owner = currentWorkflowUsers.find((user) => user.role === 'global:owner'); return { defaultGroup: owner ? [owner, ...currentWorkflowUsers.filter((user) => user.id !== owner.id)] diff --git a/packages/editor-ui/src/components/__tests__/BannersStack.test.ts b/packages/editor-ui/src/components/__tests__/BannersStack.test.ts index 031b54e6e1..728d7a6757 100644 --- a/packages/editor-ui/src/components/__tests__/BannersStack.test.ts +++ b/packages/editor-ui/src/components/__tests__/BannersStack.test.ts @@ -26,20 +26,11 @@ const initialState = { users: { 'aaa-bbb': { id: 'aaa-bbb', - globalRole: { - id: '1', - name: 'owner', - scope: 'global', - }, + role: 'global:owner', }, 'bbb-bbb': { id: 'bbb-bbb', - globalRoleId: 2, - globalRole: { - id: '2', - name: 'member', - scope: 'global', - }, + role: 'global:member', }, }, }, diff --git a/packages/editor-ui/src/components/__tests__/CollaborationPane.test.ts b/packages/editor-ui/src/components/__tests__/CollaborationPane.test.ts index efbf7de5b1..0fddfb7a96 100644 --- a/packages/editor-ui/src/components/__tests__/CollaborationPane.test.ts +++ b/packages/editor-ui/src/components/__tests__/CollaborationPane.test.ts @@ -13,13 +13,8 @@ const OWNER_USER = { email: 'owner@user.com', firstName: 'Owner', lastName: 'User', - globalRoleId: 1, + role: 'global:owner', disabled: false, - globalRole: { - id: '1', - name: 'owner', - scope: 'global', - }, isPending: false, isOwner: true, fullName: 'Owner User', @@ -31,13 +26,8 @@ const MEMBER_USER = { email: 'member@user.com', firstName: 'Member', lastName: 'User', - globalRoleId: 2, + role: 'global:member', disabled: false, - globalRole: { - id: '2', - name: 'member', - scope: 'global', - }, isPending: false, isOwner: false, fullName: 'Member User', @@ -49,13 +39,8 @@ const MEMBER_USER_2 = { email: 'member2@user.com', firstName: 'Another Member', lastName: 'User', - globalRoleId: 2, + role: 'global:member', disabled: false, - globalRole: { - id: '2', - name: 'member', - scope: 'global', - }, isPending: false, isOwner: false, fullName: 'Another Member User', diff --git a/packages/editor-ui/src/components/banners/__tests__/V1Banner.spec.ts b/packages/editor-ui/src/components/banners/__tests__/V1Banner.spec.ts index 8c9cee11b6..346f8bf01b 100644 --- a/packages/editor-ui/src/components/banners/__tests__/V1Banner.spec.ts +++ b/packages/editor-ui/src/components/banners/__tests__/V1Banner.spec.ts @@ -22,11 +22,7 @@ describe('V1 Banner', () => { it('should render banner with dismiss call if user is owner', () => { vi.spyOn(usersStore, 'currentUser', 'get').mockReturnValue({ - globalRole: { - id: 0, - name: 'owner', - createdAt: '2021-08-09T14:00:00.000Z', - }, + role: 'global:owner', }); const { container } = render(V1Banner); diff --git a/packages/editor-ui/src/rbac/checks/__tests__/hasRole.test.ts b/packages/editor-ui/src/rbac/checks/__tests__/hasRole.test.ts index 5bc0538047..adff9c6c58 100644 --- a/packages/editor-ui/src/rbac/checks/__tests__/hasRole.test.ts +++ b/packages/editor-ui/src/rbac/checks/__tests__/hasRole.test.ts @@ -12,7 +12,7 @@ describe('Checks', () => { vi.mocked(useUsersStore).mockReturnValue({ currentUser: { isDefaultUser: false, - globalRole: { name: ROLE.Owner }, + role: 'global:owner', }, } as ReturnType); @@ -23,7 +23,7 @@ describe('Checks', () => { vi.mocked(useUsersStore).mockReturnValue({ currentUser: { isDefaultUser: false, - globalRole: { name: ROLE.Member }, + role: 'global:member', }, } as ReturnType); diff --git a/packages/editor-ui/src/rbac/checks/hasRole.ts b/packages/editor-ui/src/rbac/checks/hasRole.ts index f9938e7266..7920d42d05 100644 --- a/packages/editor-ui/src/rbac/checks/hasRole.ts +++ b/packages/editor-ui/src/rbac/checks/hasRole.ts @@ -8,7 +8,7 @@ export const hasRole: RBACPermissionCheck = (checkRoles) const currentUser = usersStore.currentUser; if (currentUser && checkRoles) { - const userRole = currentUser.isDefaultUser ? ROLE.Default : currentUser.globalRole?.name; + const userRole = currentUser.isDefaultUser ? ROLE.Default : currentUser.role; return checkRoles.includes(userRole as IRole); } diff --git a/packages/editor-ui/src/rbac/middleware/__tests__/role.test.ts b/packages/editor-ui/src/rbac/middleware/__tests__/role.test.ts index 18cd46a6b3..2f07a97a46 100644 --- a/packages/editor-ui/src/rbac/middleware/__tests__/role.test.ts +++ b/packages/editor-ui/src/rbac/middleware/__tests__/role.test.ts @@ -15,11 +15,7 @@ describe('Middleware', () => { vi.mocked(useUsersStore).mockReturnValue({ currentUser: { isDefaultUser: false, - globalRole: { - id: '123', - createdAt: new Date(), - name: ROLE.Owner, - }, + role: 'global:owner', } as IUser, } as ReturnType); @@ -58,11 +54,7 @@ describe('Middleware', () => { vi.mocked(useUsersStore).mockReturnValue({ currentUser: { isDefaultUser: false, - globalRole: { - id: '123', - createdAt: new Date(), - name: ROLE.Owner, - }, + role: 'global:owner', } as IUser, } as ReturnType); diff --git a/packages/editor-ui/src/stores/__tests__/ui.test.ts b/packages/editor-ui/src/stores/__tests__/ui.test.ts index 39ff24cf76..bbab742c5d 100644 --- a/packages/editor-ui/src/stores/__tests__/ui.test.ts +++ b/packages/editor-ui/src/stores/__tests__/ui.test.ts @@ -25,11 +25,7 @@ function setUser(role: IRole) { { id: '1', isPending: false, - globalRole: { - id: '1', - name: role, - createdAt: new Date(), - }, + role, }, ]); @@ -37,7 +33,7 @@ function setUser(role: IRole) { } function setupOwnerAndCloudDeployment() { - setUser('owner'); + setUser('global:owner'); settingsStore.setSettings( merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, { n8nMetadata: { @@ -79,19 +75,19 @@ describe('UI store', () => { [ 'default', 'production', - 'owner', + 'global:owner', 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source', ], [ 'default', 'development', - 'owner', + 'global:owner', 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source', ], [ 'cloud', 'production', - 'owner', + 'global:owner', `https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent( '/account/change-plan', )}&utm_campaign=utm-test-campaign&source=test_source`, @@ -99,7 +95,7 @@ describe('UI store', () => { [ 'cloud', 'production', - 'member', + 'global:member', 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source', ], ])( diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index c8304713c7..cfdec7bb65 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -32,7 +32,7 @@ import type { InvitableRoleName, } from '@/Interface'; import { getCredentialPermissions } from '@/permissions'; -import { getPersonalizedNodeTypes, ROLE } from '@/utils/userUtils'; +import { getPersonalizedNodeTypes } from '@/utils/userUtils'; import { defineStore } from 'pinia'; import { useRootStore } from './n8nRoot.store'; import { usePostHog } from './posthog.store'; @@ -46,7 +46,7 @@ import type { Scope } from '@n8n/permissions'; import { inviteUsers, acceptInvitation } from '@/api/invitation'; const isPendingUser = (user: IUserResponse | null) => !!user?.isPending; -const isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner; +const isInstanceOwner = (user: IUserResponse | null) => user?.role === 'global:owner'; const isDefaultUser = (user: IUserResponse | null) => isInstanceOwner(user) && isPendingUser(user); export const useUsersStore = defineStore(STORES.USERS, { @@ -79,7 +79,7 @@ export const useUsersStore = defineStore(STORES.USERS, { return (userId: string): IUser | null => state.users[userId]; }, globalRoleName(): IRole { - return this.currentUser?.globalRole?.name ?? 'default'; + return this.currentUser?.role ?? 'default'; }, personalizedNodeTypes(): string[] { const user = this.currentUser; diff --git a/packages/editor-ui/src/utils/userUtils.ts b/packages/editor-ui/src/utils/userUtils.ts index 237a8067d5..ea95dbefb7 100644 --- a/packages/editor-ui/src/utils/userUtils.ts +++ b/packages/editor-ui/src/utils/userUtils.ts @@ -68,7 +68,6 @@ import type { IPersonalizationSurveyVersions, IUser, ILogInStatus, - IRole, } from '@/Interface'; /* @@ -84,13 +83,12 @@ function isPersonalizationSurveyV2OrLater( return 'version' in data; } -export type Roles = { [R in IRole as Capitalize]: R }; -export const ROLE: Roles = { - Owner: 'owner', - Member: 'member', - Admin: 'admin', +export const ROLE = { + Owner: 'global:owner', + Member: 'global:member', + Admin: 'global:admin', Default: 'default', // default user with no email when setting up instance -}; +} as const; export const LOGIN_STATUS: { LoggedIn: ILogInStatus; LoggedOut: ILogInStatus } = { LoggedIn: 'LoggedIn', // Can be owner or member or default user diff --git a/packages/editor-ui/src/views/SettingsUsersView.vue b/packages/editor-ui/src/views/SettingsUsersView.vue index 53095882e4..34aeaccd52 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/editor-ui/src/views/SettingsUsersView.vue @@ -65,7 +65,7 @@