mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2026-09-27 12:11:43 +02:00
refactor(core): Move more code into @n8n/permissions. Add aditional tests and docs (no-changelog) (#15062)
Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
co-authored by
Danny Martini
parent
cdcd059248
commit
2bb190349b
@@ -113,7 +113,7 @@ export class AuthService {
|
||||
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
||||
if (
|
||||
config.getEnv('userManagement.isInstanceOwnerSetUp') &&
|
||||
!user.isOwner &&
|
||||
user.role !== 'global:owner' &&
|
||||
!isWithinUsersLimit
|
||||
) {
|
||||
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ResolvePasswordTokenQueryDto,
|
||||
} from '@n8n/api-types';
|
||||
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
|
||||
import { hasGlobalScope } from '@n8n/permissions';
|
||||
import { Response } from 'express';
|
||||
import { Logger } from 'n8n-core';
|
||||
|
||||
@@ -62,18 +63,23 @@ export class PasswordResetController {
|
||||
|
||||
// User should just be able to reset password if one is already present
|
||||
const user = await this.userRepository.findNonShellUser(email);
|
||||
if (!user) {
|
||||
this.logger.debug('No user found in the system');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.isOwner && !this.license.isWithinUsersLimit()) {
|
||||
if (user.role !== 'global:owner' && !this.license.isWithinUsersLimit()) {
|
||||
this.logger.debug(
|
||||
'Request to send password reset email failed because the user limit was reached',
|
||||
);
|
||||
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
||||
}
|
||||
|
||||
if (
|
||||
isSamlCurrentAuthenticationMethod() &&
|
||||
!(
|
||||
user?.hasGlobalScope('user:resetPassword') === true ||
|
||||
user?.settings?.allowSSOManualLogin === true
|
||||
user &&
|
||||
(hasGlobalScope(user, 'user:resetPassword') || user.settings?.allowSSOManualLogin === true)
|
||||
)
|
||||
) {
|
||||
this.logger.debug(
|
||||
@@ -84,8 +90,8 @@ export class PasswordResetController {
|
||||
);
|
||||
}
|
||||
|
||||
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||
if (!user?.password || (ldapIdentity && user.disabled)) {
|
||||
const ldapIdentity = user.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||
if (!user.password || (ldapIdentity && user.disabled)) {
|
||||
this.logger.debug(
|
||||
'Request to send password reset email failed because no user was found for the provided email',
|
||||
{ invalidEmail: email },
|
||||
@@ -140,7 +146,7 @@ export class PasswordResetController {
|
||||
const user = await this.authService.resolvePasswordResetToken(token);
|
||||
if (!user) throw new NotFoundError('');
|
||||
|
||||
if (!user?.isOwner && !this.license.isWithinUsersLimit()) {
|
||||
if (user.role !== 'global:owner' && !this.license.isWithinUsersLimit()) {
|
||||
this.logger.debug(
|
||||
'Request to resolve password token failed because the user limit was reached',
|
||||
{ userId: user.id },
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
} from '@n8n/decorators';
|
||||
import { combineScopes } from '@n8n/permissions';
|
||||
import { combineScopes, getRoleScopes, hasGlobalScope } from '@n8n/permissions';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In, Not } from '@n8n/typeorm';
|
||||
@@ -30,13 +30,11 @@ import {
|
||||
TeamProjectOverQuotaError,
|
||||
UnlicensedProjectRoleError,
|
||||
} from '@/services/project.service.ee';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
|
||||
@RestController('/projects')
|
||||
export class ProjectController {
|
||||
constructor(
|
||||
private readonly projectsService: ProjectService,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly eventService: EventService,
|
||||
) {}
|
||||
@@ -69,8 +67,8 @@ export class ProjectController {
|
||||
role: 'project:admin',
|
||||
scopes: [
|
||||
...combineScopes({
|
||||
global: this.roleService.getRoleScopes(req.user.role),
|
||||
project: this.roleService.getRoleScopes('project:admin'),
|
||||
global: getRoleScopes(req.user.role),
|
||||
project: getRoleScopes('project:admin'),
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -88,7 +86,7 @@ export class ProjectController {
|
||||
_res: Response,
|
||||
): Promise<ProjectRequest.GetMyProjectsResponse> {
|
||||
const relations = await this.projectsService.getProjectRelationsForUser(req.user);
|
||||
const otherTeamProject = req.user.hasGlobalScope('project:read')
|
||||
const otherTeamProject = hasGlobalScope(req.user, 'project:read')
|
||||
? await this.projectRepository.findBy({
|
||||
type: 'team',
|
||||
id: Not(In(relations.map((pr) => pr.projectId))),
|
||||
@@ -106,8 +104,8 @@ export class ProjectController {
|
||||
if (result.scopes) {
|
||||
result.scopes.push(
|
||||
...combineScopes({
|
||||
global: this.roleService.getRoleScopes(req.user.role),
|
||||
project: this.roleService.getRoleScopes(pr.role),
|
||||
global: getRoleScopes(req.user.role),
|
||||
project: getRoleScopes(pr.role),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -128,9 +126,7 @@ export class ProjectController {
|
||||
);
|
||||
|
||||
if (result.scopes) {
|
||||
result.scopes.push(
|
||||
...combineScopes({ global: this.roleService.getRoleScopes(req.user.role) }),
|
||||
);
|
||||
result.scopes.push(...combineScopes({ global: getRoleScopes(req.user.role) }));
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
@@ -154,8 +150,8 @@ export class ProjectController {
|
||||
}
|
||||
const scopes: Scope[] = [
|
||||
...combineScopes({
|
||||
global: this.roleService.getRoleScopes(req.user.role),
|
||||
project: this.roleService.getRoleScopes('project:personalOwner'),
|
||||
global: getRoleScopes(req.user.role),
|
||||
project: getRoleScopes('project:personalOwner'),
|
||||
}),
|
||||
];
|
||||
return {
|
||||
@@ -191,8 +187,8 @@ export class ProjectController {
|
||||
})),
|
||||
scopes: [
|
||||
...combineScopes({
|
||||
global: this.roleService.getRoleScopes(req.user.role),
|
||||
...(myRelation ? { project: this.roleService.getRoleScopes(myRelation.role) } : {}),
|
||||
global: getRoleScopes(req.user.role),
|
||||
...(myRelation ? { project: getRoleScopes(myRelation.role) } : {}),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import { Get, RestController } from '@n8n/decorators';
|
||||
|
||||
import { type AllRoleTypes, RoleService } from '@/services/role.service';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
|
||||
@RestController('/roles')
|
||||
export class RoleController {
|
||||
constructor(private readonly roleService: RoleService) {}
|
||||
|
||||
@Get('/')
|
||||
async getAllRoles() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.roleService.getRoles()).map((e) => [
|
||||
e[0],
|
||||
(e[1] as AllRoleTypes[]).map((r) => ({
|
||||
name: this.roleService.getRoleName(r),
|
||||
role: r,
|
||||
scopes: this.roleService.getRoleScopes(r),
|
||||
licensed: this.roleService.isRoleLicensed(r),
|
||||
})),
|
||||
]),
|
||||
);
|
||||
getAllRoles() {
|
||||
return this.roleService.getAllRoles();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import type { CredentialsEntity, SharedCredentials, CredentialSharingRole, User } from '@n8n/db';
|
||||
import type { CredentialsEntity, SharedCredentials, User } from '@n8n/db';
|
||||
import { CredentialsRepository } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { hasGlobalScope, rolesWithScope } from '@n8n/permissions';
|
||||
import type { CredentialSharingRole, ProjectRole, 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 { 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,
|
||||
) {}
|
||||
|
||||
@@ -29,9 +27,9 @@ export class CredentialsFinderService {
|
||||
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);
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const credentialRoles = rolesWithScope('credential', scopes);
|
||||
where = {
|
||||
...where,
|
||||
shared: {
|
||||
@@ -53,9 +51,9 @@ export class CredentialsFinderService {
|
||||
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);
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const credentialRoles = rolesWithScope('credential', scopes);
|
||||
where = {
|
||||
...where,
|
||||
role: In(credentialRoles),
|
||||
@@ -85,9 +83,9 @@ export class CredentialsFinderService {
|
||||
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);
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const credentialRoles = rolesWithScope('credential', scopes);
|
||||
where = {
|
||||
role: In(credentialRoles),
|
||||
project: {
|
||||
@@ -115,13 +113,9 @@ export class CredentialsFinderService {
|
||||
trx?: EntityManager,
|
||||
) {
|
||||
const projectRoles =
|
||||
'scopes' in options
|
||||
? this.roleService.rolesWithScope('project', options.scopes)
|
||||
: options.projectRoles;
|
||||
'scopes' in options ? rolesWithScope('project', options.scopes) : options.projectRoles;
|
||||
const credentialRoles =
|
||||
'scopes' in options
|
||||
? this.roleService.rolesWithScope('credential', options.scopes)
|
||||
: options.credentialRoles;
|
||||
'scopes' in options ? rolesWithScope('credential', options.scopes) : options.credentialRoles;
|
||||
|
||||
const sharings = await this.sharedCredentialsRepository.findCredentialsByRoles(
|
||||
userIds,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Project, SharedCredentials } from '@n8n/db';
|
||||
import type { CredentialsEntity, User } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { hasGlobalScope, rolesWithScope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In, type EntityManager } from '@n8n/typeorm';
|
||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
@@ -10,7 +11,6 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { TransferCredentialError } from '@/errors/response-errors/transfer-credential.error';
|
||||
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';
|
||||
@@ -22,7 +22,6 @@ export class EnterpriseCredentialsService {
|
||||
private readonly ownershipService: OwnershipService,
|
||||
private readonly credentialsService: CredentialsService,
|
||||
private readonly projectService: ProjectService,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly credentialsFinderService: CredentialsFinderService,
|
||||
) {}
|
||||
|
||||
@@ -41,12 +40,12 @@ export class EnterpriseCredentialsService {
|
||||
type: 'team',
|
||||
// if user can see all projects, don't check project access
|
||||
// if they can't, find projects they can list
|
||||
...(user.hasGlobalScope('project:list')
|
||||
...(hasGlobalScope(user, 'project:list')
|
||||
? {}
|
||||
: {
|
||||
projectRelations: {
|
||||
userId: user.id,
|
||||
role: In(this.roleService.rolesWithScope('project', 'project:list')),
|
||||
role: In(rolesWithScope('project', 'project:list')),
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CreateCredentialDto } from '@n8n/api-types';
|
||||
import type { Project, User, ICredentialsDb, ScopesField } from '@n8n/db';
|
||||
import { CredentialsEntity, SharedCredentials, CredentialsRepository } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { hasGlobalScope, type Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import {
|
||||
In,
|
||||
@@ -79,7 +79,7 @@ export class CredentialsService {
|
||||
onlySharedWithMe?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const returnAll = user.hasGlobalScope('credential:list');
|
||||
const returnAll = hasGlobalScope(user, 'credential:list');
|
||||
const isDefaultSelect = !listQueryOptions.select;
|
||||
const projectId =
|
||||
typeof listQueryOptions.filter?.projectId === 'string'
|
||||
@@ -255,7 +255,7 @@ export class CredentialsService {
|
||||
// If the workflow is owned by a personal project and the owner of the
|
||||
// project has global read permissions it can use all personal credentials.
|
||||
const user = await this.userRepository.findPersonalOwnerForWorkflow(workflowId);
|
||||
if (user?.hasGlobalScope('credential:read')) {
|
||||
if (user && hasGlobalScope(user, 'credential:read')) {
|
||||
return await this.credentialsRepository.findAllPersonalCredentials();
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ export class CredentialsService {
|
||||
// read permissions then all workflows in that project can use all
|
||||
// credentials of all personal projects.
|
||||
const user = await this.userRepository.findPersonalOwnerForProject(projectId);
|
||||
if (user?.hasGlobalScope('credential:read')) {
|
||||
if (user && hasGlobalScope(user, 'credential:read')) {
|
||||
return await this.credentialsRepository.findAllPersonalCredentials();
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ export class CredentialsService {
|
||||
): Promise<SharedCredentials | null> {
|
||||
let where: FindOptionsWhere<SharedCredentials> = { credentialsId: credentialId };
|
||||
|
||||
if (!user.hasGlobalScope(globalScopes, { mode: 'allOf' })) {
|
||||
if (!hasGlobalScope(user, globalScopes, { mode: 'allOf' })) {
|
||||
where = {
|
||||
...where,
|
||||
role: 'credential:owner',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { generateNanoId } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import type { ProjectRole } from '@n8n/permissions';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { ProjectRelation } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { ProjectRole } from '@n8n/permissions';
|
||||
import { DataSource, In, Repository } from '@n8n/typeorm';
|
||||
|
||||
@Service()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { SharedCredentials } from '@n8n/db';
|
||||
import type { Project, CredentialSharingRole } from '@n8n/db';
|
||||
import type { Project } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { CredentialSharingRole, ProjectRole } from '@n8n/permissions';
|
||||
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
||||
import { DataSource, In, Not, Repository } from '@n8n/typeorm';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SharedWorkflow } from '@n8n/db';
|
||||
import type { Project, WorkflowSharingRole } from '@n8n/db';
|
||||
import type { Project } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { WorkflowSharingRole } from '@n8n/permissions';
|
||||
import { DataSource, Repository, In, Not } from '@n8n/typeorm';
|
||||
import type { EntityManager, FindManyOptions, FindOptionsWhere } from '@n8n/typeorm';
|
||||
|
||||
|
||||
+1
-3
@@ -95,9 +95,7 @@ describe('CredentialsPermissionChecker', () => {
|
||||
});
|
||||
|
||||
it('should skip credential checks if the home project owner has global scope', async () => {
|
||||
const projectOwner = mock<User>({
|
||||
hasGlobalScope: (scope) => scope === 'credential:list',
|
||||
});
|
||||
const projectOwner = mock<User>({ role: 'global:owner' });
|
||||
ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner);
|
||||
|
||||
await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Project } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { hasGlobalScope } from '@n8n/permissions';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
|
||||
@@ -45,7 +46,11 @@ export class CredentialsPermissionChecker {
|
||||
const homeProjectOwner = await this.ownershipService.getPersonalProjectOwnerCached(
|
||||
homeProject.id,
|
||||
);
|
||||
if (homeProject.type === 'personal' && homeProjectOwner?.hasGlobalScope('credential:list')) {
|
||||
if (
|
||||
homeProject.type === 'personal' &&
|
||||
homeProjectOwner &&
|
||||
hasGlobalScope(homeProjectOwner, 'credential:list')
|
||||
) {
|
||||
// Workflow belongs to a project by a user with privileges
|
||||
// so all credentials are usable. Skip credential checks.
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ICredentialsBase, IExecutionBase, IExecutionDb, ITagBase } from '@n8n/db';
|
||||
import type { AssignableRole } from '@n8n/permissions';
|
||||
import type { AssignableGlobalRole } from '@n8n/permissions';
|
||||
import type { Application } from 'express';
|
||||
import type {
|
||||
ExecutionError,
|
||||
@@ -207,7 +207,7 @@ export interface ILicensePostResponse extends ILicenseReadResponse {
|
||||
|
||||
export interface Invitation {
|
||||
email: string;
|
||||
role: AssignableRole;
|
||||
role: AssignableGlobalRole;
|
||||
}
|
||||
|
||||
export interface N8nApp {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { hasGlobalScope, rolesWithScope, type Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
@@ -8,7 +8,6 @@ import { UnexpectedError } from 'n8n-workflow';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
|
||||
/**
|
||||
* Check if a user has the required scopes. The check can be:
|
||||
@@ -28,15 +27,14 @@ export async function userHasScopes(
|
||||
projectId,
|
||||
}: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */,
|
||||
): Promise<boolean> {
|
||||
if (user.hasGlobalScope(scopes, { mode: 'allOf' })) return true;
|
||||
if (hasGlobalScope(user, scopes, { mode: 'allOf' })) return true;
|
||||
|
||||
if (globalOnly) return false;
|
||||
|
||||
// Find which project roles are defined to contain the required scopes.
|
||||
// Then find projects having this user and having those project roles.
|
||||
|
||||
const roleService = Container.get(RoleService);
|
||||
const projectRoles = roleService.rolesWithScope('project', scopes);
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const userProjectIds = (
|
||||
await Container.get(ProjectRepository).find({
|
||||
where: {
|
||||
@@ -57,7 +55,7 @@ export async function userHasScopes(
|
||||
return await Container.get(SharedCredentialsRepository).existsBy({
|
||||
credentialsId: credentialId,
|
||||
projectId: In(userProjectIds),
|
||||
role: In(roleService.rolesWithScope('credential', scopes)),
|
||||
role: In(rolesWithScope('credential', scopes)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ export async function userHasScopes(
|
||||
return await Container.get(SharedWorkflowRepository).existsBy({
|
||||
workflowId,
|
||||
projectId: In(userProjectIds),
|
||||
role: In(roleService.rolesWithScope('workflow', scopes)),
|
||||
role: In(rolesWithScope('workflow', scopes)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import type { Project, WorkflowSharingRole, User } from '@n8n/db';
|
||||
import type { Project, User } from '@n8n/db';
|
||||
import { WorkflowEntity, WorkflowTagMapping, SharedWorkflow } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { Scope, WorkflowSharingRole } from '@n8n/permissions';
|
||||
import type { WorkflowId } from 'n8n-workflow';
|
||||
|
||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProjectIcon, ProjectRole, ProjectType } from '@n8n/api-types';
|
||||
import type { ProjectIcon, ProjectType } from '@n8n/api-types';
|
||||
import type { Variables, Project, User, ListQueryDb, WorkflowHistory } from '@n8n/db';
|
||||
import type { AssignableRole, GlobalRole, Scope } from '@n8n/permissions';
|
||||
import type { AssignableGlobalRole, GlobalRole, ProjectRole, Scope } from '@n8n/permissions';
|
||||
import type express from 'express';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
@@ -137,7 +137,7 @@ export declare namespace UserRequest {
|
||||
email: string;
|
||||
inviteAcceptUrl?: string;
|
||||
emailSent: boolean;
|
||||
role: AssignableRole;
|
||||
role: AssignableGlobalRole;
|
||||
};
|
||||
error?: string;
|
||||
};
|
||||
|
||||
@@ -43,21 +43,19 @@ describe('ActiveWorkflowsService', () => {
|
||||
});
|
||||
|
||||
it('should return all workflow ids when user has full access', async () => {
|
||||
user.hasGlobalScope.mockReturnValue(true);
|
||||
user.role = 'global:admin';
|
||||
const ids = await service.getAllActiveIdsFor(user);
|
||||
|
||||
expect(ids).toEqual(['2', '3', '4']);
|
||||
expect(user.hasGlobalScope).toHaveBeenCalledWith('workflow:list');
|
||||
expect(sharedWorkflowRepository.getSharedWorkflowIds).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter out workflow ids that the user does not have access to', async () => {
|
||||
user.hasGlobalScope.mockReturnValue(false);
|
||||
user.role = 'global:member';
|
||||
sharedWorkflowRepository.getSharedWorkflowIds.mockResolvedValue(['3']);
|
||||
const ids = await service.getAllActiveIdsFor(user);
|
||||
|
||||
expect(ids).toEqual(['3']);
|
||||
expect(user.hasGlobalScope).toHaveBeenCalledWith('workflow:list');
|
||||
expect(sharedWorkflowRepository.getSharedWorkflowIds).toHaveBeenCalledWith(activeIds);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@ import { SharedCredentials } from '@n8n/db';
|
||||
import type { CredentialsEntity } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { hasScope } from '@n8n/permissions';
|
||||
import { GLOBAL_MEMBER_SCOPES, GLOBAL_OWNER_SCOPES } from '@n8n/permissions';
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
@@ -19,19 +17,11 @@ describe('CredentialsFinderService', () => {
|
||||
const sharedCredential = mock<SharedCredentials>();
|
||||
sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId });
|
||||
const owner = mock<User>({
|
||||
isOwner: true,
|
||||
hasGlobalScope: (scope) =>
|
||||
hasScope(scope, {
|
||||
global: GLOBAL_OWNER_SCOPES,
|
||||
}),
|
||||
role: 'global:owner',
|
||||
});
|
||||
const member = mock<User>({
|
||||
isOwner: false,
|
||||
role: 'global:member',
|
||||
id: 'test',
|
||||
hasGlobalScope: (scope) =>
|
||||
hasScope(scope, {
|
||||
global: GLOBAL_MEMBER_SCOPES,
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { User } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { hasGlobalScope } from '@n8n/permissions';
|
||||
import { Logger } from 'n8n-core';
|
||||
|
||||
import { ActivationErrorsService } from '@/activation-errors.service';
|
||||
@@ -28,7 +29,7 @@ export class ActiveWorkflowsService {
|
||||
const activationErrors = await this.activationErrorsService.getAll();
|
||||
const activeWorkflowIds = await this.workflowRepository.getActiveIds();
|
||||
|
||||
const hasFullAccess = user.hasGlobalScope('workflow:list');
|
||||
const hasFullAccess = hasGlobalScope(user, 'workflow:list');
|
||||
if (hasFullAccess) {
|
||||
return activeWorkflowIds.filter((workflowId) => !activationErrors[workflowId]);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateProjectDto, ProjectRole, ProjectType, UpdateProjectDto } from '@n8n/api-types';
|
||||
import type { CreateProjectDto, ProjectType, UpdateProjectDto } from '@n8n/api-types';
|
||||
import { UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
|
||||
import type { User } from '@n8n/db';
|
||||
import { Project, ProjectRelation } from '@n8n/db';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { type Scope } from '@n8n/permissions';
|
||||
import { hasGlobalScope, rolesWithScope, type Scope, type ProjectRole } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import type { FindOptionsWhere, EntityManager } from '@n8n/typeorm';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
@@ -20,7 +20,6 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { License } from '@/license';
|
||||
|
||||
import { CacheService } from './cache/cache.service';
|
||||
import { RoleService } from './role.service';
|
||||
|
||||
export class TeamProjectOverQuotaError extends UserError {
|
||||
constructor(limit: number) {
|
||||
@@ -42,7 +41,6 @@ export class ProjectService {
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly license: License,
|
||||
@@ -168,7 +166,7 @@ export class ProjectService {
|
||||
|
||||
async getAccessibleProjects(user: User): Promise<Project[]> {
|
||||
// This user is probably an admin, show them everything
|
||||
if (user.hasGlobalScope('project:read')) {
|
||||
if (hasGlobalScope(user, 'project:read')) {
|
||||
return await this.projectRepository.find();
|
||||
}
|
||||
return await this.projectRepository.getAccessibleProjects(user.id);
|
||||
@@ -234,7 +232,7 @@ export class ProjectService {
|
||||
const existing = project.projectRelations.find((pr) => pr.userId === r.userId);
|
||||
// We don't throw an error if the user already exists with that role so
|
||||
// existing projects continue working as is.
|
||||
if (existing?.role !== r.role && !this.roleService.isRoleLicensed(r.role)) {
|
||||
if (existing?.role !== r.role && !this.isProjectRoleLicensed(r.role)) {
|
||||
throw new UnlicensedProjectRoleError(r.role);
|
||||
}
|
||||
}
|
||||
@@ -246,6 +244,19 @@ export class ProjectService {
|
||||
await this.clearCredentialCanUseExternalSecretsCache(projectId);
|
||||
}
|
||||
|
||||
private isProjectRoleLicensed(role: ProjectRole) {
|
||||
switch (role) {
|
||||
case 'project:admin':
|
||||
return this.license.isProjectRoleAdminLicensed();
|
||||
case 'project:editor':
|
||||
return this.license.isProjectRoleEditorLicensed();
|
||||
case 'project:viewer':
|
||||
return this.license.isProjectRoleViewerLicensed();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async clearCredentialCanUseExternalSecretsCache(projectId: string) {
|
||||
const shares = await this.sharedCredentialsRepository.find({
|
||||
where: {
|
||||
@@ -293,8 +304,8 @@ export class ProjectService {
|
||||
id: projectId,
|
||||
};
|
||||
|
||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
|
||||
where = {
|
||||
...where,
|
||||
|
||||
@@ -1,159 +1,30 @@
|
||||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import type {
|
||||
CredentialsEntity,
|
||||
CredentialSharingRole,
|
||||
SharedCredentials,
|
||||
SharedWorkflow,
|
||||
WorkflowSharingRole,
|
||||
User,
|
||||
ListQueryDb,
|
||||
ScopesField,
|
||||
ProjectRelation,
|
||||
} from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { GlobalRole, Resource, Scope } from '@n8n/permissions';
|
||||
import {
|
||||
combineScopes,
|
||||
GLOBAL_ADMIN_SCOPES,
|
||||
GLOBAL_MEMBER_SCOPES,
|
||||
GLOBAL_OWNER_SCOPES,
|
||||
PERSONAL_PROJECT_OWNER_SCOPES,
|
||||
PROJECT_EDITOR_SCOPES,
|
||||
PROJECT_VIEWER_SCOPES,
|
||||
REGULAR_PROJECT_ADMIN_SCOPES,
|
||||
CREDENTIALS_SHARING_OWNER_SCOPES,
|
||||
CREDENTIALS_SHARING_USER_SCOPES,
|
||||
WORKFLOW_SHARING_EDITOR_SCOPES,
|
||||
WORKFLOW_SHARING_OWNER_SCOPES,
|
||||
} from '@n8n/permissions';
|
||||
import type { AllRoleTypes, Scope } from '@n8n/permissions';
|
||||
import { ALL_ROLES, combineScopes, getRoleScopes } from '@n8n/permissions';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
import { License } from '@/license';
|
||||
|
||||
export type RoleNamespace = 'global' | 'project' | 'credential' | 'workflow';
|
||||
|
||||
const GLOBAL_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
|
||||
'global:owner': GLOBAL_OWNER_SCOPES,
|
||||
'global:admin': GLOBAL_ADMIN_SCOPES,
|
||||
'global:member': GLOBAL_MEMBER_SCOPES,
|
||||
};
|
||||
|
||||
const PROJECT_SCOPE_MAP: Record<ProjectRole, Scope[]> = {
|
||||
'project:admin': REGULAR_PROJECT_ADMIN_SCOPES,
|
||||
'project:personalOwner': PERSONAL_PROJECT_OWNER_SCOPES,
|
||||
'project:editor': PROJECT_EDITOR_SCOPES,
|
||||
'project:viewer': PROJECT_VIEWER_SCOPES,
|
||||
};
|
||||
|
||||
const CREDENTIALS_SHARING_SCOPE_MAP: Record<CredentialSharingRole, Scope[]> = {
|
||||
'credential:owner': CREDENTIALS_SHARING_OWNER_SCOPES,
|
||||
'credential:user': CREDENTIALS_SHARING_USER_SCOPES,
|
||||
};
|
||||
|
||||
const WORKFLOW_SHARING_SCOPE_MAP: Record<WorkflowSharingRole, Scope[]> = {
|
||||
'workflow:owner': WORKFLOW_SHARING_OWNER_SCOPES,
|
||||
'workflow:editor': WORKFLOW_SHARING_EDITOR_SCOPES,
|
||||
};
|
||||
|
||||
interface AllMaps {
|
||||
global: Record<GlobalRole, Scope[]>;
|
||||
project: Record<ProjectRole, Scope[]>;
|
||||
credential: Record<CredentialSharingRole, Scope[]>;
|
||||
workflow: Record<WorkflowSharingRole, Scope[]>;
|
||||
}
|
||||
|
||||
const ALL_MAPS: AllMaps = {
|
||||
global: GLOBAL_SCOPE_MAP,
|
||||
project: PROJECT_SCOPE_MAP,
|
||||
credential: CREDENTIALS_SHARING_SCOPE_MAP,
|
||||
workflow: WORKFLOW_SHARING_SCOPE_MAP,
|
||||
} as const;
|
||||
|
||||
const COMBINED_MAP = Object.fromEntries(
|
||||
Object.values(ALL_MAPS).flatMap((o: Record<string, Scope[]>) => Object.entries(o)),
|
||||
) as Record<GlobalRole | ProjectRole | CredentialSharingRole | WorkflowSharingRole, Scope[]>;
|
||||
|
||||
export interface RoleMap {
|
||||
global: GlobalRole[];
|
||||
project: ProjectRole[];
|
||||
credential: CredentialSharingRole[];
|
||||
workflow: WorkflowSharingRole[];
|
||||
}
|
||||
export type AllRoleTypes = GlobalRole | ProjectRole | WorkflowSharingRole | CredentialSharingRole;
|
||||
|
||||
const ROLE_NAMES: Record<
|
||||
GlobalRole | ProjectRole | WorkflowSharingRole | CredentialSharingRole,
|
||||
string
|
||||
> = {
|
||||
'global:owner': 'Owner',
|
||||
'global:admin': 'Admin',
|
||||
'global:member': 'Member',
|
||||
'project:personalOwner': 'Project Owner',
|
||||
'project:admin': 'Project Admin',
|
||||
'project:editor': 'Project Editor',
|
||||
'project:viewer': 'Project Viewer',
|
||||
'credential:user': 'Credential User',
|
||||
'credential:owner': 'Credential Owner',
|
||||
'workflow:owner': 'Workflow Owner',
|
||||
'workflow:editor': 'Workflow Editor',
|
||||
};
|
||||
|
||||
// export type ScopesField = { scopes: Scope[] };
|
||||
|
||||
@Service()
|
||||
export class RoleService {
|
||||
constructor(private readonly license: License) {}
|
||||
|
||||
rolesWithScope(namespace: 'global', scopes: Scope | Scope[]): GlobalRole[];
|
||||
rolesWithScope(namespace: 'project', scopes: Scope | Scope[]): ProjectRole[];
|
||||
rolesWithScope(namespace: 'credential', scopes: Scope | Scope[]): CredentialSharingRole[];
|
||||
rolesWithScope(namespace: 'workflow', scopes: Scope | Scope[]): WorkflowSharingRole[];
|
||||
rolesWithScope(namespace: RoleNamespace, scopes: Scope | Scope[]) {
|
||||
if (!Array.isArray(scopes)) {
|
||||
scopes = [scopes];
|
||||
}
|
||||
|
||||
return Object.keys(ALL_MAPS[namespace]).filter((k) => {
|
||||
return scopes.every((s) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
((ALL_MAPS[namespace] as any)[k] as Scope[]).includes(s),
|
||||
);
|
||||
getAllRoles() {
|
||||
Object.values(ALL_ROLES).forEach((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
entry.licensed = this.isRoleLicensed(entry.role);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRoles(): RoleMap {
|
||||
return Object.fromEntries(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
Object.entries(ALL_MAPS).map((e) => [e[0], Object.keys(e[1])]),
|
||||
) as unknown as RoleMap;
|
||||
}
|
||||
|
||||
getRoleName(role: AllRoleTypes): string {
|
||||
return ROLE_NAMES[role];
|
||||
}
|
||||
|
||||
getRoleScopes(
|
||||
role: GlobalRole | ProjectRole | WorkflowSharingRole | CredentialSharingRole,
|
||||
filters?: Resource[],
|
||||
): Scope[] {
|
||||
let scopes = COMBINED_MAP[role];
|
||||
if (filters) {
|
||||
scopes = scopes.filter((s) => filters.includes(s.split(':')[0] as Resource));
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all distinct scopes in a set of project roles.
|
||||
*/
|
||||
getScopesBy(projectRoles: Set<ProjectRole>) {
|
||||
return [...projectRoles].reduce<Set<Scope>>((acc, projectRole) => {
|
||||
for (const scope of PROJECT_SCOPE_MAP[projectRole] ?? []) {
|
||||
acc.add(scope);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Set());
|
||||
return ALL_ROLES;
|
||||
}
|
||||
|
||||
addScopes(
|
||||
@@ -192,9 +63,7 @@ export class RoleService {
|
||||
| ListQueryDb.Workflow.WithScopes
|
||||
| ListQueryDb.Credentials.WithScopes;
|
||||
|
||||
Object.assign(entity, {
|
||||
scopes: [],
|
||||
});
|
||||
entity.scopes = [];
|
||||
|
||||
if (shared === undefined) {
|
||||
return entity;
|
||||
@@ -220,7 +89,7 @@ export class RoleService {
|
||||
shared: SharedCredentials[] | SharedWorkflow[],
|
||||
userProjectRelations: ProjectRelation[],
|
||||
): Scope[] {
|
||||
const globalScopes = this.getRoleScopes(user.role, [type]);
|
||||
const globalScopes = getRoleScopes(user.role, [type]);
|
||||
const scopesSet: Set<Scope> = new Set(globalScopes);
|
||||
for (const sharedEntity of shared) {
|
||||
const pr = userProjectRelations.find(
|
||||
@@ -228,9 +97,9 @@ export class RoleService {
|
||||
);
|
||||
let projectScopes: Scope[] = [];
|
||||
if (pr) {
|
||||
projectScopes = this.getRoleScopes(pr.role);
|
||||
projectScopes = getRoleScopes(pr.role);
|
||||
}
|
||||
const resourceMask = this.getRoleScopes(sharedEntity.role);
|
||||
const resourceMask = getRoleScopes(sharedEntity.role);
|
||||
const mergedScopes = combineScopes(
|
||||
{
|
||||
global: globalScopes,
|
||||
@@ -243,7 +112,8 @@ export class RoleService {
|
||||
return [...scopesSet].sort();
|
||||
}
|
||||
|
||||
isRoleLicensed(role: AllRoleTypes) {
|
||||
private isRoleLicensed(role: AllRoleTypes) {
|
||||
// TODO: move this info into FrontendSettings
|
||||
switch (role) {
|
||||
case 'project:admin':
|
||||
return this.license.isProjectRoleAdminLicensed();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RoleChangeRequestDto } from '@n8n/api-types';
|
||||
import { User } from '@n8n/db';
|
||||
import type { PublicUser } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { AssignableRole } from '@n8n/permissions';
|
||||
import { getGlobalScopes, type AssignableGlobalRole } from '@n8n/permissions';
|
||||
import { Logger } from 'n8n-core';
|
||||
import type { IUserSettings } from 'n8n-workflow';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
@@ -71,6 +71,7 @@ export class UserService {
|
||||
let publicUser: PublicUser = {
|
||||
...rest,
|
||||
signInType: ldapIdentity ? 'ldap' : 'email',
|
||||
isOwner: user.role === 'global:owner',
|
||||
};
|
||||
|
||||
if (options?.withInviteUrl && !options?.inviterId) {
|
||||
@@ -85,8 +86,9 @@ export class UserService {
|
||||
publicUser = await this.addFeatureFlags(publicUser, options.posthog);
|
||||
}
|
||||
|
||||
// TODO: resolve these directly in the frontend
|
||||
if (options?.withScopes) {
|
||||
publicUser.globalScopes = user.globalScopes;
|
||||
publicUser.globalScopes = getGlobalScopes(user);
|
||||
}
|
||||
|
||||
return publicUser;
|
||||
@@ -123,7 +125,7 @@ export class UserService {
|
||||
private async sendEmails(
|
||||
owner: User,
|
||||
toInviteUsers: { [key: string]: string },
|
||||
role: AssignableRole,
|
||||
role: AssignableGlobalRole,
|
||||
) {
|
||||
const domain = this.urlService.getInstanceBaseUrl();
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { SharedWorkflow, User } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { hasGlobalScope, rolesWithScope, 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 { 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,
|
||||
) {}
|
||||
constructor(private readonly sharedWorkflowRepository: SharedWorkflowRepository) {}
|
||||
|
||||
async findWorkflowForUser(
|
||||
workflowId: string,
|
||||
@@ -28,9 +24,9 @@ export class WorkflowFinderService {
|
||||
) {
|
||||
let where: FindOptionsWhere<SharedWorkflow> = {};
|
||||
|
||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
||||
const workflowRoles = this.roleService.rolesWithScope('workflow', scopes);
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const workflowRoles = rolesWithScope('workflow', scopes);
|
||||
|
||||
where = {
|
||||
role: In(workflowRoles),
|
||||
@@ -60,9 +56,9 @@ export class WorkflowFinderService {
|
||||
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);
|
||||
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||
const projectRoles = rolesWithScope('project', scopes);
|
||||
const workflowRoles = rolesWithScope('workflow', scopes);
|
||||
|
||||
where = {
|
||||
...where,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import type { WorkflowSharingRole, User } from '@n8n/db';
|
||||
import type { User } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import {
|
||||
hasGlobalScope,
|
||||
rolesWithScope,
|
||||
type ProjectRole,
|
||||
type WorkflowSharingRole,
|
||||
type Scope,
|
||||
} from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
|
||||
@@ -33,7 +38,7 @@ export class WorkflowSharingService {
|
||||
async getSharedWorkflowIds(user: User, options: ShareWorkflowOptions): Promise<string[]> {
|
||||
const { projectId } = options;
|
||||
|
||||
if (user.hasGlobalScope('workflow:read')) {
|
||||
if (hasGlobalScope(user, 'workflow:read')) {
|
||||
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
||||
select: ['workflowId'],
|
||||
...(projectId && { where: { projectId } }),
|
||||
@@ -42,13 +47,9 @@ export class WorkflowSharingService {
|
||||
}
|
||||
|
||||
const projectRoles =
|
||||
'scopes' in options
|
||||
? this.roleService.rolesWithScope('project', options.scopes)
|
||||
: options.projectRoles;
|
||||
'scopes' in options ? rolesWithScope('project', options.scopes) : options.projectRoles;
|
||||
const workflowRoles =
|
||||
'scopes' in options
|
||||
? this.roleService.rolesWithScope('workflow', options.scopes)
|
||||
: options.workflowRoles;
|
||||
'scopes' in options ? rolesWithScope('workflow', options.scopes) : options.workflowRoles;
|
||||
|
||||
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user