Skip to content

Commit 2bb1903

Browse files
netroydespairblue
andauthored
refactor(core): Move more code into @n8n/permissions. Add aditional tests and docs (no-changelog) (#15062)
Co-authored-by: Danny Martini <danny@n8n.io>
1 parent cdcd059 commit 2bb1903

File tree

85 files changed

+1010
-774
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+1010
-774
lines changed

packages/@n8n/api-types/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export { passwordSchema } from './schemas/password.schema';
1818
export type {
1919
ProjectType,
2020
ProjectIcon,
21-
ProjectRole,
2221
ProjectRelation,
2322
} from './schemas/project.schema';
2423

packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
projectNameSchema,
33
projectTypeSchema,
44
projectIconSchema,
5-
projectRoleSchema,
65
projectRelationSchema,
76
} from '../project.schema';
87

@@ -57,19 +56,6 @@ describe('project.schema', () => {
5756
});
5857
});
5958

60-
describe('projectRoleSchema', () => {
61-
test.each([
62-
{ name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true },
63-
{ name: 'valid role: project:admin', value: 'project:admin', expected: true },
64-
{ name: 'valid role: project:editor', value: 'project:editor', expected: true },
65-
{ name: 'valid role: project:viewer', value: 'project:viewer', expected: true },
66-
{ name: 'invalid role', value: 'invalid-role', expected: false },
67-
])('should validate $name', ({ value, expected }) => {
68-
const result = projectRoleSchema.safeParse(value);
69-
expect(result.success).toBe(expected);
70-
});
71-
});
72-
7359
describe('projectRelationSchema', () => {
7460
test.each([
7561
{

packages/@n8n/api-types/src/schemas/project.schema.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { projectRoleSchema } from '@n8n/permissions';
12
import { z } from 'zod';
23

34
export const projectNameSchema = z.string().min(1).max(255);
@@ -11,14 +12,6 @@ export const projectIconSchema = z.object({
1112
});
1213
export type ProjectIcon = z.infer<typeof projectIconSchema>;
1314

14-
export const projectRoleSchema = z.enum([
15-
'project:personalOwner', // personalOwner is only used for personal projects
16-
'project:admin',
17-
'project:editor',
18-
'project:viewer',
19-
]);
20-
export type ProjectRole = z.infer<typeof projectRoleSchema>;
21-
2215
export const projectRelationSchema = z.object({
2316
userId: z.string(),
2417
role: projectRoleSchema,

packages/@n8n/api-types/tsconfig.build.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"tsBuildInfoFile": "dist/build.tsbuildinfo"
88
},
99
"include": ["src/**/*.ts"],
10-
"exclude": ["test/**", "src/**/__tests__/**"]
10+
"exclude": ["src/**/__tests__/**"]
1111
}

packages/@n8n/api-types/tsconfig.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
"baseUrl": "src",
77
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
88
},
9-
"include": ["src/**/*.ts", "test/**/*.ts"]
9+
"include": ["src/**/*.ts"],
10+
"references": [
11+
{ "path": "../../workflow/tsconfig.build.json" },
12+
{ "path": "../config/tsconfig.build.json" },
13+
{ "path": "../permissions/tsconfig.build.json" }
14+
]
1015
}

packages/@n8n/db/src/entities/project-relation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ProjectRole } from '@n8n/permissions';
12
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
23

34
import { WithTimestamps } from './abstract-entity';
@@ -7,7 +8,7 @@ import { User } from './user';
78
@Entity()
89
export class ProjectRelation extends WithTimestamps {
910
@Column({ type: 'varchar' })
10-
role: 'project:personalOwner' | 'project:admin' | 'project:editor' | 'project:viewer';
11+
role: ProjectRole;
1112

1213
@ManyToOne('User', 'projectRelations')
1314
user: User;

packages/@n8n/db/src/entities/shared-credentials.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { CredentialSharingRole } from '@n8n/permissions';
12
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
23

34
import { WithTimestamps } from './abstract-entity';
45
import { CredentialsEntity } from './credentials-entity';
56
import { Project } from './project';
6-
import { CredentialSharingRole } from './types-db';
77

88
@Entity()
99
export class SharedCredentials extends WithTimestamps {
10-
@Column()
10+
@Column({ type: 'varchar' })
1111
role: CredentialSharingRole;
1212

1313
@ManyToOne('CredentialsEntity', 'shared')

packages/@n8n/db/src/entities/shared-workflow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { WorkflowSharingRole } from '@n8n/permissions';
12
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
23

34
import { WithTimestamps } from './abstract-entity';
45
import { Project } from './project';
5-
import { WorkflowSharingRole } from './types-db';
66
import { WorkflowEntity } from './workflow-entity';
77

88
@Entity()
99
export class SharedWorkflow extends WithTimestamps {
10-
@Column()
10+
@Column({ type: 'varchar' })
1111
role: WorkflowSharingRole;
1212

1313
@ManyToOne('WorkflowEntity', 'shared')

packages/@n8n/db/src/entities/types-db.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,6 @@ export const enum StatisticsNames {
269269
dataLoaded = 'data_loaded',
270270
}
271271

272-
export type CredentialSharingRole = 'credential:owner' | 'credential:user';
273-
274-
export type WorkflowSharingRole = 'workflow:owner' | 'workflow:editor';
275-
276272
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
277273

278274
export type FolderWithWorkflowAndSubFolderCount = Folder & {

packages/@n8n/db/src/entities/user.ts

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { hasScope, type ScopeOptions, type Scope, GlobalRole } from '@n8n/permissions';
2-
import { GLOBAL_OWNER_SCOPES, GLOBAL_MEMBER_SCOPES, GLOBAL_ADMIN_SCOPES } from '@n8n/permissions';
1+
import type { AuthPrincipal } from '@n8n/permissions';
2+
import { GlobalRole } from '@n8n/permissions';
33
import {
44
AfterLoad,
55
AfterUpdate,
@@ -25,14 +25,8 @@ import { lowerCaser, objectRetriever } from '../utils/transformers';
2525
import { NoUrl } from '../utils/validators/no-url.validator';
2626
import { NoXss } from '../utils/validators/no-xss.validator';
2727

28-
const STATIC_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
29-
'global:owner': GLOBAL_OWNER_SCOPES,
30-
'global:member': GLOBAL_MEMBER_SCOPES,
31-
'global:admin': GLOBAL_ADMIN_SCOPES,
32-
};
33-
3428
@Entity()
35-
export class User extends WithTimestamps implements IUser {
29+
export class User extends WithTimestamps implements IUser, AuthPrincipal {
3630
@PrimaryGeneratedColumn('uuid')
3731
id: string;
3832

@@ -113,31 +107,6 @@ export class User extends WithTimestamps implements IUser {
113107
this.isPending = this.password === null && this.role !== 'global:owner';
114108
}
115109

116-
/**
117-
* Whether the user is instance owner
118-
*/
119-
isOwner: boolean;
120-
121-
@AfterLoad()
122-
computeIsOwner(): void {
123-
this.isOwner = this.role === 'global:owner';
124-
}
125-
126-
get globalScopes() {
127-
return STATIC_SCOPE_MAP[this.role] ?? [];
128-
}
129-
130-
hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): boolean {
131-
return hasScope(
132-
scope,
133-
{
134-
global: this.globalScopes,
135-
},
136-
undefined,
137-
scopeOptions,
138-
);
139-
}
140-
141110
toJSON() {
142111
const { password, ...rest } = this;
143112
return rest;

packages/@n8n/db/tsconfig.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,12 @@
1111
// remove all options below this line
1212
"strictPropertyInitialization": false
1313
},
14-
"include": ["src/**/*.ts"]
14+
"include": ["src/**/*.ts"],
15+
"references": [
16+
{ "path": "../../core/tsconfig.build.json" },
17+
{ "path": "../../workflow/tsconfig.build.json" },
18+
{ "path": "../config/tsconfig.build.json" },
19+
{ "path": "../di/tsconfig.build.json" },
20+
{ "path": "../permissions/tsconfig.build.json" }
21+
]
1522
}

packages/@n8n/decorators/tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,11 @@
88
"experimentalDecorators": true,
99
"emitDecoratorMetadata": true
1010
},
11-
"include": ["src/**/*.ts"]
11+
"include": ["src/**/*.ts"],
12+
"references": [
13+
{ "path": "../../workflow/tsconfig.build.json" },
14+
{ "path": "../constants/tsconfig.build.json" },
15+
{ "path": "../di/tsconfig.build.json" },
16+
{ "path": "../permissions/tsconfig.build.json" }
17+
]
1218
}

packages/@n8n/permissions/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"files": [
2121
"dist/**/*"
2222
],
23+
"dependencies": {
24+
"zod": "catalog:"
25+
},
2326
"devDependencies": {
2427
"@n8n/typescript-config": "workspace:*"
2528
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
roleNamespaceSchema,
3+
globalRoleSchema,
4+
assignableGlobalRoleSchema,
5+
projectRoleSchema,
6+
credentialSharingRoleSchema,
7+
workflowSharingRoleSchema,
8+
} from '../schemas.ee';
9+
10+
describe('roleNamespaceSchema', () => {
11+
test.each([
12+
{ name: 'valid namespace: global', value: 'global', expected: true },
13+
{ name: 'valid namespace: project', value: 'project', expected: true },
14+
{ name: 'valid namespace: credential', value: 'credential', expected: true },
15+
{ name: 'valid namespace: workflow', value: 'workflow', expected: true },
16+
{ name: 'invalid namespace', value: 'invalid-namespace', expected: false },
17+
{ name: 'numeric value', value: 123, expected: false },
18+
{ name: 'null value', value: null, expected: false },
19+
])('should validate $name', ({ value, expected }) => {
20+
const result = roleNamespaceSchema.safeParse(value);
21+
expect(result.success).toBe(expected);
22+
});
23+
});
24+
25+
describe('globalRoleSchema', () => {
26+
test.each([
27+
{ name: 'valid role: global:owner', value: 'global:owner', expected: true },
28+
{ name: 'valid role: global:admin', value: 'global:admin', expected: true },
29+
{ name: 'valid role: global:member', value: 'global:member', expected: true },
30+
{ name: 'invalid role', value: 'global:invalid', expected: false },
31+
{ name: 'invalid prefix', value: 'invalid:admin', expected: false },
32+
{ name: 'empty string', value: '', expected: false },
33+
{ name: 'undefined value', value: undefined, expected: false },
34+
])('should validate $name', ({ value, expected }) => {
35+
const result = globalRoleSchema.safeParse(value);
36+
expect(result.success).toBe(expected);
37+
});
38+
});
39+
40+
describe('assignableGlobalRoleSchema', () => {
41+
test.each([
42+
{ name: 'excluded role: global:owner', value: 'global:owner', expected: false },
43+
{ name: 'valid role: global:admin', value: 'global:admin', expected: true },
44+
{ name: 'valid role: global:member', value: 'global:member', expected: true },
45+
{ name: 'invalid role', value: 'global:invalid', expected: false },
46+
{ name: 'invalid prefix', value: 'invalid:admin', expected: false },
47+
{ name: 'object value', value: {}, expected: false },
48+
])('should validate $name', ({ value, expected }) => {
49+
const result = assignableGlobalRoleSchema.safeParse(value);
50+
expect(result.success).toBe(expected);
51+
});
52+
});
53+
54+
describe('projectRoleSchema', () => {
55+
test.each([
56+
{ name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true },
57+
{ name: 'valid role: project:admin', value: 'project:admin', expected: true },
58+
{ name: 'valid role: project:editor', value: 'project:editor', expected: true },
59+
{ name: 'valid role: project:viewer', value: 'project:viewer', expected: true },
60+
{ name: 'invalid role', value: 'invalid-role', expected: false },
61+
])('should validate $name', ({ value, expected }) => {
62+
const result = projectRoleSchema.safeParse(value);
63+
expect(result.success).toBe(expected);
64+
});
65+
});
66+
67+
describe('credentialSharingRoleSchema', () => {
68+
test.each([
69+
{ name: 'valid role: credential:owner', value: 'credential:owner', expected: true },
70+
{ name: 'valid role: credential:user', value: 'credential:user', expected: true },
71+
{ name: 'invalid role', value: 'credential:admin', expected: false },
72+
{ name: 'invalid prefix', value: 'cred:owner', expected: false },
73+
{ name: 'boolean value', value: true, expected: false },
74+
{ name: 'array value', value: ['credential:owner'], expected: false },
75+
])('should validate $name', ({ value, expected }) => {
76+
const result = credentialSharingRoleSchema.safeParse(value);
77+
expect(result.success).toBe(expected);
78+
});
79+
});
80+
81+
describe('workflowSharingRoleSchema', () => {
82+
test.each([
83+
{ name: 'valid role: workflow:owner', value: 'workflow:owner', expected: true },
84+
{ name: 'valid role: workflow:editor', value: 'workflow:editor', expected: true },
85+
{ name: 'invalid role', value: 'workflow:viewer', expected: false },
86+
{ name: 'invalid prefix', value: 'work:owner', expected: false },
87+
{ name: 'undefined value', value: undefined, expected: false },
88+
{ name: 'empty string', value: '', expected: false },
89+
])('should validate $name', ({ value, expected }) => {
90+
const result = workflowSharingRoleSchema.safeParse(value);
91+
expect(result.success).toBe(expected);
92+
});
93+
});

0 commit comments

Comments
 (0)