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:
कारतोफ्फेलस्क्रिप्ट™
2025-05-06 15:11:05 +02:00
committed by GitHub
co-authored by Danny Martini
parent cdcd059248
commit 2bb190349b
85 changed files with 1011 additions and 775 deletions
@@ -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();
}
}