mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(core): Decouple RoleService from repositories (#14944)
This commit is contained in:
@@ -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<User>({
|
||||
@@ -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<OAuthRequest.OAuth1Credential.Auth>({ 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',
|
||||
|
||||
@@ -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<User>({
|
||||
@@ -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<OAuthRequest.OAuth2Credential.Auth>({ 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<OAuthRequest.OAuth2Credential.Auth>({ user, query: { id: '1' } });
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<T> {
|
||||
@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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('CredentialsController', () => {
|
||||
sharedCredentialsRepository,
|
||||
mock(),
|
||||
eventService,
|
||||
mock(),
|
||||
);
|
||||
|
||||
let req: AuthenticatedRequest;
|
||||
|
||||
@@ -43,6 +43,7 @@ describe('CredentialsService', () => {
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
);
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
140
packages/cli/src/credentials/credentials-finder.service.ts
Normal file
140
packages/cli/src/credentials/credentials-finder.service.ts
Normal file
@@ -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<CredentialsEntity> = {};
|
||||
|
||||
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<SharedCredentials> = { 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<SharedCredentials> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<CredentialsEntity> {
|
||||
constructor(
|
||||
dataSource: DataSource,
|
||||
readonly roleService: RoleService,
|
||||
) {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(CredentialsEntity, dataSource.manager);
|
||||
}
|
||||
|
||||
@@ -131,34 +125,4 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
|
||||
async findAllCredentialsForProject(projectId: string): Promise<CredentialsEntity[]> {
|
||||
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<CredentialsEntity> = {};
|
||||
|
||||
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 } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SharedCredentials> {
|
||||
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<SharedCredentials>,
|
||||
) {
|
||||
let where: FindOptionsWhere<SharedCredentials> = { 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<SharedCredentials> = {};
|
||||
|
||||
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<SharedCredentials> {
|
||||
);
|
||||
}
|
||||
|
||||
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<SharedCredentials> {
|
||||
relations: ['project'],
|
||||
});
|
||||
}
|
||||
|
||||
async findCredentialsWithOptions(
|
||||
where: FindOptionsWhere<SharedCredentials> = {},
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SharedWorkflow> {
|
||||
constructor(
|
||||
dataSource: DataSource,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(SharedWorkflow, dataSource.manager);
|
||||
}
|
||||
|
||||
@@ -108,79 +101,6 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
||||
});
|
||||
}
|
||||
|
||||
async findWorkflowForUser(
|
||||
workflowId: string,
|
||||
user: User,
|
||||
scopes: Scope[],
|
||||
{ includeTags = false, includeParentFolder = false, em = this.manager } = {},
|
||||
) {
|
||||
let where: FindOptionsWhere<SharedWorkflow> = { 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<SharedWorkflow> = {};
|
||||
|
||||
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<SharedWorkflow> {
|
||||
relations: ['project'],
|
||||
});
|
||||
}
|
||||
|
||||
async findWorkflowWithOptions(
|
||||
workflowId: string,
|
||||
options: {
|
||||
where?: FindOptionsWhere<SharedWorkflow>;
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<express.Response> => {
|
||||
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<express.Response> => {
|
||||
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'],
|
||||
|
||||
@@ -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<User>();
|
||||
const workflowRepository = mock<WorkflowRepository>();
|
||||
const sharedWorkflowRepository = mock<SharedWorkflowRepository>();
|
||||
const workflowFinderService = mock<WorkflowFinderService>();
|
||||
const activationErrorsService = mock<ActivationErrorsService>();
|
||||
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);
|
||||
|
||||
@@ -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: {
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
91
packages/cli/src/workflows/workflow-finder.service.ts
Normal file
91
packages/cli/src/workflows/workflow-finder.service.ts
Normal file
@@ -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<SharedWorkflow> = {};
|
||||
|
||||
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<SharedWorkflow> = {};
|
||||
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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<Array<Omit<WorkflowHistory, 'nodes' | 'connections'>>> {
|
||||
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<WorkflowHistory> {
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
|
||||
const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [
|
||||
'workflow:read',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<WorkflowEntity> {
|
||||
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<WorkflowEntity | undefined> {
|
||||
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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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<SharedWorkflow>(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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('EnterpriseWorkflowService', () => {
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user