n8n/packages/cli/src/controllers/role.controller.ts

207 lines
5.7 KiB
TypeScript

import {
CreateRoleDto,
RoleAssignmentsResponseDto,
RoleDeleteQueryDto,
RoleGetQueryDto,
RoleListQueryDto,
RoleMembersResponseDto,
RoleProjectMembersResponseDto,
UpdateRoleDto,
} from '@n8n/api-types';
import type {
RoleAssignmentsResponse,
RoleMembersResponse,
RoleProjectMembersResponse,
} from '@n8n/api-types';
import { EventService, RoleService } from '@n8n/backend-services';
import { LICENSE_FEATURES } from '@n8n/constants';
import { AuthenticatedRequest } from '@n8n/db';
import {
Body,
Delete,
Get,
GlobalScope,
Licensed,
Param,
Patch,
Post,
Query,
RestController,
} from '@n8n/decorators';
import { Role as RoleDTO } from '@n8n/permissions';
import { NotFoundError } from '@n8n/errors';
import { ProjectService } from '@/services/project.service.ee';
import { assertCanManageRoleType, canReassignUsers } from '@/services/role-authorization';
@RestController('/roles')
export class RoleController {
constructor(
private readonly roleService: RoleService,
private readonly eventService: EventService,
private readonly projectService: ProjectService,
) {}
@Get('/')
async getAllRoles(
_req: AuthenticatedRequest,
_res: Response,
@Query query: RoleListQueryDto,
): Promise<Record<string, RoleDTO[]>> {
const allRoles = await this.roleService.getAllRoles(query.withUsageCount);
return {
global: allRoles.filter((r) => r.roleType === 'global'),
project: allRoles.filter((r) => r.roleType === 'project'),
credential: allRoles.filter((r) => r.roleType === 'credential'),
workflow: allRoles.filter((r) => r.roleType === 'workflow'),
};
}
@Get('/:slug/assignments/:projectId/members')
async getRoleProjectMembers(
req: AuthenticatedRequest,
_res: Response,
@Param('slug') slug: string,
@Param('projectId') projectId: string,
): Promise<RoleProjectMembersResponse> {
const role = await this.roleService.getRole(slug);
assertCanManageRoleType({
roleType: role.roleType,
user: req.user,
});
// Managing a role is an instance-wide capability, but the identities of a project's
// members are project data. Gate them on the same `project:list` check the sibling
// user and project routes use, so a role manager only sees projects it can already see.
const project = await this.projectService.getProjectWithScope(req.user, projectId, [
'project:list',
]);
if (!project) {
throw new NotFoundError('Project not found');
}
const result = await this.roleService.getRoleProjectMembers(slug, projectId);
return RoleProjectMembersResponseDto.parse(result);
}
@Get('/:slug/assignments')
async getRoleAssignments(
req: AuthenticatedRequest,
_res: Response,
@Param('slug') slug: string,
): Promise<RoleAssignmentsResponse> {
const role = await this.roleService.getRole(slug);
assertCanManageRoleType({
roleType: role.roleType,
user: req.user,
});
const result = await this.roleService.getRoleAssignments(slug);
if (result.projects.length === 0) {
return RoleAssignmentsResponseDto.parse(result);
}
// `projects` only names the projects the caller can already see. `totalProjects` stays
// the instance-wide count so the assignments tab can say "showing 1 of 3" rather than
// claim the role is unassigned; a bare number names no project and no member. The
// delete-impact warning is a different field — `usedByProjects`, served by `getRole`.
const visibleProjectIds = new Set(
await this.projectService.getProjectIdsWithScope(
req.user,
['project:list'],
result.projects.map((project) => project.projectId),
),
);
return RoleAssignmentsResponseDto.parse({
...result,
projects: result.projects.filter((project) => visibleProjectIds.has(project.projectId)),
});
}
@Get('/:slug/members')
@GlobalScope('role:read')
async getRoleMembers(
_req: AuthenticatedRequest,
_res: Response,
@Param('slug') slug: string,
): Promise<RoleMembersResponse> {
const result = await this.roleService.getRoleMembers(slug);
return RoleMembersResponseDto.parse(result);
}
@Get('/:slug')
async getRoleBySlug(
_req: AuthenticatedRequest,
_res: Response,
@Param('slug') slug: string,
@Query query: RoleGetQueryDto,
): Promise<RoleDTO> {
return await this.roleService.getRole(slug, query.withUsageCount);
}
@Patch('/:slug')
@Licensed(LICENSE_FEATURES.CUSTOM_ROLES)
async updateRole(
req: AuthenticatedRequest,
_res: Response,
@Param('slug') slug: string,
@Body updateRole: UpdateRoleDto,
): Promise<RoleDTO> {
const role = await this.roleService.getRole(slug);
assertCanManageRoleType({
roleType: role.roleType,
user: req.user,
});
return await this.roleService.updateCustomRole({
slug,
newRole: updateRole,
userId: req.user.id,
});
}
@Delete('/:slug')
@Licensed(LICENSE_FEATURES.CUSTOM_ROLES)
async deleteRole(
req: AuthenticatedRequest,
_res: Response,
@Param('slug') slug: string,
@Query query: RoleDeleteQueryDto,
): Promise<RoleDTO> {
const role = await this.roleService.getRole(slug);
assertCanManageRoleType({
roleType: role.roleType,
user: req.user,
});
const reassignRoleSlug = canReassignUsers({ role, user: req.user })
? query.reassignRoleSlug
: undefined;
return await this.roleService.removeCustomRole({
slug,
reassignRoleSlug,
userId: req.user.id,
});
}
@Post('/')
@Licensed(LICENSE_FEATURES.CUSTOM_ROLES)
async createRole(
req: AuthenticatedRequest,
_res: Response,
@Body createRole: CreateRoleDto,
): Promise<RoleDTO> {
assertCanManageRoleType({
roleType: createRole.roleType,
user: req.user,
});
const result = await this.roleService.createCustomRole(createRole);
this.eventService.emit('custom-role-created', {
userId: req.user.id,
roleSlug: result.slug,
scopes: result.scopes,
source: 'ui',
});
return result;
}
}