diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts index 08cff0b6bf..1a98545d3c 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts @@ -8,11 +8,11 @@ import nock from 'nock'; import { Time } from '@/constants'; import { OAuth1CredentialController } from '@/controllers/oauth/oauth1-credential.controller'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import { CredentialsHelper } from '@/credentials-helper'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; import type { User } from '@/databases/entities/user'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; @@ -38,7 +38,7 @@ describe('OAuth1CredentialController', () => { Container.set(Cipher, cipher); const credentialsHelper = mockInstance(CredentialsHelper); const credentialsRepository = mockInstance(CredentialsRepository); - const sharedCredentialsRepository = mockInstance(SharedCredentialsRepository); + const credentialsFinderService = mockInstance(CredentialsFinderService); const csrfSecret = 'csrf-secret'; const user = mock({ @@ -73,7 +73,7 @@ describe('OAuth1CredentialController', () => { }); it('should throw a NotFoundError when no matching credential is found for the user', async () => { - sharedCredentialsRepository.findCredentialForUser.mockResolvedValueOnce(null); + credentialsFinderService.findCredentialForUser.mockResolvedValueOnce(null); const req = mock({ user, query: { id: '1' } }); await expect(controller.getAuthUri(req)).rejects.toThrowError( @@ -84,7 +84,7 @@ describe('OAuth1CredentialController', () => { it('should return a valid auth URI', async () => { jest.spyOn(Csrf.prototype, 'secretSync').mockReturnValueOnce(csrfSecret); jest.spyOn(Csrf.prototype, 'create').mockReturnValueOnce('token'); - sharedCredentialsRepository.findCredentialForUser.mockResolvedValueOnce(credential); + credentialsFinderService.findCredentialForUser.mockResolvedValueOnce(credential); credentialsHelper.getDecrypted.mockResolvedValueOnce({}); credentialsHelper.applyDefaultsAndOverwrites.mockResolvedValueOnce({ requestTokenUrl: 'https://example.domain/oauth/request_token', diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts index 99504fbeac..063347a60e 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts @@ -8,11 +8,11 @@ import nock from 'nock'; import { CREDENTIAL_BLANKING_VALUE, Time } from '@/constants'; import { OAuth2CredentialController } from '@/controllers/oauth/oauth2-credential.controller'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import { CredentialsHelper } from '@/credentials-helper'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; import type { User } from '@/databases/entities/user'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; @@ -39,7 +39,7 @@ describe('OAuth2CredentialController', () => { const externalHooks = mockInstance(ExternalHooks); const credentialsHelper = mockInstance(CredentialsHelper); const credentialsRepository = mockInstance(CredentialsRepository); - const sharedCredentialsRepository = mockInstance(SharedCredentialsRepository); + const credentialsFinderService = mockInstance(CredentialsFinderService); const csrfSecret = 'csrf-secret'; const user = mock({ @@ -81,7 +81,7 @@ describe('OAuth2CredentialController', () => { }); it('should throw a NotFoundError when no matching credential is found for the user', async () => { - sharedCredentialsRepository.findCredentialForUser.mockResolvedValueOnce(null); + credentialsFinderService.findCredentialForUser.mockResolvedValueOnce(null); const req = mock({ user, query: { id: '1' } }); await expect(controller.getAuthUri(req)).rejects.toThrowError( @@ -92,7 +92,7 @@ describe('OAuth2CredentialController', () => { it('should return a valid auth URI', async () => { jest.spyOn(Csrf.prototype, 'secretSync').mockReturnValueOnce(csrfSecret); jest.spyOn(Csrf.prototype, 'create').mockReturnValueOnce('token'); - sharedCredentialsRepository.findCredentialForUser.mockResolvedValueOnce(credential); + credentialsFinderService.findCredentialForUser.mockResolvedValueOnce(credential); credentialsHelper.getDecrypted.mockResolvedValueOnce({}); const req = mock({ user, query: { id: '1' } }); diff --git a/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts b/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts index ca82370a6a..5cc188715e 100644 --- a/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts +++ b/packages/cli/src/controllers/oauth/abstract-oauth.controller.ts @@ -7,10 +7,10 @@ import type { ICredentialDataDecryptedObject, IWorkflowExecuteAdditionalData } f import { jsonParse, UnexpectedError } from 'n8n-workflow'; import { RESPONSE_ERROR_MESSAGES, Time } from '@/constants'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import { CredentialsHelper } from '@/credentials-helper'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; @@ -46,7 +46,7 @@ export abstract class AbstractOAuthController { protected readonly externalHooks: ExternalHooks, private readonly credentialsHelper: CredentialsHelper, private readonly credentialsRepository: CredentialsRepository, - private readonly sharedCredentialsRepository: SharedCredentialsRepository, + private readonly credentialsFinderService: CredentialsFinderService, private readonly urlService: UrlService, private readonly globalConfig: GlobalConfig, ) {} @@ -65,7 +65,7 @@ export abstract class AbstractOAuthController { throw new BadRequestError('Required credential ID is missing'); } - const credential = await this.sharedCredentialsRepository.findCredentialForUser( + const credential = await this.credentialsFinderService.findCredentialForUser( credentialId, req.user, ['credential:read'], diff --git a/packages/cli/src/controllers/workflow-statistics.controller.ts b/packages/cli/src/controllers/workflow-statistics.controller.ts index c455c7249c..b64fa6ba79 100644 --- a/packages/cli/src/controllers/workflow-statistics.controller.ts +++ b/packages/cli/src/controllers/workflow-statistics.controller.ts @@ -4,10 +4,10 @@ import { Logger } from 'n8n-core'; import type { WorkflowStatistics } from '@/databases/entities/workflow-statistics'; import { StatisticsNames } from '@/databases/entities/workflow-statistics'; -import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowStatisticsRepository } from '@/databases/repositories/workflow-statistics.repository'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import type { IWorkflowStatisticsDataLoaded } from '@/interfaces'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { StatisticsRequest } from './workflow-statistics.types'; @@ -21,7 +21,7 @@ interface WorkflowStatisticsData { @RestController('/workflow-stats') export class WorkflowStatisticsController { constructor( - private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly workflowFinderService: WorkflowFinderService, private readonly workflowStatisticsRepository: WorkflowStatisticsRepository, private readonly logger: Logger, ) {} @@ -35,7 +35,7 @@ export class WorkflowStatisticsController { const { user } = req; const workflowId = req.params.id; - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); diff --git a/packages/cli/src/credentials/__tests__/credentials.controller.test.ts b/packages/cli/src/credentials/__tests__/credentials.controller.test.ts index f0504ea55e..9481c16cf5 100644 --- a/packages/cli/src/credentials/__tests__/credentials.controller.test.ts +++ b/packages/cli/src/credentials/__tests__/credentials.controller.test.ts @@ -25,6 +25,7 @@ describe('CredentialsController', () => { sharedCredentialsRepository, mock(), eventService, + mock(), ); let req: AuthenticatedRequest; diff --git a/packages/cli/src/credentials/__tests__/credentials.service.test.ts b/packages/cli/src/credentials/__tests__/credentials.service.test.ts index 5cc7dcaea1..97a81c2721 100644 --- a/packages/cli/src/credentials/__tests__/credentials.service.test.ts +++ b/packages/cli/src/credentials/__tests__/credentials.service.test.ts @@ -43,6 +43,7 @@ describe('CredentialsService', () => { mock(), mock(), mock(), + mock(), ); beforeEach(() => jest.resetAllMocks()); diff --git a/packages/cli/src/credentials/credentials-finder.service.ts b/packages/cli/src/credentials/credentials-finder.service.ts new file mode 100644 index 0000000000..d8afb3f8be --- /dev/null +++ b/packages/cli/src/credentials/credentials-finder.service.ts @@ -0,0 +1,140 @@ +import type { ProjectRole } from '@n8n/api-types'; +import { Service } from '@n8n/di'; +import type { Scope } from '@n8n/permissions'; +// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import +import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm'; +// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import +import { In } from '@n8n/typeorm'; + +import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; +import type { + SharedCredentials, + CredentialSharingRole, +} from '@/databases/entities/shared-credentials'; +import type { User } from '@/databases/entities/user'; +import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; +import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; +import { RoleService } from '@/services/role.service'; + +@Service() +export class CredentialsFinderService { + constructor( + private readonly sharedCredentialsRepository: SharedCredentialsRepository, + private readonly roleService: RoleService, + private readonly credentialsRepository: CredentialsRepository, + ) {} + + /** + * Find all credentials that the user has access to taking the scopes into + * account. + * + * This also returns `credentials.shared` which is useful for constructing + * all scopes the user has for the credential using `RoleService.addScopes`. + **/ + async findCredentialsForUser(user: User, scopes: Scope[]) { + let where: FindOptionsWhere = {}; + + if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { + const projectRoles = this.roleService.rolesWithScope('project', scopes); + const credentialRoles = this.roleService.rolesWithScope('credential', scopes); + where = { + ...where, + shared: { + role: In(credentialRoles), + project: { + projectRelations: { + role: In(projectRoles), + userId: user.id, + }, + }, + }, + }; + } + + return await this.credentialsRepository.find({ where, relations: { shared: true } }); + } + + /** Get a credential if it has been shared with a user */ + async findCredentialForUser(credentialsId: string, user: User, scopes: Scope[]) { + let where: FindOptionsWhere = { credentialsId }; + + if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { + const projectRoles = this.roleService.rolesWithScope('project', scopes); + const credentialRoles = this.roleService.rolesWithScope('credential', scopes); + where = { + ...where, + role: In(credentialRoles), + project: { + projectRelations: { + role: In(projectRoles), + userId: user.id, + }, + }, + }; + } + + const sharedCredential = await this.sharedCredentialsRepository.findOne({ + where, + // TODO: write a small relations merger and use that one here + relations: { + credentials: { + shared: { project: { projectRelations: { user: true } } }, + }, + }, + }); + if (!sharedCredential) return null; + return sharedCredential.credentials; + } + + /** Get all credentials shared to a user */ + async findAllCredentialsForUser(user: User, scopes: Scope[], trx?: EntityManager) { + let where: FindOptionsWhere = {}; + + if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { + const projectRoles = this.roleService.rolesWithScope('project', scopes); + const credentialRoles = this.roleService.rolesWithScope('credential', scopes); + where = { + role: In(credentialRoles), + project: { + projectRelations: { + role: In(projectRoles), + userId: user.id, + }, + }, + }; + } + + const sharedCredential = await this.sharedCredentialsRepository.findCredentialsWithOptions( + where, + trx, + ); + + return sharedCredential.map((sc) => ({ ...sc.credentials, projectId: sc.projectId })); + } + + async getCredentialIdsByUserAndRole( + userIds: string[], + options: + | { scopes: Scope[] } + | { projectRoles: ProjectRole[]; credentialRoles: CredentialSharingRole[] }, + trx?: EntityManager, + ) { + const projectRoles = + 'scopes' in options + ? this.roleService.rolesWithScope('project', options.scopes) + : options.projectRoles; + const credentialRoles = + 'scopes' in options + ? this.roleService.rolesWithScope('credential', options.scopes) + : options.credentialRoles; + + const sharings = await this.sharedCredentialsRepository.findCredentialsByRoles( + userIds, + projectRoles, + credentialRoles, + trx, + ); + + return sharings.map((s) => s.credentialsId); + } +} diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index d25598223b..2a6c84360e 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -40,6 +40,7 @@ import { NamingService } from '@/services/naming.service'; import { UserManagementMailer } from '@/user-management/email'; import * as utils from '@/utils'; +import { CredentialsFinderService } from './credentials-finder.service'; import { CredentialsService } from './credentials.service'; import { EnterpriseCredentialsService } from './credentials.service.ee'; @@ -56,6 +57,7 @@ export class CredentialsController { private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly projectRelationRepository: ProjectRelationRepository, private readonly eventService: EventService, + private readonly credentialsFinderService: CredentialsFinderService, ) {} @Get('/', { middlewares: listQueryMiddleware }) @@ -131,7 +133,7 @@ export class CredentialsController { async testCredentials(req: CredentialRequest.Test) { const { credentials } = req.body; - const storedCredential = await this.sharedCredentialsRepository.findCredentialForUser( + const storedCredential = await this.credentialsFinderService.findCredentialForUser( credentials.id, req.user, ['credential:read'], @@ -201,7 +203,7 @@ export class CredentialsController { params: { credentialId }, } = req; - const credential = await this.sharedCredentialsRepository.findCredentialForUser( + const credential = await this.credentialsFinderService.findCredentialForUser( credentialId, user, ['credential:update'], @@ -262,7 +264,7 @@ export class CredentialsController { async deleteCredentials(req: CredentialRequest.Delete) { const { credentialId } = req.params; - const credential = await this.sharedCredentialsRepository.findCredentialForUser( + const credential = await this.credentialsFinderService.findCredentialForUser( credentialId, req.user, ['credential:delete'], @@ -303,7 +305,7 @@ export class CredentialsController { throw new BadRequestError('Bad request'); } - const credential = await this.sharedCredentialsRepository.findCredentialForUser( + const credential = await this.credentialsFinderService.findCredentialForUser( credentialId, req.user, ['credential:share'], diff --git a/packages/cli/src/credentials/credentials.service.ee.ts b/packages/cli/src/credentials/credentials.service.ee.ts index 1fc6eabc15..896c67474d 100644 --- a/packages/cli/src/credentials/credentials.service.ee.ts +++ b/packages/cli/src/credentials/credentials.service.ee.ts @@ -14,6 +14,7 @@ import { OwnershipService } from '@/services/ownership.service'; import { ProjectService } from '@/services/project.service.ee'; import { RoleService } from '@/services/role.service'; +import { CredentialsFinderService } from './credentials-finder.service'; import { CredentialsService } from './credentials.service'; @Service() @@ -24,6 +25,7 @@ export class EnterpriseCredentialsService { private readonly credentialsService: CredentialsService, private readonly projectService: ProjectService, private readonly roleService: RoleService, + private readonly credentialsFinderService: CredentialsFinderService, ) {} async shareWithProjects( @@ -83,7 +85,7 @@ export class EnterpriseCredentialsService { credential = includeDecryptedData ? // Try to get the credential with `credential:update` scope, which // are required for decrypting the data. - await this.sharedCredentialsRepository.findCredentialForUser( + await this.credentialsFinderService.findCredentialForUser( credentialId, user, // TODO: replace credential:update with credential:decrypt once it lands @@ -99,11 +101,9 @@ export class EnterpriseCredentialsService { } else { // Otherwise try to find them with only the `credential:read` scope. In // that case we return them without the decrypted data. - credential = await this.sharedCredentialsRepository.findCredentialForUser( - credentialId, - user, - ['credential:read'], - ); + credential = await this.credentialsFinderService.findCredentialForUser(credentialId, user, [ + 'credential:read', + ]); } if (!credential) { @@ -130,7 +130,7 @@ export class EnterpriseCredentialsService { async transferOne(user: User, credentialId: string, destinationProjectId: string) { // 1. get credential - const credential = await this.sharedCredentialsRepository.findCredentialForUser( + const credential = await this.credentialsFinderService.findCredentialForUser( credentialId, user, ['credential:move'], diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index bed039279a..89fba37b6a 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -41,6 +41,8 @@ import { ProjectService } from '@/services/project.service.ee'; import type { ScopesField } from '@/services/role.service'; import { RoleService } from '@/services/role.service'; +import { CredentialsFinderService } from './credentials-finder.service'; + export type CredentialsGetSharedOptions = | { allowGlobalScope: true; globalScope: Scope } | { allowGlobalScope: false }; @@ -64,6 +66,7 @@ export class CredentialsService { private readonly projectService: ProjectService, private readonly roleService: RoleService, private readonly userRepository: UserRepository, + private readonly credentialsFinderService: CredentialsFinderService, ) {} async getMany( @@ -145,7 +148,7 @@ export class CredentialsService { } } - const ids = await this.sharedCredentialsRepository.getCredentialIdsByUserAndRole([user.id], { + const ids = await this.credentialsFinderService.getCredentialIdsByUserAndRole([user.id], { scopes: ['credential:read'], }); @@ -203,7 +206,7 @@ export class CredentialsService { const projectRelations = await this.projectService.getProjectRelationsForUser(user); // get all credentials the user has access to - const allCredentials = await this.credentialsRepository.findCredentialsForUser(user, [ + const allCredentials = await this.credentialsFinderService.findCredentialsForUser(user, [ 'credential:read', ]); @@ -434,7 +437,7 @@ export class CredentialsService { async delete(user: User, credentialId: string) { await this.externalHooks.run('credentials.delete', [credentialId]); - const credential = await this.sharedCredentialsRepository.findCredentialForUser( + const credential = await this.credentialsFinderService.findCredentialForUser( credentialId, user, ['credential:delete'], diff --git a/packages/cli/src/databases/repositories/credentials.repository.ts b/packages/cli/src/databases/repositories/credentials.repository.ts index 42fac4f8dc..ccc752ad8f 100644 --- a/packages/cli/src/databases/repositories/credentials.repository.ts +++ b/packages/cli/src/databases/repositories/credentials.repository.ts @@ -1,20 +1,14 @@ import { Service } from '@n8n/di'; -import type { Scope } from '@n8n/permissions'; import { DataSource, In, Repository, Like } from '@n8n/typeorm'; -import type { FindManyOptions, FindOptionsWhere } from '@n8n/typeorm'; +import type { FindManyOptions } from '@n8n/typeorm'; import type { ListQuery } from '@/requests'; -import { RoleService } from '@/services/role.service'; import { CredentialsEntity } from '../entities/credentials-entity'; -import type { User } from '../entities/user'; @Service() export class CredentialsRepository extends Repository { - constructor( - dataSource: DataSource, - readonly roleService: RoleService, - ) { + constructor(dataSource: DataSource) { super(CredentialsEntity, dataSource.manager); } @@ -131,34 +125,4 @@ export class CredentialsRepository extends Repository { async findAllCredentialsForProject(projectId: string): Promise { return await this.findBy({ shared: { projectId } }); } - - /** - * Find all credentials that the user has access to taking the scopes into - * account. - * - * This also returns `credentials.shared` which is useful for constructing - * all scopes the user has for the credential using `RoleService.addScopes`. - **/ - async findCredentialsForUser(user: User, scopes: Scope[]) { - let where: FindOptionsWhere = {}; - - if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { - const projectRoles = this.roleService.rolesWithScope('project', scopes); - const credentialRoles = this.roleService.rolesWithScope('credential', scopes); - where = { - ...where, - shared: { - role: In(credentialRoles), - project: { - projectRelations: { - role: In(projectRoles), - userId: user.id, - }, - }, - }, - }; - } - - return await this.find({ where, relations: { shared: true } }); - } } diff --git a/packages/cli/src/databases/repositories/shared-credentials.repository.ts b/packages/cli/src/databases/repositories/shared-credentials.repository.ts index f52eeac6cd..a190b33b09 100644 --- a/packages/cli/src/databases/repositories/shared-credentials.repository.ts +++ b/packages/cli/src/databases/repositories/shared-credentials.repository.ts @@ -1,94 +1,17 @@ import type { ProjectRole } from '@n8n/api-types'; import { Service } from '@n8n/di'; -import type { Scope } from '@n8n/permissions'; -import type { EntityManager, FindOptionsRelations, FindOptionsWhere } from '@n8n/typeorm'; +import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm'; import { DataSource, In, Not, Repository } from '@n8n/typeorm'; -import { RoleService } from '@/services/role.service'; - import type { Project } from '../entities/project'; import { type CredentialSharingRole, SharedCredentials } from '../entities/shared-credentials'; -import type { User } from '../entities/user'; @Service() export class SharedCredentialsRepository extends Repository { - constructor( - dataSource: DataSource, - private readonly roleService: RoleService, - ) { + constructor(dataSource: DataSource) { super(SharedCredentials, dataSource.manager); } - /** Get a credential if it has been shared with a user */ - async findCredentialForUser( - credentialsId: string, - user: User, - scopes: Scope[], - _relations?: FindOptionsRelations, - ) { - let where: FindOptionsWhere = { credentialsId }; - - if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { - const projectRoles = this.roleService.rolesWithScope('project', scopes); - const credentialRoles = this.roleService.rolesWithScope('credential', scopes); - where = { - ...where, - role: In(credentialRoles), - project: { - projectRelations: { - role: In(projectRoles), - userId: user.id, - }, - }, - }; - } - - const sharedCredential = await this.findOne({ - where, - // TODO: write a small relations merger and use that one here - relations: { - credentials: { - shared: { project: { projectRelations: { user: true } } }, - }, - }, - }); - if (!sharedCredential) return null; - return sharedCredential.credentials; - } - - /** Get all credentials shared to a user */ - async findAllCredentialsForUser(user: User, scopes: Scope[], trx?: EntityManager) { - trx = trx ?? this.manager; - - let where: FindOptionsWhere = {}; - - if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { - const projectRoles = this.roleService.rolesWithScope('project', scopes); - const credentialRoles = this.roleService.rolesWithScope('credential', scopes); - where = { - role: In(credentialRoles), - project: { - projectRelations: { - role: In(projectRoles), - userId: user.id, - }, - }, - }; - } - - const sharedCredential = await trx.find(SharedCredentials, { - where, - // TODO: write a small relations merger and use that one here - relations: { - credentials: { - shared: { project: { projectRelations: { user: true } } }, - }, - }, - }); - - return sharedCredential.map((sc) => ({ ...sc.credentials, projectId: sc.projectId })); - } - async findByCredentialIds(credentialIds: string[], role: CredentialSharingRole) { return await this.find({ relations: { credentials: true, project: { projectRelations: { user: true } } }, @@ -125,39 +48,6 @@ export class SharedCredentialsRepository extends Repository { ); } - async getCredentialIdsByUserAndRole( - userIds: string[], - options: - | { scopes: Scope[] } - | { projectRoles: ProjectRole[]; credentialRoles: CredentialSharingRole[] }, - trx?: EntityManager, - ) { - trx = trx ?? this.manager; - - const projectRoles = - 'scopes' in options - ? this.roleService.rolesWithScope('project', options.scopes) - : options.projectRoles; - const credentialRoles = - 'scopes' in options - ? this.roleService.rolesWithScope('credential', options.scopes) - : options.credentialRoles; - - const sharings = await trx.find(SharedCredentials, { - where: { - role: In(credentialRoles), - project: { - projectRelations: { - userId: In(userIds), - role: In(projectRoles), - }, - }, - }, - }); - - return sharings.map((s) => s.credentialsId); - } - async deleteByIds(sharedCredentialsIds: string[], projectId: string, trx?: EntityManager) { trx = trx ?? this.manager; @@ -199,4 +89,41 @@ export class SharedCredentialsRepository extends Repository { relations: ['project'], }); } + + async findCredentialsWithOptions( + where: FindOptionsWhere = {}, + trx?: EntityManager, + ) { + trx = trx ?? this.manager; + + return await trx.find(SharedCredentials, { + where, + relations: { + credentials: { + shared: { project: { projectRelations: { user: true } } }, + }, + }, + }); + } + + async findCredentialsByRoles( + userIds: string[], + projectRoles: ProjectRole[], + credentialRoles: CredentialSharingRole[], + trx?: EntityManager, + ) { + trx = trx ?? this.manager; + + return await trx.find(SharedCredentials, { + where: { + role: In(credentialRoles), + project: { + projectRelations: { + userId: In(userIds), + role: In(projectRoles), + }, + }, + }, + }); + } } diff --git a/packages/cli/src/databases/repositories/shared-workflow.repository.ts b/packages/cli/src/databases/repositories/shared-workflow.repository.ts index 6573355841..59f4f512c6 100644 --- a/packages/cli/src/databases/repositories/shared-workflow.repository.ts +++ b/packages/cli/src/databases/repositories/shared-workflow.repository.ts @@ -1,20 +1,13 @@ import { Service } from '@n8n/di'; -import type { Scope } from '@n8n/permissions'; import { DataSource, Repository, In, Not } from '@n8n/typeorm'; import type { EntityManager, FindManyOptions, FindOptionsWhere } from '@n8n/typeorm'; -import { RoleService } from '@/services/role.service'; - import type { Project } from '../entities/project'; import { SharedWorkflow, type WorkflowSharingRole } from '../entities/shared-workflow'; -import { type User } from '../entities/user'; @Service() export class SharedWorkflowRepository extends Repository { - constructor( - dataSource: DataSource, - private roleService: RoleService, - ) { + constructor(dataSource: DataSource) { super(SharedWorkflow, dataSource.manager); } @@ -108,79 +101,6 @@ export class SharedWorkflowRepository extends Repository { }); } - async findWorkflowForUser( - workflowId: string, - user: User, - scopes: Scope[], - { includeTags = false, includeParentFolder = false, em = this.manager } = {}, - ) { - let where: FindOptionsWhere = { workflowId }; - - if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { - const projectRoles = this.roleService.rolesWithScope('project', scopes); - const workflowRoles = this.roleService.rolesWithScope('workflow', scopes); - - where = { - ...where, - role: In(workflowRoles), - project: { - projectRelations: { - role: In(projectRoles), - userId: user.id, - }, - }, - }; - } - - const sharedWorkflow = await em.findOne(SharedWorkflow, { - where, - relations: { - workflow: { - shared: { project: { projectRelations: { user: true } } }, - tags: includeTags, - parentFolder: includeParentFolder, - }, - }, - }); - - if (!sharedWorkflow) { - return null; - } - - return sharedWorkflow.workflow; - } - - async findAllWorkflowsForUser(user: User, scopes: Scope[]) { - let where: FindOptionsWhere = {}; - - if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { - const projectRoles = this.roleService.rolesWithScope('project', scopes); - const workflowRoles = this.roleService.rolesWithScope('workflow', scopes); - - where = { - ...where, - role: In(workflowRoles), - project: { - projectRelations: { - role: In(projectRoles), - userId: user.id, - }, - }, - }; - } - - const sharedWorkflows = await this.find({ - where, - relations: { - workflow: { - shared: { project: { projectRelations: { user: true } } }, - }, - }, - }); - - return sharedWorkflows.map((sw) => ({ ...sw.workflow, projectId: sw.projectId })); - } - /** * Find the IDs of all the projects where a workflow is accessible. */ @@ -221,4 +141,35 @@ export class SharedWorkflowRepository extends Repository { relations: ['project'], }); } + + async findWorkflowWithOptions( + workflowId: string, + options: { + where?: FindOptionsWhere; + includeTags?: boolean; + includeParentFolder?: boolean; + em?: EntityManager; + } = {}, + ) { + const { + where = {}, + includeTags = false, + includeParentFolder = false, + em = this.manager, + } = options; + + return await em.findOne(SharedWorkflow, { + where: { + workflowId, + ...where, + }, + relations: { + workflow: { + shared: { project: { projectRelations: { user: true } } }, + tags: includeTags, + parentFolder: includeParentFolder, + }, + }, + }); + } } diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index 0a9cc5045e..f18eed4a63 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -11,12 +11,12 @@ import { z } from 'zod'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { WorkflowEntity } from '@/databases/entities/workflow-entity'; import { ProjectRepository } from '@/databases/repositories/project.repository'; -import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { TagRepository } from '@/databases/repositories/tag.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { addNodeIds, replaceInvalidCredentials } from '@/workflow-helpers'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { WorkflowHistoryService } from '@/workflows/workflow-history.ee/workflow-history.service.ee'; import { WorkflowService } from '@/workflows/workflow.service'; import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; @@ -115,7 +115,7 @@ export = { const { id } = req.params; const { excludePinnedData = false } = req.query; - const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( + const workflow = await Container.get(WorkflowFinderService).findWorkflowForUser( id, req.user, ['workflow:read'], @@ -169,7 +169,7 @@ export = { } if (projectId) { - const workflows = await Container.get(SharedWorkflowRepository).findAllWorkflowsForUser( + const workflows = await Container.get(WorkflowFinderService).findAllWorkflowsForUser( req.user, ['workflow:read'], ); @@ -189,7 +189,7 @@ export = { ); } - let workflows = await Container.get(SharedWorkflowRepository).findAllWorkflowsForUser( + let workflows = await Container.get(WorkflowFinderService).findAllWorkflowsForUser( req.user, ['workflow:read'], ); @@ -252,7 +252,7 @@ export = { updateData.id = id; updateData.versionId = uuid(); - const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( + const workflow = await Container.get(WorkflowFinderService).findWorkflowForUser( id, req.user, ['workflow:update'], @@ -319,7 +319,7 @@ export = { async (req: WorkflowRequest.Activate, res: express.Response): Promise => { const { id } = req.params; - const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( + const workflow = await Container.get(WorkflowFinderService).findWorkflowForUser( id, req.user, ['workflow:update'], @@ -358,7 +358,7 @@ export = { async (req: WorkflowRequest.Activate, res: express.Response): Promise => { const { id } = req.params; - const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( + const workflow = await Container.get(WorkflowFinderService).findWorkflowForUser( id, req.user, ['workflow:update'], @@ -396,7 +396,7 @@ export = { return res.status(400).json({ message: 'Workflow Tags Disabled' }); } - const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( + const workflow = await Container.get(WorkflowFinderService).findWorkflowForUser( id, req.user, ['workflow:read'], @@ -424,7 +424,7 @@ export = { return res.status(400).json({ message: 'Workflow Tags Disabled' }); } - const sharedWorkflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( + const sharedWorkflow = await Container.get(WorkflowFinderService).findWorkflowForUser( id, req.user, ['workflow:update'], diff --git a/packages/cli/src/services/__tests__/active-workflows.service.test.ts b/packages/cli/src/services/__tests__/active-workflows.service.test.ts index 788ef3fb68..590281f236 100644 --- a/packages/cli/src/services/__tests__/active-workflows.service.test.ts +++ b/packages/cli/src/services/__tests__/active-workflows.service.test.ts @@ -7,17 +7,20 @@ import type { SharedWorkflowRepository } from '@/databases/repositories/shared-w import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ActiveWorkflowsService } from '@/services/active-workflows.service'; +import type { WorkflowFinderService } from '@/workflows/workflow-finder.service'; describe('ActiveWorkflowsService', () => { const user = mock(); const workflowRepository = mock(); const sharedWorkflowRepository = mock(); + const workflowFinderService = mock(); const activationErrorsService = mock(); const service = new ActiveWorkflowsService( mock(), workflowRepository, sharedWorkflowRepository, activationErrorsService, + workflowFinderService, ); const activeIds = ['1', '2', '3', '4']; @@ -63,22 +66,22 @@ describe('ActiveWorkflowsService', () => { const workflowId = 'workflowId'; it('should throw a BadRequestError a user does not have access to the workflow id', async () => { - sharedWorkflowRepository.findWorkflowForUser.mockResolvedValue(null); + workflowFinderService.findWorkflowForUser.mockResolvedValue(null); await expect(service.getActivationError(workflowId, user)).rejects.toThrow(BadRequestError); - expect(sharedWorkflowRepository.findWorkflowForUser).toHaveBeenCalledWith(workflowId, user, [ + expect(workflowFinderService.findWorkflowForUser).toHaveBeenCalledWith(workflowId, user, [ 'workflow:read', ]); expect(activationErrorsService.get).not.toHaveBeenCalled(); }); it('should return the error when the user has access', async () => { - sharedWorkflowRepository.findWorkflowForUser.mockResolvedValue(new WorkflowEntity()); + workflowFinderService.findWorkflowForUser.mockResolvedValue(new WorkflowEntity()); activationErrorsService.get.mockResolvedValue('some-error'); const error = await service.getActivationError(workflowId, user); expect(error).toEqual('some-error'); - expect(sharedWorkflowRepository.findWorkflowForUser).toHaveBeenCalledWith(workflowId, user, [ + expect(workflowFinderService.findWorkflowForUser).toHaveBeenCalledWith(workflowId, user, [ 'workflow:read', ]); expect(activationErrorsService.get).toHaveBeenCalledWith(workflowId); diff --git a/packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts b/packages/cli/src/services/__tests__/credentials-finder.service.test.ts similarity index 82% rename from packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts rename to packages/cli/src/services/__tests__/credentials-finder.service.test.ts index d7e108389a..373f19eda2 100644 --- a/packages/cli/src/databases/repositories/__tests__/shared-credentials.repository.test.ts +++ b/packages/cli/src/services/__tests__/credentials-finder.service.test.ts @@ -3,16 +3,16 @@ import { hasScope } from '@n8n/permissions'; import { In } from '@n8n/typeorm'; import { mock } from 'jest-mock-extended'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; import { SharedCredentials } from '@/databases/entities/shared-credentials'; import type { User } from '@/databases/entities/user'; -import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES } from '@/permissions.ee/global-roles'; import { mockEntityManager } from '@test/mocking'; -describe('SharedCredentialsRepository', () => { +describe('CredentialsFinderService', () => { const entityManager = mockEntityManager(SharedCredentials); - const repository = Container.get(SharedCredentialsRepository); + const credentialsFinderService = Container.get(CredentialsFinderService); describe('findCredentialForUser', () => { const credentialsId = 'cred_123'; @@ -40,9 +40,11 @@ describe('SharedCredentialsRepository', () => { test('should allow instance owner access to all credentials', async () => { entityManager.findOne.mockResolvedValueOnce(sharedCredential); - const credential = await repository.findCredentialForUser(credentialsId, owner, [ - 'credential:read', - ]); + const credential = await credentialsFinderService.findCredentialForUser( + credentialsId, + owner, + ['credential:read'], + ); expect(entityManager.findOne).toHaveBeenCalledWith(SharedCredentials, { relations: { credentials: { shared: { project: { projectRelations: { user: true } } } } }, where: { credentialsId }, @@ -52,9 +54,11 @@ describe('SharedCredentialsRepository', () => { test('should allow members', async () => { entityManager.findOne.mockResolvedValueOnce(sharedCredential); - const credential = await repository.findCredentialForUser(credentialsId, member, [ - 'credential:read', - ]); + const credential = await credentialsFinderService.findCredentialForUser( + credentialsId, + member, + ['credential:read'], + ); expect(entityManager.findOne).toHaveBeenCalledWith(SharedCredentials, { relations: { credentials: { shared: { project: { projectRelations: { user: true } } } } }, where: { @@ -78,9 +82,11 @@ describe('SharedCredentialsRepository', () => { test('should return null when no shared credential is found', async () => { entityManager.findOne.mockResolvedValueOnce(null); - const credential = await repository.findCredentialForUser(credentialsId, member, [ - 'credential:read', - ]); + const credential = await credentialsFinderService.findCredentialForUser( + credentialsId, + member, + ['credential:read'], + ); expect(entityManager.findOne).toHaveBeenCalledWith(SharedCredentials, { relations: { credentials: { shared: { project: { projectRelations: { user: true } } } } }, where: { diff --git a/packages/cli/src/services/access.service.ts b/packages/cli/src/services/access.service.ts index 7a7030b93e..2756be9572 100644 --- a/packages/cli/src/services/access.service.ts +++ b/packages/cli/src/services/access.service.ts @@ -2,8 +2,8 @@ import { Service } from '@n8n/di'; import type { Workflow } from 'n8n-workflow'; import type { User } from '@/databases/entities/user'; -import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; /** * Responsible for checking whether a user has access to a resource. @@ -12,7 +12,7 @@ import { UserRepository } from '@/databases/repositories/user.repository'; export class AccessService { constructor( private readonly userRepository: UserRepository, - private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly workflowFinderService: WorkflowFinderService, ) {} /** Whether a user has read access to a workflow based on their project and scope. */ @@ -21,7 +21,7 @@ export class AccessService { if (!user) return false; - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); diff --git a/packages/cli/src/services/active-workflows.service.ts b/packages/cli/src/services/active-workflows.service.ts index 98b96af33a..ebe718f898 100644 --- a/packages/cli/src/services/active-workflows.service.ts +++ b/packages/cli/src/services/active-workflows.service.ts @@ -6,6 +6,7 @@ import type { User } from '@/databases/entities/user'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; @Service() export class ActiveWorkflowsService { @@ -14,6 +15,7 @@ export class ActiveWorkflowsService { private readonly workflowRepository: WorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly activationErrorsService: ActivationErrorsService, + private readonly workflowFinderService: WorkflowFinderService, ) {} async getAllActiveIdsInStorage() { @@ -37,7 +39,7 @@ export class ActiveWorkflowsService { } async getActivationError(workflowId: string, user: User) { - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); if (!workflow) { diff --git a/packages/cli/src/workflows/workflow-finder.service.ts b/packages/cli/src/workflows/workflow-finder.service.ts new file mode 100644 index 0000000000..0c220d0f47 --- /dev/null +++ b/packages/cli/src/workflows/workflow-finder.service.ts @@ -0,0 +1,91 @@ +import { Service } from '@n8n/di'; +import type { Scope } from '@n8n/permissions'; +// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import +import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm'; +// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import +import { In } from '@n8n/typeorm'; + +import type { SharedWorkflow } from '@/databases/entities/shared-workflow'; +import type { User } from '@/databases/entities/user'; +import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; +import { RoleService } from '@/services/role.service'; + +@Service() +export class WorkflowFinderService { + constructor( + private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly roleService: RoleService, + ) {} + + async findWorkflowForUser( + workflowId: string, + user: User, + scopes: Scope[], + options: { + includeTags?: boolean; + includeParentFolder?: boolean; + em?: EntityManager; + } = {}, + ) { + let where: FindOptionsWhere = {}; + + if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { + const projectRoles = this.roleService.rolesWithScope('project', scopes); + const workflowRoles = this.roleService.rolesWithScope('workflow', scopes); + + where = { + role: In(workflowRoles), + project: { + projectRelations: { + role: In(projectRoles), + userId: user.id, + }, + }, + }; + } + + const sharedWorkflow = await this.sharedWorkflowRepository.findWorkflowWithOptions(workflowId, { + where, + includeTags: options.includeTags, + includeParentFolder: options.includeParentFolder, + em: options.em, + }); + + if (!sharedWorkflow) { + return null; + } + + return sharedWorkflow.workflow; + } + + async findAllWorkflowsForUser(user: User, scopes: Scope[]) { + let where: FindOptionsWhere = {}; + + if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) { + const projectRoles = this.roleService.rolesWithScope('project', scopes); + const workflowRoles = this.roleService.rolesWithScope('workflow', scopes); + + where = { + ...where, + role: In(workflowRoles), + project: { + projectRelations: { + role: In(projectRoles), + userId: user.id, + }, + }, + }; + } + + const sharedWorkflows = await this.sharedWorkflowRepository.find({ + where, + relations: { + workflow: { + shared: { project: { projectRelations: { user: true } } }, + }, + }, + }); + + return sharedWorkflows.map((sw) => ({ ...sw.workflow, projectId: sw.projectId })); + } +} diff --git a/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts b/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts index b80b38eb9e..fd70031578 100644 --- a/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts +++ b/packages/cli/src/workflows/workflow-history.ee/__tests__/workflow-history.service.ee.test.ts @@ -1,19 +1,19 @@ import { mockClear } from 'jest-mock-extended'; import { User } from '@/databases/entities/user'; -import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { WorkflowHistoryService } from '@/workflows/workflow-history.ee/workflow-history.service.ee'; import { mockInstance, mockLogger } from '@test/mocking'; import { getWorkflow } from '@test-integration/workflow'; const workflowHistoryRepository = mockInstance(WorkflowHistoryRepository); const logger = mockLogger(); -const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository); +const workflowFinderService = mockInstance(WorkflowFinderService); const workflowHistoryService = new WorkflowHistoryService( logger, workflowHistoryRepository, - sharedWorkflowRepository, + workflowFinderService, ); const testUser = Object.assign(new User(), { id: '1234', diff --git a/packages/cli/src/workflows/workflow-history.ee/workflow-history.service.ee.ts b/packages/cli/src/workflows/workflow-history.ee/workflow-history.service.ee.ts index 11bcaa8eb1..c464417527 100644 --- a/packages/cli/src/workflows/workflow-history.ee/workflow-history.service.ee.ts +++ b/packages/cli/src/workflows/workflow-history.ee/workflow-history.service.ee.ts @@ -5,19 +5,19 @@ import { ensureError } from 'n8n-workflow'; import type { User } from '@/databases/entities/user'; import type { WorkflowHistory } from '@/databases/entities/workflow-history'; -import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository'; import { SharedWorkflowNotFoundError } from '@/errors/shared-workflow-not-found.error'; import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error'; import { isWorkflowHistoryEnabled } from './workflow-history-helper.ee'; +import { WorkflowFinderService } from '../workflow-finder.service'; @Service() export class WorkflowHistoryService { constructor( private readonly logger: Logger, private readonly workflowHistoryRepository: WorkflowHistoryRepository, - private readonly sharedWorkflowRepository: SharedWorkflowRepository, + private readonly workflowFinderService: WorkflowFinderService, ) {} async getList( @@ -26,7 +26,7 @@ export class WorkflowHistoryService { take: number, skip: number, ): Promise>> { - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); @@ -46,7 +46,7 @@ export class WorkflowHistoryService { } async getVersion(user: User, workflowId: string, versionId: string): Promise { - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); diff --git a/packages/cli/src/workflows/workflow.service.ee.ts b/packages/cli/src/workflows/workflow.service.ee.ts index d2d4511dd2..3cbacdf29c 100644 --- a/packages/cli/src/workflows/workflow.service.ee.ts +++ b/packages/cli/src/workflows/workflow.service.ee.ts @@ -7,6 +7,7 @@ import type { IWorkflowBase, WorkflowId } from 'n8n-workflow'; import { NodeOperationError, UserError, WorkflowActivationError } from 'n8n-workflow'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import { CredentialsService } from '@/credentials/credentials.service'; import { EnterpriseCredentialsService } from '@/credentials/credentials.service.ee'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; @@ -14,7 +15,6 @@ import { Project } from '@/databases/entities/project'; import { SharedWorkflow } from '@/databases/entities/shared-workflow'; import type { User } from '@/databases/entities/user'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; -import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -23,6 +23,7 @@ import { TransferWorkflowError } from '@/errors/response-errors/transfer-workflo import { OwnershipService } from '@/services/ownership.service'; import { ProjectService } from '@/services/project.service.ee'; +import { WorkflowFinderService } from './workflow-finder.service'; import type { WorkflowWithSharingsAndCredentials, WorkflowWithSharingsMetaDataAndCredentials, @@ -39,8 +40,9 @@ export class EnterpriseWorkflowService { private readonly ownershipService: OwnershipService, private readonly projectService: ProjectService, private readonly activeWorkflowManager: ActiveWorkflowManager, - private readonly sharedCredentialsRepository: SharedCredentialsRepository, + private readonly credentialsFinderService: CredentialsFinderService, private readonly enterpriseCredentialsService: EnterpriseCredentialsService, + private readonly workflowFinderService: WorkflowFinderService, ) {} async shareWithProjects( @@ -265,7 +267,7 @@ export class EnterpriseWorkflowService { shareCredentials: string[] = [], ) { // 1. get workflow - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:move', ]); NotFoundError.isDefinedAndNotNull( @@ -324,7 +326,7 @@ export class EnterpriseWorkflowService { // 8. share credentials into the destination project await this.workflowRepository.manager.transaction(async (trx) => { - const allCredentials = await this.sharedCredentialsRepository.findAllCredentialsForUser( + const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser( user, ['credential:share'], trx, diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index f2a497279b..bae2dd9f39 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -36,6 +36,7 @@ import { RoleService } from '@/services/role.service'; import { TagService } from '@/services/tag.service'; import * as WorkflowHelpers from '@/workflow-helpers'; +import { WorkflowFinderService } from './workflow-finder.service'; import { WorkflowHistoryService } from './workflow-history.ee/workflow-history.service.ee'; import { WorkflowSharingService } from './workflow-sharing.service'; @@ -60,6 +61,7 @@ export class WorkflowService { private readonly eventService: EventService, private readonly globalConfig: GlobalConfig, private readonly folderService: FolderService, + private readonly workflowFinderService: WorkflowFinderService, ) {} async getMany( @@ -185,7 +187,7 @@ export class WorkflowService { parentFolderId?: string, forceSave?: boolean, ): Promise { - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:update', ]); @@ -367,7 +369,7 @@ export class WorkflowService { async delete(user: User, workflowId: string): Promise { await this.externalHooks.run('workflow.delete', [workflowId]); - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:delete', ]); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index d1d8df10fc..4bbe8bf1bb 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -55,6 +55,7 @@ import * as utils from '@/utils'; import * as WorkflowHelpers from '@/workflow-helpers'; import { WorkflowExecutionService } from './workflow-execution.service'; +import { WorkflowFinderService } from './workflow-finder.service'; import { WorkflowHistoryService } from './workflow-history.ee/workflow-history.service.ee'; import { WorkflowRequest } from './workflow.request'; import { WorkflowService } from './workflow.service'; @@ -84,6 +85,7 @@ export class WorkflowsController { private readonly eventService: EventService, private readonly globalConfig: GlobalConfig, private readonly folderService: FolderService, + private readonly workflowFinderService: WorkflowFinderService, ) {} @Post('/') @@ -176,7 +178,7 @@ export class WorkflowsController { await transactionManager.save(newSharedWorkflow); - return await this.sharedWorkflowRepository.findWorkflowForUser( + return await this.workflowFinderService.findWorkflowForUser( workflow.id, req.user, ['workflow:read'], @@ -292,7 +294,7 @@ export class WorkflowsController { relations.tags = true; } - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser( + const workflow = await this.workflowFinderService.findWorkflowForUser( workflowId, req.user, ['workflow:read'], @@ -320,7 +322,7 @@ export class WorkflowsController { // sharing disabled - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser( + const workflow = await this.workflowFinderService.findWorkflowForUser( workflowId, req.user, ['workflow:read'], @@ -442,7 +444,7 @@ export class WorkflowsController { throw new BadRequestError('Bad request'); } - const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, req.user, [ + const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, req.user, [ 'workflow:share', ]); diff --git a/packages/cli/test/integration/credentials/credentials.service.test.ts b/packages/cli/test/integration/credentials/credentials.service.test.ts index 9638228183..b3ab1b7a40 100644 --- a/packages/cli/test/integration/credentials/credentials.service.test.ts +++ b/packages/cli/test/integration/credentials/credentials.service.test.ts @@ -1,9 +1,9 @@ import { Container } from '@n8n/di'; +import { CredentialsFinderService } from '@/credentials/credentials-finder.service'; import { CredentialsService } from '@/credentials/credentials.service'; import type { CredentialsEntity } from '@/databases/entities/credentials-entity'; import type { User } from '@/databases/entities/user'; -import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { createTeamProject, linkUserToProject } from '@test-integration/db/projects'; import { saveCredential, shareCredentialWithUsers } from '../shared/db/credentials'; @@ -32,9 +32,11 @@ beforeAll(async () => { describe('credentials service', () => { describe('replaceCredentialContentsForSharee', () => { it('should replace the contents of the credential for sharee', async () => { - const storedCredential = await Container.get( - SharedCredentialsRepository, - ).findCredentialForUser(credential.id, memberWhoDoesNotOwnCredential, ['credential:read']); + const storedCredential = await Container.get(CredentialsFinderService).findCredentialForUser( + credential.id, + memberWhoDoesNotOwnCredential, + ['credential:read'], + ); const decryptedData = Container.get(CredentialsService).decrypt(storedCredential!); @@ -64,10 +66,12 @@ describe('credentials service', () => { }); const storedProjectCredential = await Container.get( - SharedCredentialsRepository, + CredentialsFinderService, ).findCredentialForUser(projectCredential.id, viewerMember, ['credential:read']); - const decryptedData = Container.get(CredentialsService).decrypt(storedProjectCredential!); + if (!storedProjectCredential) throw new Error('Could not find credential'); + + const decryptedData = Container.get(CredentialsService).decrypt(storedProjectCredential); const mergedCredentials = { id: projectCredential.id, @@ -78,7 +82,7 @@ describe('credentials service', () => { await Container.get(CredentialsService).replaceCredentialContentsForSharee( viewerMember, - storedProjectCredential!, + storedProjectCredential, decryptedData, mergedCredentials, ); @@ -95,10 +99,12 @@ describe('credentials service', () => { }); const storedProjectCredential = await Container.get( - SharedCredentialsRepository, + CredentialsFinderService, ).findCredentialForUser(projectCredential.id, editorMember, ['credential:read']); - const decryptedData = Container.get(CredentialsService).decrypt(storedProjectCredential!); + if (!storedProjectCredential) throw new Error('Could not find credential'); + + const decryptedData = Container.get(CredentialsService).decrypt(storedProjectCredential); const originalData = { accessToken: '' }; const mergedCredentials = { @@ -110,7 +116,7 @@ describe('credentials service', () => { await Container.get(CredentialsService).replaceCredentialContentsForSharee( editorMember, - storedProjectCredential!, + storedProjectCredential, decryptedData, mergedCredentials, ); diff --git a/packages/cli/test/integration/workflows/workflow.service.ee.test.ts b/packages/cli/test/integration/workflows/workflow.service.ee.test.ts index 421f27e419..92f07feb92 100644 --- a/packages/cli/test/integration/workflows/workflow.service.ee.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.ee.test.ts @@ -35,6 +35,7 @@ describe('EnterpriseWorkflowService', () => { mock(), mock(), mock(), + mock(), ); }); diff --git a/packages/cli/test/integration/workflows/workflow.service.test.ts b/packages/cli/test/integration/workflows/workflow.service.test.ts index 6fb7422937..b3dbb972b6 100644 --- a/packages/cli/test/integration/workflows/workflow.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.test.ts @@ -7,6 +7,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { OrchestrationService } from '@/services/orchestration.service'; import { Telemetry } from '@/telemetry'; +import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { WorkflowService } from '@/workflows/workflow.service'; import { mockInstance } from '../../shared/mocking'; @@ -42,6 +43,7 @@ beforeAll(async () => { mock(), mock(), mock(), + Container.get(WorkflowFinderService), ); });