feat(core): Allow custom project roles from being set to a user project relation (#18926)

This commit is contained in:
Guillaume Jacquart
2025-09-01 10:22:33 +02:00
committed by GitHub
parent 5b5f60212a
commit 027edbe89d
32 changed files with 597 additions and 121 deletions

View File

@@ -16,6 +16,7 @@ describe('InviteUsersRequestDto', () => {
request: [
{ email: 'user1@example.com', role: 'global:member' },
{ email: 'user2@example.com', role: 'global:admin' },
{ email: 'user3@example.com', role: 'custom:role' },
],
},
])('should validate $name', ({ request }) => {
@@ -42,7 +43,7 @@ describe('InviteUsersRequestDto', () => {
request: [
{
email: 'user@example.com',
role: 'invalid-role',
role: 'global:owner',
},
],
expectedErrorPath: [0, 'role'],

View File

@@ -1,10 +1,9 @@
import { assignableGlobalRoleSchema } from '@n8n/permissions';
import { z } from 'zod';
const roleSchema = z.enum(['global:member', 'global:admin']);
const invitedUserSchema = z.object({
email: z.string().email(),
role: roleSchema.default('global:member'),
role: assignableGlobalRoleSchema.default('global:member'),
});
const invitationsSchema = z.array(invitedUserSchema);

View File

@@ -93,7 +93,7 @@ describe('AddUsersToProjectDto', () => {
relations: [
{
userId: 'user-123',
role: 'invalid-role',
role: '',
},
],
},

View File

@@ -31,13 +31,6 @@ describe('ChangeUserRoleInProject', () => {
},
expectedErrorPath: ['role'],
},
{
name: 'invalid role value',
request: {
role: 'invalid-role',
},
expectedErrorPath: ['role'],
},
{
name: 'personal owner role',
request: { role: PROJECT_OWNER_ROLE_SLUG },

View File

@@ -120,7 +120,7 @@ describe('UpdateProjectDto', () => {
relations: [
{
userId: 'user-123',
role: 'invalid-role',
role: 'project:personalOwner',
},
],
},

View File

@@ -1,6 +1,6 @@
import { teamRoleSchema } from '@n8n/permissions';
import { assignableProjectRoleSchema } from '@n8n/permissions';
import { Z } from 'zod-class';
export class ChangeUserRoleInProject extends Z.class({
role: teamRoleSchema,
role: assignableProjectRoleSchema,
}) {}

View File

@@ -11,27 +11,28 @@ describe('RoleChangeRequestDto', () => {
expect(result.error?.issues[0].message).toBe('New role is required');
});
it('should fail validation with invalid newRoleName', () => {
it('should fail validation with invalid newRoleName global:owner', () => {
const data = {
newRoleName: 'invalidRole',
newRoleName: 'global:owner',
};
const result = RoleChangeRequestDto.safeParse(data);
expect(result.success).toBe(false);
expect(result.error?.issues[0].path[0]).toBe('newRoleName');
expect(result.error?.issues[0].message).toBe(
"Invalid enum value. Expected 'global:admin' | 'global:member', received 'invalidRole'",
);
expect(result.error?.issues[0].message).toBe('This global role value is not assignable');
});
it('should pass validation with valid data', () => {
const data = {
newRoleName: 'global:admin',
};
it.each<string>(['global:admin', 'custom:role'])(
'should pass validation with valid newRoleName %s',
(role) => {
const data = {
newRoleName: role,
};
const result = RoleChangeRequestDto.safeParse(data);
const result = RoleChangeRequestDto.safeParse(data);
expect(result.success).toBe(true);
});
expect(result.success).toBe(true);
},
);
});

View File

@@ -1,8 +1,11 @@
import { z } from 'zod';
import { assignableGlobalRoleSchema } from '@n8n/permissions';
import { Z } from 'zod-class';
export class RoleChangeRequestDto extends Z.class({
newRoleName: z.enum(['global:admin', 'global:member'], {
required_error: 'New role is required',
}),
newRoleName: assignableGlobalRoleSchema
// enforce required (non-nullable, non-optional) with custom error message on undefined
.nullish()
.refine((val): val is NonNullable<typeof val> => val !== null && typeof val !== 'undefined', {
message: 'New role is required',
}),
}) {}

View File

@@ -82,7 +82,7 @@ describe('project.schema', () => {
},
{
name: 'invalid role',
value: { userId: 'user-123', role: 'invalid-role' },
value: { userId: 'user-123', role: 'project:personalOwner' },
expected: false,
},
{

View File

@@ -1,4 +1,4 @@
import { teamRoleSchema } from '@n8n/permissions';
import { assignableProjectRoleSchema } from '@n8n/permissions';
import { z } from 'zod';
export const projectNameSchema = z.string().min(1).max(255);
@@ -16,6 +16,6 @@ export const projectDescriptionSchema = z.string().max(512);
export const projectRelationSchema = z.object({
userId: z.string().min(1),
role: teamRoleSchema,
role: assignableProjectRoleSchema,
});
export type ProjectRelation = z.infer<typeof projectRelationSchema>;