mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00: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:
committed by
GitHub
parent
cdcd059248
commit
2bb190349b
@@ -18,7 +18,6 @@ export { passwordSchema } from './schemas/password.schema';
|
|||||||
export type {
|
export type {
|
||||||
ProjectType,
|
ProjectType,
|
||||||
ProjectIcon,
|
ProjectIcon,
|
||||||
ProjectRole,
|
|
||||||
ProjectRelation,
|
ProjectRelation,
|
||||||
} from './schemas/project.schema';
|
} from './schemas/project.schema';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
projectNameSchema,
|
projectNameSchema,
|
||||||
projectTypeSchema,
|
projectTypeSchema,
|
||||||
projectIconSchema,
|
projectIconSchema,
|
||||||
projectRoleSchema,
|
|
||||||
projectRelationSchema,
|
projectRelationSchema,
|
||||||
} from '../project.schema';
|
} from '../project.schema';
|
||||||
|
|
||||||
@@ -57,19 +56,6 @@ describe('project.schema', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('projectRoleSchema', () => {
|
|
||||||
test.each([
|
|
||||||
{ name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true },
|
|
||||||
{ name: 'valid role: project:admin', value: 'project:admin', expected: true },
|
|
||||||
{ name: 'valid role: project:editor', value: 'project:editor', expected: true },
|
|
||||||
{ name: 'valid role: project:viewer', value: 'project:viewer', expected: true },
|
|
||||||
{ name: 'invalid role', value: 'invalid-role', expected: false },
|
|
||||||
])('should validate $name', ({ value, expected }) => {
|
|
||||||
const result = projectRoleSchema.safeParse(value);
|
|
||||||
expect(result.success).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('projectRelationSchema', () => {
|
describe('projectRelationSchema', () => {
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { projectRoleSchema } from '@n8n/permissions';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const projectNameSchema = z.string().min(1).max(255);
|
export const projectNameSchema = z.string().min(1).max(255);
|
||||||
@@ -11,14 +12,6 @@ export const projectIconSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type ProjectIcon = z.infer<typeof projectIconSchema>;
|
export type ProjectIcon = z.infer<typeof projectIconSchema>;
|
||||||
|
|
||||||
export const projectRoleSchema = z.enum([
|
|
||||||
'project:personalOwner', // personalOwner is only used for personal projects
|
|
||||||
'project:admin',
|
|
||||||
'project:editor',
|
|
||||||
'project:viewer',
|
|
||||||
]);
|
|
||||||
export type ProjectRole = z.infer<typeof projectRoleSchema>;
|
|
||||||
|
|
||||||
export const projectRelationSchema = z.object({
|
export const projectRelationSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
role: projectRoleSchema,
|
role: projectRoleSchema,
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["test/**", "src/**/__tests__/**"]
|
"exclude": ["src/**/__tests__/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,10 @@
|
|||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../workflow/tsconfig.build.json" },
|
||||||
|
{ "path": "../config/tsconfig.build.json" },
|
||||||
|
{ "path": "../permissions/tsconfig.build.json" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ProjectRole } from '@n8n/permissions';
|
||||||
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { WithTimestamps } from './abstract-entity';
|
import { WithTimestamps } from './abstract-entity';
|
||||||
@@ -7,7 +8,7 @@ import { User } from './user';
|
|||||||
@Entity()
|
@Entity()
|
||||||
export class ProjectRelation extends WithTimestamps {
|
export class ProjectRelation extends WithTimestamps {
|
||||||
@Column({ type: 'varchar' })
|
@Column({ type: 'varchar' })
|
||||||
role: 'project:personalOwner' | 'project:admin' | 'project:editor' | 'project:viewer';
|
role: ProjectRole;
|
||||||
|
|
||||||
@ManyToOne('User', 'projectRelations')
|
@ManyToOne('User', 'projectRelations')
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { CredentialSharingRole } from '@n8n/permissions';
|
||||||
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { WithTimestamps } from './abstract-entity';
|
import { WithTimestamps } from './abstract-entity';
|
||||||
import { CredentialsEntity } from './credentials-entity';
|
import { CredentialsEntity } from './credentials-entity';
|
||||||
import { Project } from './project';
|
import { Project } from './project';
|
||||||
import { CredentialSharingRole } from './types-db';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class SharedCredentials extends WithTimestamps {
|
export class SharedCredentials extends WithTimestamps {
|
||||||
@Column()
|
@Column({ type: 'varchar' })
|
||||||
role: CredentialSharingRole;
|
role: CredentialSharingRole;
|
||||||
|
|
||||||
@ManyToOne('CredentialsEntity', 'shared')
|
@ManyToOne('CredentialsEntity', 'shared')
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { WorkflowSharingRole } from '@n8n/permissions';
|
||||||
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { WithTimestamps } from './abstract-entity';
|
import { WithTimestamps } from './abstract-entity';
|
||||||
import { Project } from './project';
|
import { Project } from './project';
|
||||||
import { WorkflowSharingRole } from './types-db';
|
|
||||||
import { WorkflowEntity } from './workflow-entity';
|
import { WorkflowEntity } from './workflow-entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class SharedWorkflow extends WithTimestamps {
|
export class SharedWorkflow extends WithTimestamps {
|
||||||
@Column()
|
@Column({ type: 'varchar' })
|
||||||
role: WorkflowSharingRole;
|
role: WorkflowSharingRole;
|
||||||
|
|
||||||
@ManyToOne('WorkflowEntity', 'shared')
|
@ManyToOne('WorkflowEntity', 'shared')
|
||||||
|
|||||||
@@ -269,10 +269,6 @@ export const enum StatisticsNames {
|
|||||||
dataLoaded = 'data_loaded',
|
dataLoaded = 'data_loaded',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CredentialSharingRole = 'credential:owner' | 'credential:user';
|
|
||||||
|
|
||||||
export type WorkflowSharingRole = 'workflow:owner' | 'workflow:editor';
|
|
||||||
|
|
||||||
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
|
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
|
||||||
|
|
||||||
export type FolderWithWorkflowAndSubFolderCount = Folder & {
|
export type FolderWithWorkflowAndSubFolderCount = Folder & {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { hasScope, type ScopeOptions, type Scope, GlobalRole } from '@n8n/permissions';
|
import type { AuthPrincipal } from '@n8n/permissions';
|
||||||
import { GLOBAL_OWNER_SCOPES, GLOBAL_MEMBER_SCOPES, GLOBAL_ADMIN_SCOPES } from '@n8n/permissions';
|
import { GlobalRole } from '@n8n/permissions';
|
||||||
import {
|
import {
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
AfterUpdate,
|
AfterUpdate,
|
||||||
@@ -25,14 +25,8 @@ import { lowerCaser, objectRetriever } from '../utils/transformers';
|
|||||||
import { NoUrl } from '../utils/validators/no-url.validator';
|
import { NoUrl } from '../utils/validators/no-url.validator';
|
||||||
import { NoXss } from '../utils/validators/no-xss.validator';
|
import { NoXss } from '../utils/validators/no-xss.validator';
|
||||||
|
|
||||||
const STATIC_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
|
|
||||||
'global:owner': GLOBAL_OWNER_SCOPES,
|
|
||||||
'global:member': GLOBAL_MEMBER_SCOPES,
|
|
||||||
'global:admin': GLOBAL_ADMIN_SCOPES,
|
|
||||||
};
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User extends WithTimestamps implements IUser {
|
export class User extends WithTimestamps implements IUser, AuthPrincipal {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@@ -113,31 +107,6 @@ export class User extends WithTimestamps implements IUser {
|
|||||||
this.isPending = this.password === null && this.role !== 'global:owner';
|
this.isPending = this.password === null && this.role !== 'global:owner';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the user is instance owner
|
|
||||||
*/
|
|
||||||
isOwner: boolean;
|
|
||||||
|
|
||||||
@AfterLoad()
|
|
||||||
computeIsOwner(): void {
|
|
||||||
this.isOwner = this.role === 'global:owner';
|
|
||||||
}
|
|
||||||
|
|
||||||
get globalScopes() {
|
|
||||||
return STATIC_SCOPE_MAP[this.role] ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): boolean {
|
|
||||||
return hasScope(
|
|
||||||
scope,
|
|
||||||
{
|
|
||||||
global: this.globalScopes,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
scopeOptions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
const { password, ...rest } = this;
|
const { password, ...rest } = this;
|
||||||
return rest;
|
return rest;
|
||||||
|
|||||||
@@ -11,5 +11,12 @@
|
|||||||
// remove all options below this line
|
// remove all options below this line
|
||||||
"strictPropertyInitialization": false
|
"strictPropertyInitialization": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../core/tsconfig.build.json" },
|
||||||
|
{ "path": "../../workflow/tsconfig.build.json" },
|
||||||
|
{ "path": "../config/tsconfig.build.json" },
|
||||||
|
{ "path": "../di/tsconfig.build.json" },
|
||||||
|
{ "path": "../permissions/tsconfig.build.json" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,11 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true
|
"emitDecoratorMetadata": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../workflow/tsconfig.build.json" },
|
||||||
|
{ "path": "../constants/tsconfig.build.json" },
|
||||||
|
{ "path": "../di/tsconfig.build.json" },
|
||||||
|
{ "path": "../permissions/tsconfig.build.json" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist/**/*"
|
"dist/**/*"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "catalog:"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@n8n/typescript-config": "workspace:*"
|
"@n8n/typescript-config": "workspace:*"
|
||||||
}
|
}
|
||||||
|
|||||||
93
packages/@n8n/permissions/src/__tests__/schemas.test.ts
Normal file
93
packages/@n8n/permissions/src/__tests__/schemas.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
roleNamespaceSchema,
|
||||||
|
globalRoleSchema,
|
||||||
|
assignableGlobalRoleSchema,
|
||||||
|
projectRoleSchema,
|
||||||
|
credentialSharingRoleSchema,
|
||||||
|
workflowSharingRoleSchema,
|
||||||
|
} from '../schemas.ee';
|
||||||
|
|
||||||
|
describe('roleNamespaceSchema', () => {
|
||||||
|
test.each([
|
||||||
|
{ name: 'valid namespace: global', value: 'global', expected: true },
|
||||||
|
{ name: 'valid namespace: project', value: 'project', expected: true },
|
||||||
|
{ name: 'valid namespace: credential', value: 'credential', expected: true },
|
||||||
|
{ name: 'valid namespace: workflow', value: 'workflow', expected: true },
|
||||||
|
{ name: 'invalid namespace', value: 'invalid-namespace', expected: false },
|
||||||
|
{ name: 'numeric value', value: 123, expected: false },
|
||||||
|
{ name: 'null value', value: null, expected: false },
|
||||||
|
])('should validate $name', ({ value, expected }) => {
|
||||||
|
const result = roleNamespaceSchema.safeParse(value);
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('globalRoleSchema', () => {
|
||||||
|
test.each([
|
||||||
|
{ name: 'valid role: global:owner', value: 'global:owner', expected: true },
|
||||||
|
{ name: 'valid role: global:admin', value: 'global:admin', expected: true },
|
||||||
|
{ name: 'valid role: global:member', value: 'global:member', expected: true },
|
||||||
|
{ name: 'invalid role', value: 'global:invalid', expected: false },
|
||||||
|
{ name: 'invalid prefix', value: 'invalid:admin', expected: false },
|
||||||
|
{ name: 'empty string', value: '', expected: false },
|
||||||
|
{ name: 'undefined value', value: undefined, expected: false },
|
||||||
|
])('should validate $name', ({ value, expected }) => {
|
||||||
|
const result = globalRoleSchema.safeParse(value);
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assignableGlobalRoleSchema', () => {
|
||||||
|
test.each([
|
||||||
|
{ name: 'excluded role: global:owner', value: 'global:owner', expected: false },
|
||||||
|
{ name: 'valid role: global:admin', value: 'global:admin', expected: true },
|
||||||
|
{ name: 'valid role: global:member', value: 'global:member', expected: true },
|
||||||
|
{ name: 'invalid role', value: 'global:invalid', expected: false },
|
||||||
|
{ name: 'invalid prefix', value: 'invalid:admin', expected: false },
|
||||||
|
{ name: 'object value', value: {}, expected: false },
|
||||||
|
])('should validate $name', ({ value, expected }) => {
|
||||||
|
const result = assignableGlobalRoleSchema.safeParse(value);
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('projectRoleSchema', () => {
|
||||||
|
test.each([
|
||||||
|
{ name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true },
|
||||||
|
{ name: 'valid role: project:admin', value: 'project:admin', expected: true },
|
||||||
|
{ name: 'valid role: project:editor', value: 'project:editor', expected: true },
|
||||||
|
{ name: 'valid role: project:viewer', value: 'project:viewer', expected: true },
|
||||||
|
{ name: 'invalid role', value: 'invalid-role', expected: false },
|
||||||
|
])('should validate $name', ({ value, expected }) => {
|
||||||
|
const result = projectRoleSchema.safeParse(value);
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('credentialSharingRoleSchema', () => {
|
||||||
|
test.each([
|
||||||
|
{ name: 'valid role: credential:owner', value: 'credential:owner', expected: true },
|
||||||
|
{ name: 'valid role: credential:user', value: 'credential:user', expected: true },
|
||||||
|
{ name: 'invalid role', value: 'credential:admin', expected: false },
|
||||||
|
{ name: 'invalid prefix', value: 'cred:owner', expected: false },
|
||||||
|
{ name: 'boolean value', value: true, expected: false },
|
||||||
|
{ name: 'array value', value: ['credential:owner'], expected: false },
|
||||||
|
])('should validate $name', ({ value, expected }) => {
|
||||||
|
const result = credentialSharingRoleSchema.safeParse(value);
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('workflowSharingRoleSchema', () => {
|
||||||
|
test.each([
|
||||||
|
{ name: 'valid role: workflow:owner', value: 'workflow:owner', expected: true },
|
||||||
|
{ name: 'valid role: workflow:editor', value: 'workflow:editor', expected: true },
|
||||||
|
{ name: 'invalid role', value: 'workflow:viewer', expected: false },
|
||||||
|
{ name: 'invalid prefix', value: 'work:owner', expected: false },
|
||||||
|
{ name: 'undefined value', value: undefined, expected: false },
|
||||||
|
{ name: 'empty string', value: '', expected: false },
|
||||||
|
])('should validate $name', ({ value, expected }) => {
|
||||||
|
const result = workflowSharingRoleSchema.safeParse(value);
|
||||||
|
expect(result.success).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
115
packages/@n8n/permissions/src/__tests__/types.test.ts
Normal file
115
packages/@n8n/permissions/src/__tests__/types.test.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import type { ApiKeyScope, Scope } from '@/types.ee';
|
||||||
|
|
||||||
|
// These are a type-level tests,
|
||||||
|
// that will be catch issues in the `typecheck` step instead of in an actual test run
|
||||||
|
describe('ApiKeyScope', () => {
|
||||||
|
test('Valid scopes', () => {
|
||||||
|
const validScopes: ApiKeyScope[] = [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:move',
|
||||||
|
'execution:delete',
|
||||||
|
'execution:get',
|
||||||
|
'execution:list',
|
||||||
|
'execution:read',
|
||||||
|
'project:create',
|
||||||
|
'project:delete',
|
||||||
|
'project:list',
|
||||||
|
'project:update',
|
||||||
|
'securityAudit:generate',
|
||||||
|
'sourceControl:pull',
|
||||||
|
'tag:create',
|
||||||
|
'tag:delete',
|
||||||
|
'tag:list',
|
||||||
|
'tag:read',
|
||||||
|
'tag:update',
|
||||||
|
'user:changeRole',
|
||||||
|
'user:create',
|
||||||
|
'user:delete',
|
||||||
|
'user:list',
|
||||||
|
'user:read',
|
||||||
|
'variable:create',
|
||||||
|
'variable:delete',
|
||||||
|
'variable:list',
|
||||||
|
'workflow:activate',
|
||||||
|
'workflow:create',
|
||||||
|
'workflow:deactivate',
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:list',
|
||||||
|
'workflow:move',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:update',
|
||||||
|
'workflowTags:list',
|
||||||
|
'workflowTags:update',
|
||||||
|
];
|
||||||
|
// Useless assertion to avoid disabling noUnusedLocals
|
||||||
|
expect(validScopes).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid scopes', () => {
|
||||||
|
const invalidScopes: ApiKeyScope[] = [
|
||||||
|
// @ts-expect-error - Operations does not exist for workflows
|
||||||
|
'workflows:invalid',
|
||||||
|
// @ts-expect-error - Operations does not exist for credentials
|
||||||
|
'credentials:invalid',
|
||||||
|
// @ts-expect-error - Cross-resource mismatches
|
||||||
|
'workflow:pull',
|
||||||
|
];
|
||||||
|
// Useless assertion to avoid disabling noUnusedLocals
|
||||||
|
expect(invalidScopes).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// These are a type-level tests,
|
||||||
|
// that will be catch issues in the `typecheck` step instead of in an actual test run
|
||||||
|
describe('Scope', () => {
|
||||||
|
test('Valid scopes', () => {
|
||||||
|
// non-exhaustive list
|
||||||
|
const validScopes: Scope[] = [
|
||||||
|
'credential:create',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:move',
|
||||||
|
'ldap:sync',
|
||||||
|
'project:create',
|
||||||
|
'project:delete',
|
||||||
|
'project:list',
|
||||||
|
'project:update',
|
||||||
|
'securityAudit:generate',
|
||||||
|
'sourceControl:pull',
|
||||||
|
'tag:create',
|
||||||
|
'tag:delete',
|
||||||
|
'tag:list',
|
||||||
|
'tag:read',
|
||||||
|
'tag:update',
|
||||||
|
'user:changeRole',
|
||||||
|
'user:create',
|
||||||
|
'user:delete',
|
||||||
|
'user:list',
|
||||||
|
'user:read',
|
||||||
|
'variable:create',
|
||||||
|
'variable:delete',
|
||||||
|
'variable:list',
|
||||||
|
'workflow:create',
|
||||||
|
'workflow:delete',
|
||||||
|
'workflow:list',
|
||||||
|
'workflow:move',
|
||||||
|
'workflow:read',
|
||||||
|
'workflow:update',
|
||||||
|
];
|
||||||
|
// Useless assertion to avoid disabling noUnusedLocals
|
||||||
|
expect(validScopes).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid scopes', () => {
|
||||||
|
const invalidScopes: Scope[] = [
|
||||||
|
// @ts-expect-error - Operations does not exist for workflows
|
||||||
|
'workflows:invalid',
|
||||||
|
// @ts-expect-error - Operations does not exist for credentials
|
||||||
|
'credentials:invalid',
|
||||||
|
// @ts-expect-error - Cross-resource mismatches
|
||||||
|
'workflow:resetPassword',
|
||||||
|
];
|
||||||
|
// Useless assertion to avoid disabling noUnusedLocals
|
||||||
|
expect(invalidScopes).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import type { Scope, ScopeLevels, GlobalScopes, MaskLevels } from './types.ee';
|
|
||||||
|
|
||||||
export function combineScopes(userScopes: GlobalScopes, masks?: MaskLevels): Set<Scope>;
|
|
||||||
export function combineScopes(userScopes: ScopeLevels, masks?: MaskLevels): Set<Scope>;
|
|
||||||
export function combineScopes(
|
|
||||||
userScopes: GlobalScopes | ScopeLevels,
|
|
||||||
masks?: MaskLevels,
|
|
||||||
): Set<Scope> {
|
|
||||||
const maskedScopes: GlobalScopes | ScopeLevels = Object.fromEntries(
|
|
||||||
Object.entries(userScopes).map((e) => [e[0], [...e[1]]]),
|
|
||||||
) as GlobalScopes | ScopeLevels;
|
|
||||||
|
|
||||||
if (masks?.sharing) {
|
|
||||||
if ('project' in maskedScopes) {
|
|
||||||
maskedScopes.project = maskedScopes.project.filter((v) => masks.sharing.includes(v));
|
|
||||||
}
|
|
||||||
if ('resource' in maskedScopes) {
|
|
||||||
maskedScopes.resource = maskedScopes.resource.filter((v) => masks.sharing.includes(v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Set(Object.values(maskedScopes).flat());
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export const DEFAULT_OPERATIONS = ['create', 'read', 'update', 'delete', 'list'] as const;
|
export const DEFAULT_OPERATIONS = ['create', 'read', 'update', 'delete', 'list'] as const;
|
||||||
|
|
||||||
export const RESOURCES = {
|
export const RESOURCES = {
|
||||||
annotationTag: [...DEFAULT_OPERATIONS] as const,
|
annotationTag: [...DEFAULT_OPERATIONS] as const,
|
||||||
auditLogs: ['manage'] as const,
|
auditLogs: ['manage'] as const,
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { combineScopes } from './combineScopes.ee';
|
|
||||||
import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions, MaskLevels } from './types.ee';
|
|
||||||
|
|
||||||
export function hasScope(
|
|
||||||
scope: Scope | Scope[],
|
|
||||||
userScopes: GlobalScopes,
|
|
||||||
masks?: MaskLevels,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): boolean;
|
|
||||||
export function hasScope(
|
|
||||||
scope: Scope | Scope[],
|
|
||||||
userScopes: ScopeLevels,
|
|
||||||
masks?: MaskLevels,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): boolean;
|
|
||||||
export function hasScope(
|
|
||||||
scope: Scope | Scope[],
|
|
||||||
userScopes: GlobalScopes | ScopeLevels,
|
|
||||||
masks?: MaskLevels,
|
|
||||||
options: ScopeOptions = { mode: 'oneOf' },
|
|
||||||
): boolean {
|
|
||||||
if (!Array.isArray(scope)) {
|
|
||||||
scope = [scope];
|
|
||||||
}
|
|
||||||
|
|
||||||
const userScopeSet = combineScopes(userScopes, masks);
|
|
||||||
|
|
||||||
if (options.mode === 'allOf') {
|
|
||||||
return !!scope.length && scope.every((s) => userScopeSet.has(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
return scope.some((s) => userScopeSet.has(s));
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
export type * from './types.ee';
|
export type * from './types.ee';
|
||||||
export * from './constants.ee';
|
export * from './constants.ee';
|
||||||
export * from './hasScope.ee';
|
|
||||||
export * from './combineScopes.ee';
|
export * from './roles/scopes/global-scopes.ee';
|
||||||
export * from './global-roles.ee';
|
export * from './roles/role-maps.ee';
|
||||||
export * from './project-roles.ee';
|
export * from './roles/all-roles';
|
||||||
export * from './resource-roles.ee';
|
|
||||||
|
export { projectRoleSchema } from './schemas.ee';
|
||||||
|
|
||||||
|
export { hasScope } from './utilities/hasScope.ee';
|
||||||
|
export { hasGlobalScope } from './utilities/hasGlobalScope.ee';
|
||||||
|
export { combineScopes } from './utilities/combineScopes.ee';
|
||||||
|
export { rolesWithScope } from './utilities/rolesWithScope.ee';
|
||||||
|
export { getGlobalScopes } from './utilities/getGlobalScopes.ee';
|
||||||
|
export { getRoleScopes } from './utilities/getRoleScopes.ee';
|
||||||
|
|||||||
37
packages/@n8n/permissions/src/roles/all-roles.ts
Normal file
37
packages/@n8n/permissions/src/roles/all-roles.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
CREDENTIALS_SHARING_SCOPE_MAP,
|
||||||
|
GLOBAL_SCOPE_MAP,
|
||||||
|
PROJECT_SCOPE_MAP,
|
||||||
|
WORKFLOW_SHARING_SCOPE_MAP,
|
||||||
|
} from './role-maps.ee';
|
||||||
|
import type { AllRolesMap, AllRoleTypes, Scope } from '../types.ee';
|
||||||
|
import { getRoleScopes } from '../utilities/getRoleScopes.ee';
|
||||||
|
|
||||||
|
const ROLE_NAMES: Record<AllRoleTypes, 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',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapToRoleObject = <T extends keyof typeof ROLE_NAMES>(roles: Record<T, Scope[]>) =>
|
||||||
|
(Object.keys(roles) as T[]).map((role) => ({
|
||||||
|
role,
|
||||||
|
name: ROLE_NAMES[role],
|
||||||
|
scopes: getRoleScopes(role),
|
||||||
|
licensed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ALL_ROLES: AllRolesMap = {
|
||||||
|
global: mapToRoleObject(GLOBAL_SCOPE_MAP),
|
||||||
|
project: mapToRoleObject(PROJECT_SCOPE_MAP),
|
||||||
|
credential: mapToRoleObject(CREDENTIALS_SHARING_SCOPE_MAP),
|
||||||
|
workflow: mapToRoleObject(WORKFLOW_SHARING_SCOPE_MAP),
|
||||||
|
};
|
||||||
56
packages/@n8n/permissions/src/roles/role-maps.ee.ts
Normal file
56
packages/@n8n/permissions/src/roles/role-maps.ee.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
CREDENTIALS_SHARING_OWNER_SCOPES,
|
||||||
|
CREDENTIALS_SHARING_USER_SCOPES,
|
||||||
|
} from './scopes/credential-sharing-scopes.ee';
|
||||||
|
import {
|
||||||
|
GLOBAL_OWNER_SCOPES,
|
||||||
|
GLOBAL_ADMIN_SCOPES,
|
||||||
|
GLOBAL_MEMBER_SCOPES,
|
||||||
|
} from './scopes/global-scopes.ee';
|
||||||
|
import {
|
||||||
|
REGULAR_PROJECT_ADMIN_SCOPES,
|
||||||
|
PERSONAL_PROJECT_OWNER_SCOPES,
|
||||||
|
PROJECT_EDITOR_SCOPES,
|
||||||
|
PROJECT_VIEWER_SCOPES,
|
||||||
|
} from './scopes/project-scopes.ee';
|
||||||
|
import {
|
||||||
|
WORKFLOW_SHARING_OWNER_SCOPES,
|
||||||
|
WORKFLOW_SHARING_EDITOR_SCOPES,
|
||||||
|
} from './scopes/workflow-sharing-scopes.ee';
|
||||||
|
import type {
|
||||||
|
CredentialSharingRole,
|
||||||
|
GlobalRole,
|
||||||
|
ProjectRole,
|
||||||
|
Scope,
|
||||||
|
WorkflowSharingRole,
|
||||||
|
} from '../types.ee';
|
||||||
|
|
||||||
|
export const GLOBAL_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
|
||||||
|
'global:owner': GLOBAL_OWNER_SCOPES,
|
||||||
|
'global:admin': GLOBAL_ADMIN_SCOPES,
|
||||||
|
'global:member': GLOBAL_MEMBER_SCOPES,
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CREDENTIALS_SHARING_SCOPE_MAP: Record<CredentialSharingRole, Scope[]> = {
|
||||||
|
'credential:owner': CREDENTIALS_SHARING_OWNER_SCOPES,
|
||||||
|
'credential:user': CREDENTIALS_SHARING_USER_SCOPES,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WORKFLOW_SHARING_SCOPE_MAP: Record<WorkflowSharingRole, Scope[]> = {
|
||||||
|
'workflow:owner': WORKFLOW_SHARING_OWNER_SCOPES,
|
||||||
|
'workflow:editor': WORKFLOW_SHARING_EDITOR_SCOPES,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ALL_ROLE_MAPS = {
|
||||||
|
global: GLOBAL_SCOPE_MAP,
|
||||||
|
project: PROJECT_SCOPE_MAP,
|
||||||
|
credential: CREDENTIALS_SHARING_SCOPE_MAP,
|
||||||
|
workflow: WORKFLOW_SHARING_SCOPE_MAP,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Scope } from '../../types.ee';
|
||||||
|
|
||||||
|
export const CREDENTIALS_SHARING_OWNER_SCOPES: Scope[] = [
|
||||||
|
'credential:read',
|
||||||
|
'credential:update',
|
||||||
|
'credential:delete',
|
||||||
|
'credential:share',
|
||||||
|
'credential:move',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CREDENTIALS_SHARING_USER_SCOPES: Scope[] = ['credential:read'];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Scope } from './types.ee';
|
import type { Scope } from '../../types.ee';
|
||||||
|
|
||||||
export const GLOBAL_OWNER_SCOPES: Scope[] = [
|
export const GLOBAL_OWNER_SCOPES: Scope[] = [
|
||||||
'annotationTag:create',
|
'annotationTag:create',
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Scope } from './types.ee';
|
import type { Scope } from '../../types.ee';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Diff between admin in personal project and admin in other projects:
|
* Diff between admin in personal project and admin in other projects:
|
||||||
@@ -1,14 +1,4 @@
|
|||||||
import type { Scope } from './types.ee';
|
import type { Scope } from '../../types.ee';
|
||||||
|
|
||||||
export const CREDENTIALS_SHARING_OWNER_SCOPES: Scope[] = [
|
|
||||||
'credential:read',
|
|
||||||
'credential:update',
|
|
||||||
'credential:delete',
|
|
||||||
'credential:share',
|
|
||||||
'credential:move',
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CREDENTIALS_SHARING_USER_SCOPES: Scope[] = ['credential:read'];
|
|
||||||
|
|
||||||
export const WORKFLOW_SHARING_OWNER_SCOPES: Scope[] = [
|
export const WORKFLOW_SHARING_OWNER_SCOPES: Scope[] = [
|
||||||
'workflow:read',
|
'workflow:read',
|
||||||
20
packages/@n8n/permissions/src/schemas.ee.ts
Normal file
20
packages/@n8n/permissions/src/schemas.ee.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const roleNamespaceSchema = z.enum(['global', 'project', 'credential', 'workflow']);
|
||||||
|
|
||||||
|
export const globalRoleSchema = z.enum(['global:owner', 'global:admin', 'global:member']);
|
||||||
|
|
||||||
|
export const assignableGlobalRoleSchema = globalRoleSchema.exclude([
|
||||||
|
'global:owner', // Owner cannot be changed
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const projectRoleSchema = z.enum([
|
||||||
|
'project:personalOwner', // personalOwner is only used for personal projects
|
||||||
|
'project:admin',
|
||||||
|
'project:editor',
|
||||||
|
'project:viewer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const credentialSharingRoleSchema = z.enum(['credential:owner', 'credential:user']);
|
||||||
|
|
||||||
|
export const workflowSharingRoleSchema = z.enum(['workflow:owner', 'workflow:editor']);
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
import type { RESOURCES, API_KEY_RESOURCES } from './constants.ee';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import type { RESOURCES, API_KEY_RESOURCES } from './constants.ee';
|
||||||
|
import type {
|
||||||
|
assignableGlobalRoleSchema,
|
||||||
|
credentialSharingRoleSchema,
|
||||||
|
globalRoleSchema,
|
||||||
|
projectRoleSchema,
|
||||||
|
roleNamespaceSchema,
|
||||||
|
workflowSharingRoleSchema,
|
||||||
|
} from './schemas.ee';
|
||||||
|
|
||||||
|
/** Represents a resource that can have permissions applied to it */
|
||||||
export type Resource = keyof typeof RESOURCES;
|
export type Resource = keyof typeof RESOURCES;
|
||||||
|
|
||||||
export type ResourceScope<
|
/** A permission scope for a specific resource + operation combination */
|
||||||
|
type ResourceScope<
|
||||||
R extends Resource,
|
R extends Resource,
|
||||||
Operation extends (typeof RESOURCES)[R][number] = (typeof RESOURCES)[R][number],
|
Operation extends (typeof RESOURCES)[R][number] = (typeof RESOURCES)[R][number],
|
||||||
> = `${R}:${Operation}`;
|
> = `${R}:${Operation}`;
|
||||||
|
|
||||||
export type WildcardScope = `${Resource}:*` | '*';
|
/** A wildcard scope applies to all operations on a resource or all resources */
|
||||||
|
type WildcardScope = `${Resource}:*` | '*';
|
||||||
|
|
||||||
// This is purely an intermediary type.
|
// This is purely an intermediary type.
|
||||||
// If we tried to do use `ResourceScope<Resource>` directly we'd end
|
// If we tried to do use `ResourceScope<Resource>` directly we'd end
|
||||||
@@ -16,26 +29,57 @@ type AllScopesObject = {
|
|||||||
[R in Resource]: ResourceScope<R>;
|
[R in Resource]: ResourceScope<R>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Scope = AllScopesObject[Resource];
|
/** A permission scope in the system, either a specific resource:operation or a wildcard */
|
||||||
|
export type Scope = AllScopesObject[Resource] | WildcardScope;
|
||||||
|
|
||||||
export type ScopeLevel = 'global' | 'project' | 'resource';
|
export type ScopeLevels = {
|
||||||
export type GetScopeLevel<T extends ScopeLevel> = Record<T, Scope[]>;
|
global: Scope[];
|
||||||
export type GlobalScopes = GetScopeLevel<'global'>;
|
project?: Scope[];
|
||||||
export type ProjectScopes = GetScopeLevel<'project'>;
|
resource?: Scope[];
|
||||||
export type ResourceScopes = GetScopeLevel<'resource'>;
|
};
|
||||||
export type ScopeLevels = GlobalScopes & (ProjectScopes | (ProjectScopes & ResourceScopes));
|
|
||||||
|
|
||||||
export type MaskLevel = 'sharing';
|
export type MaskLevels = {
|
||||||
export type GetMaskLevel<T extends MaskLevel> = Record<T, Scope[]>;
|
sharing: Scope[];
|
||||||
export type SharingMasks = GetMaskLevel<'sharing'>;
|
};
|
||||||
export type MaskLevels = SharingMasks;
|
|
||||||
|
|
||||||
export type ScopeMode = 'oneOf' | 'allOf';
|
export type ScopeOptions = { mode: 'oneOf' | 'allOf' };
|
||||||
export type ScopeOptions = { mode: ScopeMode };
|
|
||||||
|
|
||||||
export type PublicApiKeyResources = keyof typeof API_KEY_RESOURCES;
|
export type RoleNamespace = z.infer<typeof roleNamespaceSchema>;
|
||||||
|
export type GlobalRole = z.infer<typeof globalRoleSchema>;
|
||||||
|
export type AssignableGlobalRole = z.infer<typeof assignableGlobalRoleSchema>;
|
||||||
|
export type CredentialSharingRole = z.infer<typeof credentialSharingRoleSchema>;
|
||||||
|
export type WorkflowSharingRole = z.infer<typeof workflowSharingRoleSchema>;
|
||||||
|
export type ProjectRole = z.infer<typeof projectRoleSchema>;
|
||||||
|
|
||||||
export type ApiKeyResourceScope<
|
/** Union of all possible role types in the system */
|
||||||
|
export type AllRoleTypes = GlobalRole | ProjectRole | WorkflowSharingRole | CredentialSharingRole;
|
||||||
|
|
||||||
|
type RoleObject<T extends AllRoleTypes> = {
|
||||||
|
role: T;
|
||||||
|
name: string;
|
||||||
|
scopes: Scope[];
|
||||||
|
licensed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AllRolesMap = {
|
||||||
|
global: Array<RoleObject<GlobalRole>>;
|
||||||
|
project: Array<RoleObject<ProjectRole>>;
|
||||||
|
credential: Array<RoleObject<CredentialSharingRole>>;
|
||||||
|
workflow: Array<RoleObject<WorkflowSharingRole>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an authenticated entity in the system that can have specific permissions via a role.
|
||||||
|
* @property role - The global role this principal has
|
||||||
|
*/
|
||||||
|
export type AuthPrincipal = {
|
||||||
|
role: GlobalRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
// #region Public API
|
||||||
|
type PublicApiKeyResources = keyof typeof API_KEY_RESOURCES;
|
||||||
|
|
||||||
|
type ApiKeyResourceScope<
|
||||||
R extends PublicApiKeyResources,
|
R extends PublicApiKeyResources,
|
||||||
Operation extends (typeof API_KEY_RESOURCES)[R][number] = (typeof API_KEY_RESOURCES)[R][number],
|
Operation extends (typeof API_KEY_RESOURCES)[R][number] = (typeof API_KEY_RESOURCES)[R][number],
|
||||||
> = `${R}:${Operation}`;
|
> = `${R}:${Operation}`;
|
||||||
@@ -49,5 +93,4 @@ type AllApiKeyScopesObject = {
|
|||||||
|
|
||||||
export type ApiKeyScope = AllApiKeyScopesObject[PublicApiKeyResources];
|
export type ApiKeyScope = AllApiKeyScopesObject[PublicApiKeyResources];
|
||||||
|
|
||||||
export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member';
|
// #endregion
|
||||||
export type AssignableRole = Exclude<GlobalRole, 'global:owner'>;
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Scope, ScopeLevels, MaskLevels } from '../../types.ee';
|
||||||
|
import { combineScopes } from '../combineScopes.ee';
|
||||||
|
|
||||||
|
describe('combineScopes', () => {
|
||||||
|
describe('basic scope combining', () => {
|
||||||
|
test.each([
|
||||||
|
['single level', { global: ['workflow:read'] }, 1],
|
||||||
|
[
|
||||||
|
'multiple levels',
|
||||||
|
{
|
||||||
|
global: ['user:list'],
|
||||||
|
project: ['workflow:read'],
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'duplicates',
|
||||||
|
{
|
||||||
|
global: ['workflow:read'],
|
||||||
|
project: ['workflow:read'],
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
] satisfies Array<[string, ScopeLevels, number]>)('%s', (_, input, expectedSize) => {
|
||||||
|
expect(combineScopes(input).size).toBe(expectedSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('masking behavior', () => {
|
||||||
|
test.each([
|
||||||
|
[
|
||||||
|
'filters project scopes',
|
||||||
|
{ project: ['workflow:read', 'workflow:update'], global: [] },
|
||||||
|
{ sharing: ['workflow:read'] },
|
||||||
|
['workflow:read'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'filters resource scopes',
|
||||||
|
{ resource: ['credential:read', 'credential:update'], global: [] },
|
||||||
|
{ sharing: ['credential:read'] },
|
||||||
|
['credential:read'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ignores global scopes',
|
||||||
|
{ global: ['user:list'], project: ['workflow:read'] },
|
||||||
|
{ sharing: [] },
|
||||||
|
['user:list'],
|
||||||
|
],
|
||||||
|
['handles undefined masks', { global: ['user:list'] }, undefined, ['user:list']],
|
||||||
|
[
|
||||||
|
'handles empty resource scopes',
|
||||||
|
{ resource: [], global: ['user:list'] },
|
||||||
|
{ sharing: ['credential:read'] },
|
||||||
|
['user:list'],
|
||||||
|
],
|
||||||
|
] satisfies Array<[string, ScopeLevels, MaskLevels | undefined, Scope[]]>)(
|
||||||
|
'%s',
|
||||||
|
(_, scopes, masks, expected) => {
|
||||||
|
const result = combineScopes(scopes, masks);
|
||||||
|
expect(result.size).toBe(expected.length);
|
||||||
|
expected.forEach((scope) => expect(result.has(scope)).toBe(true));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { GLOBAL_SCOPE_MAP } from '../../roles/role-maps.ee';
|
||||||
|
import type { GlobalRole } from '../../types.ee';
|
||||||
|
import { getGlobalScopes } from '../getGlobalScopes.ee';
|
||||||
|
|
||||||
|
describe('getGlobalScopes', () => {
|
||||||
|
test.each(['global:owner', 'global:admin', 'global:member'] as const)(
|
||||||
|
'should return correct scopes for %s',
|
||||||
|
(role) => {
|
||||||
|
const scopes = getGlobalScopes({ role });
|
||||||
|
|
||||||
|
expect(scopes).toEqual(GLOBAL_SCOPE_MAP[role]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('should return empty array for non-existent role', () => {
|
||||||
|
const scopes = getGlobalScopes({ role: 'non:existent' as GlobalRole });
|
||||||
|
|
||||||
|
expect(scopes).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { AllRoleTypes, Resource } from '../../types.ee';
|
||||||
|
import { getRoleScopes, COMBINED_ROLE_MAP } from '../getRoleScopes.ee';
|
||||||
|
|
||||||
|
describe('getRoleScopes', () => {
|
||||||
|
describe('role scope retrieval', () => {
|
||||||
|
test.each(['global:owner', 'global:admin', 'project:admin'] satisfies AllRoleTypes[])(
|
||||||
|
'should return scopes for %s',
|
||||||
|
(role) => {
|
||||||
|
const scopes = getRoleScopes(role);
|
||||||
|
expect(scopes).toEqual(COMBINED_ROLE_MAP[role]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resource filtering', () => {
|
||||||
|
test.each(['workflow', 'credential', 'user'] satisfies Resource[])(
|
||||||
|
'should filter %s scopes',
|
||||||
|
(resource) => {
|
||||||
|
const filtered = getRoleScopes('global:owner', [resource]);
|
||||||
|
expect(filtered.every((s) => s.startsWith(`${resource}:`))).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('should handle multiple filters', () => {
|
||||||
|
const filtered = getRoleScopes('global:owner', ['workflow', 'credential']);
|
||||||
|
expect(filtered.some((s) => s.startsWith('workflow:'))).toBe(true);
|
||||||
|
expect(filtered.some((s) => s.startsWith('credential:'))).toBe(true);
|
||||||
|
expect(filtered.every((s) => !s.startsWith('tag:'))).toBe(true);
|
||||||
|
expect(filtered.every((s) => !s.startsWith('user:'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty array for no matches', () => {
|
||||||
|
expect(getRoleScopes('global:member', ['nonexistent' as Resource])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { GlobalRole, Scope } from '../../types.ee';
|
||||||
|
import { hasGlobalScope } from '../hasGlobalScope.ee';
|
||||||
|
|
||||||
|
describe('hasGlobalScope', () => {
|
||||||
|
describe('single scope checks', () => {
|
||||||
|
test.each([
|
||||||
|
{ role: 'global:owner', scope: 'workflow:create', expected: true },
|
||||||
|
{ role: 'global:admin', scope: 'user:delete', expected: true },
|
||||||
|
{ role: 'global:member', scope: 'workflow:read', expected: false },
|
||||||
|
{ role: 'non:existent', scope: 'workflow:read', expected: false },
|
||||||
|
] as Array<{ role: GlobalRole; scope: Scope; expected: boolean }>)(
|
||||||
|
'$role with $scope -> $expected',
|
||||||
|
({ role, scope, expected }) => {
|
||||||
|
expect(hasGlobalScope({ role }, scope)).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple scopes', () => {
|
||||||
|
test('oneOf mode (default)', () => {
|
||||||
|
expect(
|
||||||
|
hasGlobalScope({ role: 'global:member' }, [
|
||||||
|
'tag:create',
|
||||||
|
'user:list',
|
||||||
|
// a member cannot create users
|
||||||
|
'user:create',
|
||||||
|
]),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allOf mode', () => {
|
||||||
|
expect(
|
||||||
|
hasGlobalScope(
|
||||||
|
{ role: 'global:member' },
|
||||||
|
[
|
||||||
|
'tag:create',
|
||||||
|
'user:list',
|
||||||
|
// a member cannot create users
|
||||||
|
'user:create',
|
||||||
|
],
|
||||||
|
{ mode: 'allOf' },
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge cases', () => {
|
||||||
|
expect(hasGlobalScope({ role: 'global:owner' }, [])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Scope, ScopeLevels } from '../../types.ee';
|
||||||
|
import { hasScope } from '../hasScope.ee';
|
||||||
|
|
||||||
|
describe('hasScope', () => {
|
||||||
|
const userScopes: ScopeLevels = {
|
||||||
|
global: ['user:list'],
|
||||||
|
project: ['workflow:read', 'workflow:update'],
|
||||||
|
resource: ['credential:read'],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('scope checking', () => {
|
||||||
|
test.each([
|
||||||
|
['workflow:read', true],
|
||||||
|
['workflow:delete', false],
|
||||||
|
['user:list', true],
|
||||||
|
] satisfies Array<[Scope, boolean]>)('%s -> %s', (scope, expected) => {
|
||||||
|
expect(hasScope(scope, userScopes)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('masking behavior', () => {
|
||||||
|
test('filters non-global scopes', () => {
|
||||||
|
expect(hasScope('workflow:read', userScopes, { sharing: ['workflow:update'] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores global scopes', () => {
|
||||||
|
expect(hasScope('user:list', userScopes, { sharing: [] })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checking modes', () => {
|
||||||
|
test('oneOf (default)', () => {
|
||||||
|
expect(hasScope(['workflow:read', 'invalid:scope'] as Scope[], userScopes)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allOf', () => {
|
||||||
|
expect(
|
||||||
|
hasScope(['workflow:read', 'workflow:update'], userScopes, undefined, { mode: 'allOf' }),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge cases', () => {
|
||||||
|
expect(hasScope([], userScopes, undefined, { mode: 'allOf' })).toBe(false);
|
||||||
|
expect(hasScope([], userScopes, undefined, { mode: 'oneOf' })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { GlobalRole, Scope } from '../../types.ee';
|
||||||
|
import { rolesWithScope } from '../rolesWithScope.ee';
|
||||||
|
|
||||||
|
describe('rolesWithScope', () => {
|
||||||
|
describe('global roles', () => {
|
||||||
|
test.each([
|
||||||
|
['workflow:create', ['global:owner', 'global:admin']],
|
||||||
|
['user:list', ['global:owner', 'global:admin', 'global:member']],
|
||||||
|
['invalid:scope', []],
|
||||||
|
] as Array<[Scope, GlobalRole[]]>)('%s -> %s', (scope, expected) => {
|
||||||
|
expect(rolesWithScope('global', scope)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple scopes', () => {
|
||||||
|
test('returns roles with all scopes', () => {
|
||||||
|
expect(
|
||||||
|
rolesWithScope('global', [
|
||||||
|
// all global roles have this scope
|
||||||
|
'tag:create',
|
||||||
|
// only owner and admin have this scope
|
||||||
|
'user:delete',
|
||||||
|
]),
|
||||||
|
).toEqual(['global:owner', 'global:admin']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
31
packages/@n8n/permissions/src/utilities/combineScopes.ee.ts
Normal file
31
packages/@n8n/permissions/src/utilities/combineScopes.ee.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Scope, ScopeLevels, MaskLevels } from '../types.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines scopes from different levels into a deduplicated set.
|
||||||
|
*
|
||||||
|
* @param userScopes - Scopes organized by level (global, project, resource)
|
||||||
|
* @param masks - Optional filters for non-global scopes
|
||||||
|
* @returns Set containing all allowed scopes
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* combineScopes({
|
||||||
|
* global: ['user:list'],
|
||||||
|
* project: ['workflow:read'],
|
||||||
|
* }, { sharing: ['workflow:read'] });
|
||||||
|
*/
|
||||||
|
export function combineScopes(userScopes: ScopeLevels, masks?: MaskLevels): Set<Scope> {
|
||||||
|
const maskedScopes: ScopeLevels = Object.fromEntries(
|
||||||
|
Object.entries(userScopes).map((e) => [e[0], [...e[1]]]),
|
||||||
|
) as ScopeLevels;
|
||||||
|
|
||||||
|
if (masks?.sharing) {
|
||||||
|
if (maskedScopes.project) {
|
||||||
|
maskedScopes.project = maskedScopes.project.filter((v) => masks.sharing.includes(v));
|
||||||
|
}
|
||||||
|
if (maskedScopes.resource) {
|
||||||
|
maskedScopes.resource = maskedScopes.resource.filter((v) => masks.sharing.includes(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(Object.values(maskedScopes).flat());
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { GLOBAL_SCOPE_MAP } from '../roles/role-maps.ee';
|
||||||
|
import type { AuthPrincipal } from '../types.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets global scopes for a principal's role.
|
||||||
|
* @param principal - Contains the role to look up
|
||||||
|
* @returns Array of scopes for the role, or empty array if not found
|
||||||
|
*/
|
||||||
|
export const getGlobalScopes = (principal: AuthPrincipal) => GLOBAL_SCOPE_MAP[principal.role] ?? [];
|
||||||
20
packages/@n8n/permissions/src/utilities/getRoleScopes.ee.ts
Normal file
20
packages/@n8n/permissions/src/utilities/getRoleScopes.ee.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
|
||||||
|
import type { AllRoleTypes, Resource, Scope } from '../types.ee';
|
||||||
|
|
||||||
|
export const COMBINED_ROLE_MAP = Object.fromEntries(
|
||||||
|
Object.values(ALL_ROLE_MAPS).flatMap((o: Record<string, Scope[]>) => Object.entries(o)),
|
||||||
|
) as Record<AllRoleTypes, Scope[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets scopes for a role, optionally filtered by resource types.
|
||||||
|
* @param role - The role to look up
|
||||||
|
* @param filters - Optional resources to filter scopes by
|
||||||
|
* @returns Array of matching scopes
|
||||||
|
*/
|
||||||
|
export function getRoleScopes(role: AllRoleTypes, filters?: Resource[]): Scope[] {
|
||||||
|
let scopes = COMBINED_ROLE_MAP[role];
|
||||||
|
if (filters) {
|
||||||
|
scopes = scopes.filter((s) => filters.includes(s.split(':')[0] as Resource));
|
||||||
|
}
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
17
packages/@n8n/permissions/src/utilities/hasGlobalScope.ee.ts
Normal file
17
packages/@n8n/permissions/src/utilities/hasGlobalScope.ee.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { getGlobalScopes } from './getGlobalScopes.ee';
|
||||||
|
import { hasScope } from './hasScope.ee';
|
||||||
|
import type { AuthPrincipal, Scope, ScopeOptions } from '../types.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an auth-principal has specified global scope(s).
|
||||||
|
* @param principal - The authentication principal to check permissions for
|
||||||
|
* @param scope - Scope(s) to verify
|
||||||
|
*/
|
||||||
|
export const hasGlobalScope = (
|
||||||
|
principal: AuthPrincipal,
|
||||||
|
scope: Scope | Scope[],
|
||||||
|
scopeOptions?: ScopeOptions,
|
||||||
|
): boolean => {
|
||||||
|
const global = getGlobalScopes(principal);
|
||||||
|
return hasScope(scope, { global }, undefined, scopeOptions);
|
||||||
|
};
|
||||||
22
packages/@n8n/permissions/src/utilities/hasScope.ee.ts
Normal file
22
packages/@n8n/permissions/src/utilities/hasScope.ee.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { combineScopes } from './combineScopes.ee';
|
||||||
|
import type { Scope, ScopeLevels, ScopeOptions, MaskLevels } from '../types.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if scopes exist in user's permissions.
|
||||||
|
* @param scope - Scope(s) to check
|
||||||
|
* @param userScopes - User's permission levels
|
||||||
|
* @param masks - Optional scope filters
|
||||||
|
* @param options - Checking mode (default: oneOf)
|
||||||
|
*/
|
||||||
|
export const hasScope = (
|
||||||
|
scope: Scope | Scope[],
|
||||||
|
userScopes: ScopeLevels,
|
||||||
|
masks?: MaskLevels,
|
||||||
|
options: ScopeOptions = { mode: 'oneOf' },
|
||||||
|
): boolean => {
|
||||||
|
if (!Array.isArray(scope)) scope = [scope];
|
||||||
|
const userScopeSet = combineScopes(userScopes, masks);
|
||||||
|
return options.mode === 'allOf'
|
||||||
|
? !!scope.length && scope.every((s) => userScopeSet.has(s))
|
||||||
|
: scope.some((s) => userScopeSet.has(s));
|
||||||
|
};
|
||||||
37
packages/@n8n/permissions/src/utilities/rolesWithScope.ee.ts
Normal file
37
packages/@n8n/permissions/src/utilities/rolesWithScope.ee.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
|
||||||
|
import type {
|
||||||
|
CredentialSharingRole,
|
||||||
|
GlobalRole,
|
||||||
|
ProjectRole,
|
||||||
|
RoleNamespace,
|
||||||
|
Scope,
|
||||||
|
WorkflowSharingRole,
|
||||||
|
} from '../types.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves roles within a specific namespace that have all the given scopes.
|
||||||
|
* @param namespace - The role namespace to search in
|
||||||
|
* @param scopes - Scope(s) to filter by
|
||||||
|
*/
|
||||||
|
export function rolesWithScope(namespace: 'global', scopes: Scope | Scope[]): GlobalRole[];
|
||||||
|
export function rolesWithScope(namespace: 'project', scopes: Scope | Scope[]): ProjectRole[];
|
||||||
|
export function rolesWithScope(
|
||||||
|
namespace: 'credential',
|
||||||
|
scopes: Scope | Scope[],
|
||||||
|
): CredentialSharingRole[];
|
||||||
|
export function rolesWithScope(
|
||||||
|
namespace: 'workflow',
|
||||||
|
scopes: Scope | Scope[],
|
||||||
|
): WorkflowSharingRole[];
|
||||||
|
export function rolesWithScope(namespace: RoleNamespace, scopes: Scope | Scope[]) {
|
||||||
|
if (!Array.isArray(scopes)) {
|
||||||
|
scopes = [scopes];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(ALL_ROLE_MAPS[namespace]).filter((k) => {
|
||||||
|
return scopes.every((s) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||||
|
((ALL_ROLE_MAPS[namespace] as any)[k] as Scope[]).includes(s),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import { hasScope } from '@/hasScope.ee';
|
|
||||||
import type { Scope } from '@/types.ee';
|
|
||||||
|
|
||||||
const ownerPermissions: Scope[] = [
|
|
||||||
'workflow:create',
|
|
||||||
'workflow:read',
|
|
||||||
'workflow:update',
|
|
||||||
'workflow:delete',
|
|
||||||
'workflow:list',
|
|
||||||
'user:create',
|
|
||||||
'user:read',
|
|
||||||
'user:update',
|
|
||||||
'user:delete',
|
|
||||||
'user:list',
|
|
||||||
'credential:create',
|
|
||||||
'credential:read',
|
|
||||||
'credential:update',
|
|
||||||
'credential:delete',
|
|
||||||
'credential:list',
|
|
||||||
'variable:create',
|
|
||||||
'variable:read',
|
|
||||||
'variable:update',
|
|
||||||
'variable:delete',
|
|
||||||
'variable:list',
|
|
||||||
];
|
|
||||||
const memberPermissions: Scope[] = ['user:list', 'variable:list', 'variable:read'];
|
|
||||||
|
|
||||||
describe('hasScope', () => {
|
|
||||||
test('should work with a single permission on both modes with only global scopes', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'user:list',
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'oneOf' },
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'user:list',
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'allOf' },
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:read',
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'oneOf' },
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:read',
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'allOf' },
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work with oneOf mode', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(['workflow:create', 'workflow:read'], {
|
|
||||||
global: ownerPermissions,
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(['workflow:create', 'workflow:read'], {
|
|
||||||
global: memberPermissions,
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope([], {
|
|
||||||
global: memberPermissions,
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work with allOf mode', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
['workflow:create', 'workflow:read'],
|
|
||||||
{
|
|
||||||
global: ownerPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'allOf' },
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
['workflow:create', 'workflow:read'],
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'allOf' },
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
['workflow:create', 'user:list'],
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'allOf' },
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
global: memberPermissions,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ mode: 'allOf' },
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hasScope masking', () => {
|
|
||||||
test('should return true without mask when scopes present', () => {
|
|
||||||
expect(
|
|
||||||
hasScope('workflow:read', {
|
|
||||||
global: ['user:list'],
|
|
||||||
project: ['workflow:read'],
|
|
||||||
resource: [],
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false without mask when scopes are not present', () => {
|
|
||||||
expect(
|
|
||||||
hasScope('workflow:update', {
|
|
||||||
global: ['user:list'],
|
|
||||||
project: ['workflow:read'],
|
|
||||||
resource: [],
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false when mask does not include scope but scopes list does contain required scope', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:update',
|
|
||||||
{
|
|
||||||
global: ['user:list'],
|
|
||||||
project: ['workflow:read', 'workflow:update'],
|
|
||||||
resource: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharing: ['workflow:read'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return true when mask does include scope and scope list includes scope', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:update',
|
|
||||||
{
|
|
||||||
global: ['user:list'],
|
|
||||||
project: ['workflow:read', 'workflow:update'],
|
|
||||||
resource: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharing: ['workflow:read', 'workflow:update'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return true when mask does include scope and scopes list includes scope on multiple levels', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:update',
|
|
||||||
{
|
|
||||||
global: ['user:list'],
|
|
||||||
project: ['workflow:read', 'workflow:update'],
|
|
||||||
resource: ['workflow:update'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharing: ['workflow:read', 'workflow:update'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not mask out global scopes', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:update',
|
|
||||||
{
|
|
||||||
global: ['workflow:read', 'workflow:update'],
|
|
||||||
project: ['workflow:read'],
|
|
||||||
resource: ['workflow:read'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharing: ['workflow:read'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false when scope is not in mask or scope list', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:update',
|
|
||||||
{
|
|
||||||
global: ['workflow:read'],
|
|
||||||
project: ['workflow:read'],
|
|
||||||
resource: ['workflow:read'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharing: ['workflow:read'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false when scope is in mask or not scope list', () => {
|
|
||||||
expect(
|
|
||||||
hasScope(
|
|
||||||
'workflow:update',
|
|
||||||
{
|
|
||||||
global: ['workflow:read'],
|
|
||||||
project: ['workflow:read'],
|
|
||||||
resource: ['workflow:read'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharing: ['workflow:read', 'workflow:update'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["test/**"]
|
"exclude": ["src/**/__tests__/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
},
|
},
|
||||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export class AuthService {
|
|||||||
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
||||||
if (
|
if (
|
||||||
config.getEnv('userManagement.isInstanceOwnerSetUp') &&
|
config.getEnv('userManagement.isInstanceOwnerSetUp') &&
|
||||||
!user.isOwner &&
|
user.role !== 'global:owner' &&
|
||||||
!isWithinUsersLimit
|
!isWithinUsersLimit
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ResolvePasswordTokenQueryDto,
|
ResolvePasswordTokenQueryDto,
|
||||||
} from '@n8n/api-types';
|
} from '@n8n/api-types';
|
||||||
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
|
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
|
||||||
|
import { hasGlobalScope } from '@n8n/permissions';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Logger } from 'n8n-core';
|
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
|
// User should just be able to reset password if one is already present
|
||||||
const user = await this.userRepository.findNonShellUser(email);
|
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(
|
this.logger.debug(
|
||||||
'Request to send password reset email failed because the user limit was reached',
|
'Request to send password reset email failed because the user limit was reached',
|
||||||
);
|
);
|
||||||
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isSamlCurrentAuthenticationMethod() &&
|
isSamlCurrentAuthenticationMethod() &&
|
||||||
!(
|
!(
|
||||||
user?.hasGlobalScope('user:resetPassword') === true ||
|
user &&
|
||||||
user?.settings?.allowSSOManualLogin === true
|
(hasGlobalScope(user, 'user:resetPassword') || user.settings?.allowSSOManualLogin === true)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@@ -84,8 +90,8 @@ export class PasswordResetController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
|
const ldapIdentity = user.authIdentities?.find((i) => i.providerType === 'ldap');
|
||||||
if (!user?.password || (ldapIdentity && user.disabled)) {
|
if (!user.password || (ldapIdentity && user.disabled)) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Request to send password reset email failed because no user was found for the provided email',
|
'Request to send password reset email failed because no user was found for the provided email',
|
||||||
{ invalidEmail: email },
|
{ invalidEmail: email },
|
||||||
@@ -140,7 +146,7 @@ export class PasswordResetController {
|
|||||||
const user = await this.authService.resolvePasswordResetToken(token);
|
const user = await this.authService.resolvePasswordResetToken(token);
|
||||||
if (!user) throw new NotFoundError('');
|
if (!user) throw new NotFoundError('');
|
||||||
|
|
||||||
if (!user?.isOwner && !this.license.isWithinUsersLimit()) {
|
if (user.role !== 'global:owner' && !this.license.isWithinUsersLimit()) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Request to resolve password token failed because the user limit was reached',
|
'Request to resolve password token failed because the user limit was reached',
|
||||||
{ userId: user.id },
|
{ userId: user.id },
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
} from '@n8n/decorators';
|
} from '@n8n/decorators';
|
||||||
import { combineScopes } from '@n8n/permissions';
|
import { combineScopes, getRoleScopes, hasGlobalScope } from '@n8n/permissions';
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope } from '@n8n/permissions';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In, Not } from '@n8n/typeorm';
|
import { In, Not } from '@n8n/typeorm';
|
||||||
@@ -30,13 +30,11 @@ import {
|
|||||||
TeamProjectOverQuotaError,
|
TeamProjectOverQuotaError,
|
||||||
UnlicensedProjectRoleError,
|
UnlicensedProjectRoleError,
|
||||||
} from '@/services/project.service.ee';
|
} from '@/services/project.service.ee';
|
||||||
import { RoleService } from '@/services/role.service';
|
|
||||||
|
|
||||||
@RestController('/projects')
|
@RestController('/projects')
|
||||||
export class ProjectController {
|
export class ProjectController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly projectsService: ProjectService,
|
private readonly projectsService: ProjectService,
|
||||||
private readonly roleService: RoleService,
|
|
||||||
private readonly projectRepository: ProjectRepository,
|
private readonly projectRepository: ProjectRepository,
|
||||||
private readonly eventService: EventService,
|
private readonly eventService: EventService,
|
||||||
) {}
|
) {}
|
||||||
@@ -69,8 +67,8 @@ export class ProjectController {
|
|||||||
role: 'project:admin',
|
role: 'project:admin',
|
||||||
scopes: [
|
scopes: [
|
||||||
...combineScopes({
|
...combineScopes({
|
||||||
global: this.roleService.getRoleScopes(req.user.role),
|
global: getRoleScopes(req.user.role),
|
||||||
project: this.roleService.getRoleScopes('project:admin'),
|
project: getRoleScopes('project:admin'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -88,7 +86,7 @@ export class ProjectController {
|
|||||||
_res: Response,
|
_res: Response,
|
||||||
): Promise<ProjectRequest.GetMyProjectsResponse> {
|
): Promise<ProjectRequest.GetMyProjectsResponse> {
|
||||||
const relations = await this.projectsService.getProjectRelationsForUser(req.user);
|
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({
|
? await this.projectRepository.findBy({
|
||||||
type: 'team',
|
type: 'team',
|
||||||
id: Not(In(relations.map((pr) => pr.projectId))),
|
id: Not(In(relations.map((pr) => pr.projectId))),
|
||||||
@@ -106,8 +104,8 @@ export class ProjectController {
|
|||||||
if (result.scopes) {
|
if (result.scopes) {
|
||||||
result.scopes.push(
|
result.scopes.push(
|
||||||
...combineScopes({
|
...combineScopes({
|
||||||
global: this.roleService.getRoleScopes(req.user.role),
|
global: getRoleScopes(req.user.role),
|
||||||
project: this.roleService.getRoleScopes(pr.role),
|
project: getRoleScopes(pr.role),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -128,9 +126,7 @@ export class ProjectController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.scopes) {
|
if (result.scopes) {
|
||||||
result.scopes.push(
|
result.scopes.push(...combineScopes({ global: getRoleScopes(req.user.role) }));
|
||||||
...combineScopes({ global: this.roleService.getRoleScopes(req.user.role) }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(result);
|
results.push(result);
|
||||||
@@ -154,8 +150,8 @@ export class ProjectController {
|
|||||||
}
|
}
|
||||||
const scopes: Scope[] = [
|
const scopes: Scope[] = [
|
||||||
...combineScopes({
|
...combineScopes({
|
||||||
global: this.roleService.getRoleScopes(req.user.role),
|
global: getRoleScopes(req.user.role),
|
||||||
project: this.roleService.getRoleScopes('project:personalOwner'),
|
project: getRoleScopes('project:personalOwner'),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
@@ -191,8 +187,8 @@ export class ProjectController {
|
|||||||
})),
|
})),
|
||||||
scopes: [
|
scopes: [
|
||||||
...combineScopes({
|
...combineScopes({
|
||||||
global: this.roleService.getRoleScopes(req.user.role),
|
global: getRoleScopes(req.user.role),
|
||||||
...(myRelation ? { project: this.roleService.getRoleScopes(myRelation.role) } : {}),
|
...(myRelation ? { project: getRoleScopes(myRelation.role) } : {}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import { Get, RestController } from '@n8n/decorators';
|
import { Get, RestController } from '@n8n/decorators';
|
||||||
|
|
||||||
import { type AllRoleTypes, RoleService } from '@/services/role.service';
|
import { RoleService } from '@/services/role.service';
|
||||||
|
|
||||||
@RestController('/roles')
|
@RestController('/roles')
|
||||||
export class RoleController {
|
export class RoleController {
|
||||||
constructor(private readonly roleService: RoleService) {}
|
constructor(private readonly roleService: RoleService) {}
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async getAllRoles() {
|
getAllRoles() {
|
||||||
return Object.fromEntries(
|
return this.roleService.getAllRoles();
|
||||||
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),
|
|
||||||
})),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
import type { CredentialsEntity, SharedCredentials, User } from '@n8n/db';
|
||||||
import type { CredentialsEntity, SharedCredentials, CredentialSharingRole, User } from '@n8n/db';
|
|
||||||
import { CredentialsRepository } from '@n8n/db';
|
import { CredentialsRepository } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||||
import { RoleService } from '@/services/role.service';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CredentialsFinderService {
|
export class CredentialsFinderService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
||||||
private readonly roleService: RoleService,
|
|
||||||
private readonly credentialsRepository: CredentialsRepository,
|
private readonly credentialsRepository: CredentialsRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -29,9 +27,9 @@ export class CredentialsFinderService {
|
|||||||
async findCredentialsForUser(user: User, scopes: Scope[]) {
|
async findCredentialsForUser(user: User, scopes: Scope[]) {
|
||||||
let where: FindOptionsWhere<CredentialsEntity> = {};
|
let where: FindOptionsWhere<CredentialsEntity> = {};
|
||||||
|
|
||||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
const credentialRoles = this.roleService.rolesWithScope('credential', scopes);
|
const credentialRoles = rolesWithScope('credential', scopes);
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
shared: {
|
shared: {
|
||||||
@@ -53,9 +51,9 @@ export class CredentialsFinderService {
|
|||||||
async findCredentialForUser(credentialsId: string, user: User, scopes: Scope[]) {
|
async findCredentialForUser(credentialsId: string, user: User, scopes: Scope[]) {
|
||||||
let where: FindOptionsWhere<SharedCredentials> = { credentialsId };
|
let where: FindOptionsWhere<SharedCredentials> = { credentialsId };
|
||||||
|
|
||||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
const credentialRoles = this.roleService.rolesWithScope('credential', scopes);
|
const credentialRoles = rolesWithScope('credential', scopes);
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
role: In(credentialRoles),
|
role: In(credentialRoles),
|
||||||
@@ -85,9 +83,9 @@ export class CredentialsFinderService {
|
|||||||
async findAllCredentialsForUser(user: User, scopes: Scope[], trx?: EntityManager) {
|
async findAllCredentialsForUser(user: User, scopes: Scope[], trx?: EntityManager) {
|
||||||
let where: FindOptionsWhere<SharedCredentials> = {};
|
let where: FindOptionsWhere<SharedCredentials> = {};
|
||||||
|
|
||||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
const credentialRoles = this.roleService.rolesWithScope('credential', scopes);
|
const credentialRoles = rolesWithScope('credential', scopes);
|
||||||
where = {
|
where = {
|
||||||
role: In(credentialRoles),
|
role: In(credentialRoles),
|
||||||
project: {
|
project: {
|
||||||
@@ -115,13 +113,9 @@ export class CredentialsFinderService {
|
|||||||
trx?: EntityManager,
|
trx?: EntityManager,
|
||||||
) {
|
) {
|
||||||
const projectRoles =
|
const projectRoles =
|
||||||
'scopes' in options
|
'scopes' in options ? rolesWithScope('project', options.scopes) : options.projectRoles;
|
||||||
? this.roleService.rolesWithScope('project', options.scopes)
|
|
||||||
: options.projectRoles;
|
|
||||||
const credentialRoles =
|
const credentialRoles =
|
||||||
'scopes' in options
|
'scopes' in options ? rolesWithScope('credential', options.scopes) : options.credentialRoles;
|
||||||
? this.roleService.rolesWithScope('credential', options.scopes)
|
|
||||||
: options.credentialRoles;
|
|
||||||
|
|
||||||
const sharings = await this.sharedCredentialsRepository.findCredentialsByRoles(
|
const sharings = await this.sharedCredentialsRepository.findCredentialsByRoles(
|
||||||
userIds,
|
userIds,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Project, SharedCredentials } from '@n8n/db';
|
import { Project, SharedCredentials } from '@n8n/db';
|
||||||
import type { CredentialsEntity, User } from '@n8n/db';
|
import type { CredentialsEntity, User } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
import { hasGlobalScope, rolesWithScope } from '@n8n/permissions';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In, type EntityManager } from '@n8n/typeorm';
|
import { In, type EntityManager } from '@n8n/typeorm';
|
||||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
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 { TransferCredentialError } from '@/errors/response-errors/transfer-credential.error';
|
||||||
import { OwnershipService } from '@/services/ownership.service';
|
import { OwnershipService } from '@/services/ownership.service';
|
||||||
import { ProjectService } from '@/services/project.service.ee';
|
import { ProjectService } from '@/services/project.service.ee';
|
||||||
import { RoleService } from '@/services/role.service';
|
|
||||||
|
|
||||||
import { CredentialsFinderService } from './credentials-finder.service';
|
import { CredentialsFinderService } from './credentials-finder.service';
|
||||||
import { CredentialsService } from './credentials.service';
|
import { CredentialsService } from './credentials.service';
|
||||||
@@ -22,7 +22,6 @@ export class EnterpriseCredentialsService {
|
|||||||
private readonly ownershipService: OwnershipService,
|
private readonly ownershipService: OwnershipService,
|
||||||
private readonly credentialsService: CredentialsService,
|
private readonly credentialsService: CredentialsService,
|
||||||
private readonly projectService: ProjectService,
|
private readonly projectService: ProjectService,
|
||||||
private readonly roleService: RoleService,
|
|
||||||
private readonly credentialsFinderService: CredentialsFinderService,
|
private readonly credentialsFinderService: CredentialsFinderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -41,12 +40,12 @@ export class EnterpriseCredentialsService {
|
|||||||
type: 'team',
|
type: 'team',
|
||||||
// if user can see all projects, don't check project access
|
// if user can see all projects, don't check project access
|
||||||
// if they can't, find projects they can list
|
// if they can't, find projects they can list
|
||||||
...(user.hasGlobalScope('project:list')
|
...(hasGlobalScope(user, 'project:list')
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
projectRelations: {
|
projectRelations: {
|
||||||
userId: user.id,
|
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 type { Project, User, ICredentialsDb, ScopesField } from '@n8n/db';
|
||||||
import { CredentialsEntity, SharedCredentials, CredentialsRepository } from '@n8n/db';
|
import { CredentialsEntity, SharedCredentials, CredentialsRepository } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import {
|
import {
|
||||||
In,
|
In,
|
||||||
@@ -79,7 +79,7 @@ export class CredentialsService {
|
|||||||
onlySharedWithMe?: boolean;
|
onlySharedWithMe?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const returnAll = user.hasGlobalScope('credential:list');
|
const returnAll = hasGlobalScope(user, 'credential:list');
|
||||||
const isDefaultSelect = !listQueryOptions.select;
|
const isDefaultSelect = !listQueryOptions.select;
|
||||||
const projectId =
|
const projectId =
|
||||||
typeof listQueryOptions.filter?.projectId === 'string'
|
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
|
// 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.
|
// project has global read permissions it can use all personal credentials.
|
||||||
const user = await this.userRepository.findPersonalOwnerForWorkflow(workflowId);
|
const user = await this.userRepository.findPersonalOwnerForWorkflow(workflowId);
|
||||||
if (user?.hasGlobalScope('credential:read')) {
|
if (user && hasGlobalScope(user, 'credential:read')) {
|
||||||
return await this.credentialsRepository.findAllPersonalCredentials();
|
return await this.credentialsRepository.findAllPersonalCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ export class CredentialsService {
|
|||||||
// read permissions then all workflows in that project can use all
|
// read permissions then all workflows in that project can use all
|
||||||
// credentials of all personal projects.
|
// credentials of all personal projects.
|
||||||
const user = await this.userRepository.findPersonalOwnerForProject(projectId);
|
const user = await this.userRepository.findPersonalOwnerForProject(projectId);
|
||||||
if (user?.hasGlobalScope('credential:read')) {
|
if (user && hasGlobalScope(user, 'credential:read')) {
|
||||||
return await this.credentialsRepository.findAllPersonalCredentials();
|
return await this.credentialsRepository.findAllPersonalCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ export class CredentialsService {
|
|||||||
): Promise<SharedCredentials | null> {
|
): Promise<SharedCredentials | null> {
|
||||||
let where: FindOptionsWhere<SharedCredentials> = { credentialsId: credentialId };
|
let where: FindOptionsWhere<SharedCredentials> = { credentialsId: credentialId };
|
||||||
|
|
||||||
if (!user.hasGlobalScope(globalScopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, globalScopes, { mode: 'allOf' })) {
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
role: 'credential:owner',
|
role: 'credential:owner',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import { generateNanoId } from '@n8n/db';
|
import { generateNanoId } from '@n8n/db';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
|
import type { ProjectRole } from '@n8n/permissions';
|
||||||
import { UserError } from 'n8n-workflow';
|
import { UserError } from 'n8n-workflow';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import { ProjectRelation } from '@n8n/db';
|
import { ProjectRelation } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
import type { ProjectRole } from '@n8n/permissions';
|
||||||
import { DataSource, In, Repository } from '@n8n/typeorm';
|
import { DataSource, In, Repository } from '@n8n/typeorm';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import { SharedCredentials } from '@n8n/db';
|
import { SharedCredentials } from '@n8n/db';
|
||||||
import type { Project, CredentialSharingRole } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
import type { CredentialSharingRole, ProjectRole } from '@n8n/permissions';
|
||||||
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
||||||
import { DataSource, In, Not, Repository } from '@n8n/typeorm';
|
import { DataSource, In, Not, Repository } from '@n8n/typeorm';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SharedWorkflow } from '@n8n/db';
|
import { SharedWorkflow } from '@n8n/db';
|
||||||
import type { Project, WorkflowSharingRole } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
import type { WorkflowSharingRole } from '@n8n/permissions';
|
||||||
import { DataSource, Repository, In, Not } from '@n8n/typeorm';
|
import { DataSource, Repository, In, Not } from '@n8n/typeorm';
|
||||||
import type { EntityManager, FindManyOptions, FindOptionsWhere } from '@n8n/typeorm';
|
import type { EntityManager, FindManyOptions, FindOptionsWhere } from '@n8n/typeorm';
|
||||||
|
|
||||||
|
|||||||
@@ -95,9 +95,7 @@ describe('CredentialsPermissionChecker', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip credential checks if the home project owner has global scope', async () => {
|
it('should skip credential checks if the home project owner has global scope', async () => {
|
||||||
const projectOwner = mock<User>({
|
const projectOwner = mock<User>({ role: 'global:owner' });
|
||||||
hasGlobalScope: (scope) => scope === 'credential:list',
|
|
||||||
});
|
|
||||||
ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner);
|
ownershipService.getPersonalProjectOwnerCached.mockResolvedValueOnce(projectOwner);
|
||||||
|
|
||||||
await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow();
|
await expect(permissionChecker.check(workflowId, [node])).resolves.not.toThrow();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
import { hasGlobalScope } from '@n8n/permissions';
|
||||||
import type { INode } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
import { UserError } from 'n8n-workflow';
|
import { UserError } from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -45,7 +46,11 @@ export class CredentialsPermissionChecker {
|
|||||||
const homeProjectOwner = await this.ownershipService.getPersonalProjectOwnerCached(
|
const homeProjectOwner = await this.ownershipService.getPersonalProjectOwnerCached(
|
||||||
homeProject.id,
|
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
|
// Workflow belongs to a project by a user with privileges
|
||||||
// so all credentials are usable. Skip credential checks.
|
// so all credentials are usable. Skip credential checks.
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ICredentialsBase, IExecutionBase, IExecutionDb, ITagBase } from '@n8n/db';
|
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 { Application } from 'express';
|
||||||
import type {
|
import type {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
@@ -207,7 +207,7 @@ export interface ILicensePostResponse extends ILicenseReadResponse {
|
|||||||
|
|
||||||
export interface Invitation {
|
export interface Invitation {
|
||||||
email: string;
|
email: string;
|
||||||
role: AssignableRole;
|
role: AssignableGlobalRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface N8nApp {
|
export interface N8nApp {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
@@ -8,7 +8,6 @@ import { UnexpectedError } from 'n8n-workflow';
|
|||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.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:
|
* Check if a user has the required scopes. The check can be:
|
||||||
@@ -28,15 +27,14 @@ export async function userHasScopes(
|
|||||||
projectId,
|
projectId,
|
||||||
}: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */,
|
}: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (user.hasGlobalScope(scopes, { mode: 'allOf' })) return true;
|
if (hasGlobalScope(user, scopes, { mode: 'allOf' })) return true;
|
||||||
|
|
||||||
if (globalOnly) return false;
|
if (globalOnly) return false;
|
||||||
|
|
||||||
// Find which project roles are defined to contain the required scopes.
|
// Find which project roles are defined to contain the required scopes.
|
||||||
// Then find projects having this user and having those project roles.
|
// Then find projects having this user and having those project roles.
|
||||||
|
|
||||||
const roleService = Container.get(RoleService);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
const projectRoles = roleService.rolesWithScope('project', scopes);
|
|
||||||
const userProjectIds = (
|
const userProjectIds = (
|
||||||
await Container.get(ProjectRepository).find({
|
await Container.get(ProjectRepository).find({
|
||||||
where: {
|
where: {
|
||||||
@@ -57,7 +55,7 @@ export async function userHasScopes(
|
|||||||
return await Container.get(SharedCredentialsRepository).existsBy({
|
return await Container.get(SharedCredentialsRepository).existsBy({
|
||||||
credentialsId: credentialId,
|
credentialsId: credentialId,
|
||||||
projectId: In(userProjectIds),
|
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({
|
return await Container.get(SharedWorkflowRepository).existsBy({
|
||||||
workflowId,
|
workflowId,
|
||||||
projectId: In(userProjectIds),
|
projectId: In(userProjectIds),
|
||||||
role: In(roleService.rolesWithScope('workflow', scopes)),
|
role: In(rolesWithScope('workflow', scopes)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { GlobalConfig } from '@n8n/config';
|
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 { WorkflowEntity, WorkflowTagMapping, SharedWorkflow } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
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 type { WorkflowId } from 'n8n-workflow';
|
||||||
|
|
||||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
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 { 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 express from 'express';
|
||||||
import type {
|
import type {
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
@@ -137,7 +137,7 @@ export declare namespace UserRequest {
|
|||||||
email: string;
|
email: string;
|
||||||
inviteAcceptUrl?: string;
|
inviteAcceptUrl?: string;
|
||||||
emailSent: boolean;
|
emailSent: boolean;
|
||||||
role: AssignableRole;
|
role: AssignableGlobalRole;
|
||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,21 +43,19 @@ describe('ActiveWorkflowsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return all workflow ids when user has full access', async () => {
|
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);
|
const ids = await service.getAllActiveIdsFor(user);
|
||||||
|
|
||||||
expect(ids).toEqual(['2', '3', '4']);
|
expect(ids).toEqual(['2', '3', '4']);
|
||||||
expect(user.hasGlobalScope).toHaveBeenCalledWith('workflow:list');
|
|
||||||
expect(sharedWorkflowRepository.getSharedWorkflowIds).not.toHaveBeenCalled();
|
expect(sharedWorkflowRepository.getSharedWorkflowIds).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out workflow ids that the user does not have access to', async () => {
|
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']);
|
sharedWorkflowRepository.getSharedWorkflowIds.mockResolvedValue(['3']);
|
||||||
const ids = await service.getAllActiveIdsFor(user);
|
const ids = await service.getAllActiveIdsFor(user);
|
||||||
|
|
||||||
expect(ids).toEqual(['3']);
|
expect(ids).toEqual(['3']);
|
||||||
expect(user.hasGlobalScope).toHaveBeenCalledWith('workflow:list');
|
|
||||||
expect(sharedWorkflowRepository.getSharedWorkflowIds).toHaveBeenCalledWith(activeIds);
|
expect(sharedWorkflowRepository.getSharedWorkflowIds).toHaveBeenCalledWith(activeIds);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { SharedCredentials } from '@n8n/db';
|
|||||||
import type { CredentialsEntity } from '@n8n/db';
|
import type { CredentialsEntity } from '@n8n/db';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
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 { In } from '@n8n/typeorm';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
@@ -19,19 +17,11 @@ describe('CredentialsFinderService', () => {
|
|||||||
const sharedCredential = mock<SharedCredentials>();
|
const sharedCredential = mock<SharedCredentials>();
|
||||||
sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId });
|
sharedCredential.credentials = mock<CredentialsEntity>({ id: credentialsId });
|
||||||
const owner = mock<User>({
|
const owner = mock<User>({
|
||||||
isOwner: true,
|
role: 'global:owner',
|
||||||
hasGlobalScope: (scope) =>
|
|
||||||
hasScope(scope, {
|
|
||||||
global: GLOBAL_OWNER_SCOPES,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const member = mock<User>({
|
const member = mock<User>({
|
||||||
isOwner: false,
|
role: 'global:member',
|
||||||
id: 'test',
|
id: 'test',
|
||||||
hasGlobalScope: (scope) =>
|
|
||||||
hasScope(scope, {
|
|
||||||
global: GLOBAL_MEMBER_SCOPES,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
|
import { hasGlobalScope } from '@n8n/permissions';
|
||||||
import { Logger } from 'n8n-core';
|
import { Logger } from 'n8n-core';
|
||||||
|
|
||||||
import { ActivationErrorsService } from '@/activation-errors.service';
|
import { ActivationErrorsService } from '@/activation-errors.service';
|
||||||
@@ -28,7 +29,7 @@ export class ActiveWorkflowsService {
|
|||||||
const activationErrors = await this.activationErrorsService.getAll();
|
const activationErrors = await this.activationErrorsService.getAll();
|
||||||
const activeWorkflowIds = await this.workflowRepository.getActiveIds();
|
const activeWorkflowIds = await this.workflowRepository.getActiveIds();
|
||||||
|
|
||||||
const hasFullAccess = user.hasGlobalScope('workflow:list');
|
const hasFullAccess = hasGlobalScope(user, 'workflow:list');
|
||||||
if (hasFullAccess) {
|
if (hasFullAccess) {
|
||||||
return activeWorkflowIds.filter((workflowId) => !activationErrors[workflowId]);
|
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 { UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import { Project, ProjectRelation } from '@n8n/db';
|
import { Project, ProjectRelation } from '@n8n/db';
|
||||||
import { Container, Service } from '@n8n/di';
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import type { FindOptionsWhere, EntityManager } from '@n8n/typeorm';
|
import type { FindOptionsWhere, EntityManager } from '@n8n/typeorm';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// 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 { License } from '@/license';
|
||||||
|
|
||||||
import { CacheService } from './cache/cache.service';
|
import { CacheService } from './cache/cache.service';
|
||||||
import { RoleService } from './role.service';
|
|
||||||
|
|
||||||
export class TeamProjectOverQuotaError extends UserError {
|
export class TeamProjectOverQuotaError extends UserError {
|
||||||
constructor(limit: number) {
|
constructor(limit: number) {
|
||||||
@@ -42,7 +41,6 @@ export class ProjectService {
|
|||||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||||
private readonly projectRepository: ProjectRepository,
|
private readonly projectRepository: ProjectRepository,
|
||||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||||
private readonly roleService: RoleService,
|
|
||||||
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
private readonly license: License,
|
private readonly license: License,
|
||||||
@@ -168,7 +166,7 @@ export class ProjectService {
|
|||||||
|
|
||||||
async getAccessibleProjects(user: User): Promise<Project[]> {
|
async getAccessibleProjects(user: User): Promise<Project[]> {
|
||||||
// This user is probably an admin, show them everything
|
// 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.find();
|
||||||
}
|
}
|
||||||
return await this.projectRepository.getAccessibleProjects(user.id);
|
return await this.projectRepository.getAccessibleProjects(user.id);
|
||||||
@@ -234,7 +232,7 @@ export class ProjectService {
|
|||||||
const existing = project.projectRelations.find((pr) => pr.userId === r.userId);
|
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
|
// We don't throw an error if the user already exists with that role so
|
||||||
// existing projects continue working as is.
|
// 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);
|
throw new UnlicensedProjectRoleError(r.role);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +244,19 @@ export class ProjectService {
|
|||||||
await this.clearCredentialCanUseExternalSecretsCache(projectId);
|
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) {
|
async clearCredentialCanUseExternalSecretsCache(projectId: string) {
|
||||||
const shares = await this.sharedCredentialsRepository.find({
|
const shares = await this.sharedCredentialsRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@@ -293,8 +304,8 @@ export class ProjectService {
|
|||||||
id: projectId,
|
id: projectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
|
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
|
|||||||
@@ -1,159 +1,30 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import type {
|
import type {
|
||||||
CredentialsEntity,
|
CredentialsEntity,
|
||||||
CredentialSharingRole,
|
|
||||||
SharedCredentials,
|
SharedCredentials,
|
||||||
SharedWorkflow,
|
SharedWorkflow,
|
||||||
WorkflowSharingRole,
|
|
||||||
User,
|
User,
|
||||||
ListQueryDb,
|
ListQueryDb,
|
||||||
ScopesField,
|
ScopesField,
|
||||||
ProjectRelation,
|
ProjectRelation,
|
||||||
} from '@n8n/db';
|
} from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import type { GlobalRole, Resource, Scope } from '@n8n/permissions';
|
import type { AllRoleTypes, Scope } from '@n8n/permissions';
|
||||||
import {
|
import { ALL_ROLES, combineScopes, getRoleScopes } from '@n8n/permissions';
|
||||||
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 { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { License } from '@/license';
|
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()
|
@Service()
|
||||||
export class RoleService {
|
export class RoleService {
|
||||||
constructor(private readonly license: License) {}
|
constructor(private readonly license: License) {}
|
||||||
|
|
||||||
rolesWithScope(namespace: 'global', scopes: Scope | Scope[]): GlobalRole[];
|
getAllRoles() {
|
||||||
rolesWithScope(namespace: 'project', scopes: Scope | Scope[]): ProjectRole[];
|
Object.values(ALL_ROLES).forEach((entries) => {
|
||||||
rolesWithScope(namespace: 'credential', scopes: Scope | Scope[]): CredentialSharingRole[];
|
entries.forEach((entry) => {
|
||||||
rolesWithScope(namespace: 'workflow', scopes: Scope | Scope[]): WorkflowSharingRole[];
|
entry.licensed = this.isRoleLicensed(entry.role);
|
||||||
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),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
return ALL_ROLES;
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addScopes(
|
addScopes(
|
||||||
@@ -192,9 +63,7 @@ export class RoleService {
|
|||||||
| ListQueryDb.Workflow.WithScopes
|
| ListQueryDb.Workflow.WithScopes
|
||||||
| ListQueryDb.Credentials.WithScopes;
|
| ListQueryDb.Credentials.WithScopes;
|
||||||
|
|
||||||
Object.assign(entity, {
|
entity.scopes = [];
|
||||||
scopes: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shared === undefined) {
|
if (shared === undefined) {
|
||||||
return entity;
|
return entity;
|
||||||
@@ -220,7 +89,7 @@ export class RoleService {
|
|||||||
shared: SharedCredentials[] | SharedWorkflow[],
|
shared: SharedCredentials[] | SharedWorkflow[],
|
||||||
userProjectRelations: ProjectRelation[],
|
userProjectRelations: ProjectRelation[],
|
||||||
): Scope[] {
|
): Scope[] {
|
||||||
const globalScopes = this.getRoleScopes(user.role, [type]);
|
const globalScopes = getRoleScopes(user.role, [type]);
|
||||||
const scopesSet: Set<Scope> = new Set(globalScopes);
|
const scopesSet: Set<Scope> = new Set(globalScopes);
|
||||||
for (const sharedEntity of shared) {
|
for (const sharedEntity of shared) {
|
||||||
const pr = userProjectRelations.find(
|
const pr = userProjectRelations.find(
|
||||||
@@ -228,9 +97,9 @@ export class RoleService {
|
|||||||
);
|
);
|
||||||
let projectScopes: Scope[] = [];
|
let projectScopes: Scope[] = [];
|
||||||
if (pr) {
|
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(
|
const mergedScopes = combineScopes(
|
||||||
{
|
{
|
||||||
global: globalScopes,
|
global: globalScopes,
|
||||||
@@ -243,7 +112,8 @@ export class RoleService {
|
|||||||
return [...scopesSet].sort();
|
return [...scopesSet].sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
isRoleLicensed(role: AllRoleTypes) {
|
private isRoleLicensed(role: AllRoleTypes) {
|
||||||
|
// TODO: move this info into FrontendSettings
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'project:admin':
|
case 'project:admin':
|
||||||
return this.license.isProjectRoleAdminLicensed();
|
return this.license.isProjectRoleAdminLicensed();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { RoleChangeRequestDto } from '@n8n/api-types';
|
|||||||
import { User } from '@n8n/db';
|
import { User } from '@n8n/db';
|
||||||
import type { PublicUser } from '@n8n/db';
|
import type { PublicUser } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import type { AssignableRole } from '@n8n/permissions';
|
import { getGlobalScopes, type AssignableGlobalRole } from '@n8n/permissions';
|
||||||
import { Logger } from 'n8n-core';
|
import { Logger } from 'n8n-core';
|
||||||
import type { IUserSettings } from 'n8n-workflow';
|
import type { IUserSettings } from 'n8n-workflow';
|
||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
@@ -71,6 +71,7 @@ export class UserService {
|
|||||||
let publicUser: PublicUser = {
|
let publicUser: PublicUser = {
|
||||||
...rest,
|
...rest,
|
||||||
signInType: ldapIdentity ? 'ldap' : 'email',
|
signInType: ldapIdentity ? 'ldap' : 'email',
|
||||||
|
isOwner: user.role === 'global:owner',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options?.withInviteUrl && !options?.inviterId) {
|
if (options?.withInviteUrl && !options?.inviterId) {
|
||||||
@@ -85,8 +86,9 @@ export class UserService {
|
|||||||
publicUser = await this.addFeatureFlags(publicUser, options.posthog);
|
publicUser = await this.addFeatureFlags(publicUser, options.posthog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: resolve these directly in the frontend
|
||||||
if (options?.withScopes) {
|
if (options?.withScopes) {
|
||||||
publicUser.globalScopes = user.globalScopes;
|
publicUser.globalScopes = getGlobalScopes(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return publicUser;
|
return publicUser;
|
||||||
@@ -123,7 +125,7 @@ export class UserService {
|
|||||||
private async sendEmails(
|
private async sendEmails(
|
||||||
owner: User,
|
owner: User,
|
||||||
toInviteUsers: { [key: string]: string },
|
toInviteUsers: { [key: string]: string },
|
||||||
role: AssignableRole,
|
role: AssignableGlobalRole,
|
||||||
) {
|
) {
|
||||||
const domain = this.urlService.getInstanceBaseUrl();
|
const domain = this.urlService.getInstanceBaseUrl();
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import type { SharedWorkflow, User } from '@n8n/db';
|
import type { SharedWorkflow, User } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
import type { EntityManager, FindOptionsWhere } from '@n8n/typeorm';
|
||||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||||
import { RoleService } from '@/services/role.service';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class WorkflowFinderService {
|
export class WorkflowFinderService {
|
||||||
constructor(
|
constructor(private readonly sharedWorkflowRepository: SharedWorkflowRepository) {}
|
||||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
|
||||||
private readonly roleService: RoleService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async findWorkflowForUser(
|
async findWorkflowForUser(
|
||||||
workflowId: string,
|
workflowId: string,
|
||||||
@@ -28,9 +24,9 @@ export class WorkflowFinderService {
|
|||||||
) {
|
) {
|
||||||
let where: FindOptionsWhere<SharedWorkflow> = {};
|
let where: FindOptionsWhere<SharedWorkflow> = {};
|
||||||
|
|
||||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
const workflowRoles = this.roleService.rolesWithScope('workflow', scopes);
|
const workflowRoles = rolesWithScope('workflow', scopes);
|
||||||
|
|
||||||
where = {
|
where = {
|
||||||
role: In(workflowRoles),
|
role: In(workflowRoles),
|
||||||
@@ -60,9 +56,9 @@ export class WorkflowFinderService {
|
|||||||
async findAllWorkflowsForUser(user: User, scopes: Scope[]) {
|
async findAllWorkflowsForUser(user: User, scopes: Scope[]) {
|
||||||
let where: FindOptionsWhere<SharedWorkflow> = {};
|
let where: FindOptionsWhere<SharedWorkflow> = {};
|
||||||
|
|
||||||
if (!user.hasGlobalScope(scopes, { mode: 'allOf' })) {
|
if (!hasGlobalScope(user, scopes, { mode: 'allOf' })) {
|
||||||
const projectRoles = this.roleService.rolesWithScope('project', scopes);
|
const projectRoles = rolesWithScope('project', scopes);
|
||||||
const workflowRoles = this.roleService.rolesWithScope('workflow', scopes);
|
const workflowRoles = rolesWithScope('workflow', scopes);
|
||||||
|
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
import type { User } from '@n8n/db';
|
||||||
import type { WorkflowSharingRole, User } from '@n8n/db';
|
|
||||||
import { Service } from '@n8n/di';
|
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
|
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
|
|
||||||
@@ -33,7 +38,7 @@ export class WorkflowSharingService {
|
|||||||
async getSharedWorkflowIds(user: User, options: ShareWorkflowOptions): Promise<string[]> {
|
async getSharedWorkflowIds(user: User, options: ShareWorkflowOptions): Promise<string[]> {
|
||||||
const { projectId } = options;
|
const { projectId } = options;
|
||||||
|
|
||||||
if (user.hasGlobalScope('workflow:read')) {
|
if (hasGlobalScope(user, 'workflow:read')) {
|
||||||
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
||||||
select: ['workflowId'],
|
select: ['workflowId'],
|
||||||
...(projectId && { where: { projectId } }),
|
...(projectId && { where: { projectId } }),
|
||||||
@@ -42,13 +47,9 @@ export class WorkflowSharingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectRoles =
|
const projectRoles =
|
||||||
'scopes' in options
|
'scopes' in options ? rolesWithScope('project', options.scopes) : options.projectRoles;
|
||||||
? this.roleService.rolesWithScope('project', options.scopes)
|
|
||||||
: options.projectRoles;
|
|
||||||
const workflowRoles =
|
const workflowRoles =
|
||||||
'scopes' in options
|
'scopes' in options ? rolesWithScope('workflow', options.scopes) : options.workflowRoles;
|
||||||
? this.roleService.rolesWithScope('workflow', options.scopes)
|
|
||||||
: options.workflowRoles;
|
|
||||||
|
|
||||||
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export function assertReturnedUserProps(user: User) {
|
|||||||
expect(user.personalizationAnswers).toBeNull();
|
expect(user.personalizationAnswers).toBeNull();
|
||||||
expect(user.password).toBeUndefined();
|
expect(user.password).toBeUndefined();
|
||||||
expect(user.isPending).toBe(false);
|
expect(user.isPending).toBe(false);
|
||||||
expect(user.globalScopes).toBeDefined();
|
|
||||||
expect(user.globalScopes).not.toHaveLength(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const assertStoredUserProps = (user: User) => {
|
export const assertStoredUserProps = (user: User) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import type { ListQueryDb } from '@n8n/db';
|
import type { ListQueryDb } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import type { ProjectRole } from '@n8n/permissions';
|
||||||
import { In } from '@n8n/typeorm';
|
import { In } from '@n8n/typeorm';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import type { GlobalRole, Scope } from '@n8n/permissions';
|
import { getRoleScopes, type GlobalRole, type ProjectRole, type Scope } from '@n8n/permissions';
|
||||||
import { EntityNotFoundError } from '@n8n/typeorm';
|
import { EntityNotFoundError } from '@n8n/typeorm';
|
||||||
|
|
||||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||||
@@ -12,7 +11,6 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre
|
|||||||
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
|
||||||
import { getWorkflowById } from '@/public-api/v1/handlers/workflows/workflows.service';
|
import { getWorkflowById } from '@/public-api/v1/handlers/workflows/workflows.service';
|
||||||
import { CacheService } from '@/services/cache/cache.service';
|
import { CacheService } from '@/services/cache/cache.service';
|
||||||
import { RoleService } from '@/services/role.service';
|
|
||||||
import { createFolder } from '@test-integration/db/folders';
|
import { createFolder } from '@test-integration/db/folders';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -391,7 +389,7 @@ describe('POST /projects/', () => {
|
|||||||
await findProject(respProject.id);
|
await findProject(respProject.id);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
expect(resp.body.data.role).toBe('project:admin');
|
expect(resp.body.data.role).toBe('project:admin');
|
||||||
for (const scope of Container.get(RoleService).getRoleScopes('project:admin')) {
|
for (const scope of getRoleScopes('project:admin')) {
|
||||||
expect(resp.body.data.scopes).toContain(scope);
|
expect(resp.body.data.scopes).toContain(scope);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
import { getRoleScopes } from '@n8n/permissions';
|
||||||
import type { CredentialSharingRole } from '@n8n/db';
|
import type {
|
||||||
import type { WorkflowSharingRole } from '@n8n/db';
|
GlobalRole,
|
||||||
import { Container } from '@n8n/di';
|
ProjectRole,
|
||||||
import type { GlobalRole, Scope } from '@n8n/permissions';
|
CredentialSharingRole,
|
||||||
|
WorkflowSharingRole,
|
||||||
import { RoleService } from '@/services/role.service';
|
Scope,
|
||||||
|
} from '@n8n/permissions';
|
||||||
|
|
||||||
import { createMember } from './shared/db/users';
|
import { createMember } from './shared/db/users';
|
||||||
import type { SuperAgentTest } from './shared/types';
|
import type { SuperAgentTest } from './shared/types';
|
||||||
@@ -49,19 +50,19 @@ beforeAll(async () => {
|
|||||||
{
|
{
|
||||||
name: 'Owner',
|
name: 'Owner',
|
||||||
role: 'global:owner',
|
role: 'global:owner',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('global:owner'),
|
scopes: getRoleScopes('global:owner'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
role: 'global:admin',
|
role: 'global:admin',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('global:admin'),
|
scopes: getRoleScopes('global:admin'),
|
||||||
licensed: false,
|
licensed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Member',
|
name: 'Member',
|
||||||
role: 'global:member',
|
role: 'global:member',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('global:member'),
|
scopes: getRoleScopes('global:member'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -69,19 +70,19 @@ beforeAll(async () => {
|
|||||||
{
|
{
|
||||||
name: 'Project Owner',
|
name: 'Project Owner',
|
||||||
role: 'project:personalOwner',
|
role: 'project:personalOwner',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('project:personalOwner'),
|
scopes: getRoleScopes('project:personalOwner'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Project Admin',
|
name: 'Project Admin',
|
||||||
role: 'project:admin',
|
role: 'project:admin',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('project:admin'),
|
scopes: getRoleScopes('project:admin'),
|
||||||
licensed: false,
|
licensed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Project Editor',
|
name: 'Project Editor',
|
||||||
role: 'project:editor',
|
role: 'project:editor',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('project:editor'),
|
scopes: getRoleScopes('project:editor'),
|
||||||
licensed: false,
|
licensed: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -89,13 +90,13 @@ beforeAll(async () => {
|
|||||||
{
|
{
|
||||||
name: 'Credential Owner',
|
name: 'Credential Owner',
|
||||||
role: 'credential:owner',
|
role: 'credential:owner',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('credential:owner'),
|
scopes: getRoleScopes('credential:owner'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Credential User',
|
name: 'Credential User',
|
||||||
role: 'credential:user',
|
role: 'credential:user',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('credential:user'),
|
scopes: getRoleScopes('credential:user'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -103,13 +104,13 @@ beforeAll(async () => {
|
|||||||
{
|
{
|
||||||
name: 'Workflow Owner',
|
name: 'Workflow Owner',
|
||||||
role: 'workflow:owner',
|
role: 'workflow:owner',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('workflow:owner'),
|
scopes: getRoleScopes('workflow:owner'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Workflow Editor',
|
name: 'Workflow Editor',
|
||||||
role: 'workflow:editor',
|
role: 'workflow:editor',
|
||||||
scopes: Container.get(RoleService).getRoleScopes('workflow:editor'),
|
scopes: getRoleScopes('workflow:editor'),
|
||||||
licensed: true,
|
licensed: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { ProjectRole, Scope } from '@n8n/permissions';
|
||||||
|
|
||||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import type { CredentialSharingRole } from '@n8n/db';
|
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import type { ICredentialsDb } from '@n8n/db';
|
import type { ICredentialsDb } from '@n8n/db';
|
||||||
import { CredentialsEntity } from '@n8n/db';
|
import { CredentialsEntity } from '@n8n/db';
|
||||||
import { CredentialsRepository } from '@n8n/db';
|
import { CredentialsRepository } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import type { CredentialSharingRole } from '@n8n/permissions';
|
||||||
|
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import type { ProjectRelation } from '@n8n/db';
|
import type { ProjectRelation } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import type { ProjectRole } from '@n8n/permissions';
|
||||||
|
|
||||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export async function newUser(attributes: Partial<User> = {}): Promise<User> {
|
|||||||
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
||||||
const userInstance = await newUser(attributes);
|
const userInstance = await newUser(attributes);
|
||||||
const { user } = await Container.get(UserRepository).createUserWithProject(userInstance);
|
const { user } = await Container.get(UserRepository).createUserWithProject(userInstance);
|
||||||
user.computeIsOwner();
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Project } from '@n8n/db';
|
import { Project } from '@n8n/db';
|
||||||
import { User } from '@n8n/db';
|
import { User } from '@n8n/db';
|
||||||
import type { SharedWorkflow, WorkflowSharingRole } from '@n8n/db';
|
import type { SharedWorkflow } from '@n8n/db';
|
||||||
import type { IWorkflowDb } from '@n8n/db';
|
import type { IWorkflowDb } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import type { WorkflowSharingRole } from '@n8n/permissions';
|
||||||
import type { DeepPartial } from '@n8n/typeorm';
|
import type { DeepPartial } from '@n8n/typeorm';
|
||||||
import type { IWorkflowBase } from 'n8n-workflow';
|
import type { IWorkflowBase } from 'n8n-workflow';
|
||||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { ProjectRole } from '@n8n/api-types';
|
|
||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import type { WorkflowWithSharingsMetaDataAndCredentials } from '@n8n/db';
|
import type { WorkflowWithSharingsMetaDataAndCredentials } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
import type { ProjectRole } from '@n8n/permissions';
|
||||||
import { ApplicationError, WorkflowActivationError, type INode } from 'n8n-workflow';
|
import { ApplicationError, WorkflowActivationError, type INode } from 'n8n-workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import type { AllRolesMap } from '@n8n/permissions';
|
||||||
import type { IRestApiContext } from '@/Interface';
|
import type { IRestApiContext } from '@/Interface';
|
||||||
import type { RoleMap } from '@/types/roles.types';
|
|
||||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||||
|
|
||||||
export const getRoles = async (context: IRestApiContext): Promise<RoleMap> => {
|
export const getRoles = async (context: IRestApiContext): Promise<AllRolesMap> => {
|
||||||
return await makeRestApiRequest(context, 'GET', '/roles');
|
return await makeRestApiRequest(context, 'GET', '/roles');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AllRolesMap } from '@n8n/permissions';
|
||||||
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
import ProjectSharing from '@/components/Projects/ProjectSharing.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
@@ -12,7 +13,6 @@ import { useUIStore } from '@/stores/ui.store';
|
|||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
|
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import type { RoleMap } from '@/types/roles.types';
|
|
||||||
import { splitName } from '@/utils/projects.utils';
|
import { splitName } from '@/utils/projects.utils';
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||||
@@ -82,7 +82,7 @@ const credentialRoleTranslations = computed<Record<string, string>>(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const credentialRoles = computed<RoleMap['credential']>(() => {
|
const credentialRoles = computed<AllRolesMap['credential']>(() => {
|
||||||
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
|
return rolesStore.processedCredentialRoles.map(({ role, scopes, licensed }) => ({
|
||||||
role,
|
role,
|
||||||
name: credentialRoleTranslations.value[role],
|
name: credentialRoleTranslations.value[role],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { AllRolesMap } from '@n8n/permissions';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
|
import type { ProjectListItem, ProjectSharingData } from '@/types/projects.types';
|
||||||
import ProjectSharingInfo from '@/components/Projects/ProjectSharingInfo.vue';
|
import ProjectSharingInfo from '@/components/Projects/ProjectSharingInfo.vue';
|
||||||
import type { RoleMap } from '@/types/roles.types';
|
|
||||||
import { sortByProperty } from '@n8n/utils/sort/sortByProperty';
|
import { sortByProperty } from '@n8n/utils/sort/sortByProperty';
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
@@ -11,7 +11,7 @@ const locale = useI18n();
|
|||||||
type Props = {
|
type Props = {
|
||||||
projects: ProjectListItem[];
|
projects: ProjectListItem[];
|
||||||
homeProject?: ProjectSharingData;
|
homeProject?: ProjectSharingData;
|
||||||
roles?: RoleMap['workflow' | 'credential' | 'project'];
|
roles?: AllRolesMap['workflow' | 'credential' | 'project'];
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
static?: boolean;
|
static?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ProjectRole, RoleMap } from '@/types/roles.types';
|
import type { ProjectRole, AllRolesMap } from '@n8n/permissions';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import * as rolesApi from '@/api/roles.api';
|
import * as rolesApi from '@/api/roles.api';
|
||||||
@@ -7,7 +7,7 @@ import { useRootStore } from './root.store';
|
|||||||
export const useRolesStore = defineStore('roles', () => {
|
export const useRolesStore = defineStore('roles', () => {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
|
|
||||||
const roles = ref<RoleMap>({
|
const roles = ref<AllRolesMap>({
|
||||||
global: [],
|
global: [],
|
||||||
project: [],
|
project: [],
|
||||||
credential: [],
|
credential: [],
|
||||||
@@ -22,7 +22,7 @@ export const useRolesStore = defineStore('roles', () => {
|
|||||||
() => new Map(projectRoleOrder.value.map((role, idx) => [role, idx])),
|
() => new Map(projectRoleOrder.value.map((role, idx) => [role, idx])),
|
||||||
);
|
);
|
||||||
|
|
||||||
const processedProjectRoles = computed<RoleMap['project']>(() =>
|
const processedProjectRoles = computed<AllRolesMap['project']>(() =>
|
||||||
roles.value.project
|
roles.value.project
|
||||||
.filter((role) => projectRoleOrderMap.value.has(role.role))
|
.filter((role) => projectRoleOrderMap.value.has(role.role))
|
||||||
.sort(
|
.sort(
|
||||||
@@ -32,11 +32,11 @@ export const useRolesStore = defineStore('roles', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const processedCredentialRoles = computed<RoleMap['credential']>(() =>
|
const processedCredentialRoles = computed<AllRolesMap['credential']>(() =>
|
||||||
roles.value.credential.filter((role) => role.role !== 'credential:owner'),
|
roles.value.credential.filter((role) => role.role !== 'credential:owner'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const processedWorkflowRoles = computed<RoleMap['workflow']>(() =>
|
const processedWorkflowRoles = computed<AllRolesMap['workflow']>(() =>
|
||||||
roles.value.workflow.filter((role) => role.role !== 'workflow:owner'),
|
roles.value.workflow.filter((role) => role.role !== 'workflow:owner'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope, ProjectRole } from '@n8n/permissions';
|
||||||
import type { IUserResponse } from '@/Interface';
|
import type { IUserResponse } from '@/Interface';
|
||||||
import type { ProjectRole } from '@/types/roles.types';
|
|
||||||
|
|
||||||
export const ProjectTypes = {
|
export const ProjectTypes = {
|
||||||
Personal: 'personal',
|
Personal: 'personal',
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { Scope } from '@n8n/permissions';
|
|
||||||
|
|
||||||
export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member';
|
|
||||||
export type ProjectRole =
|
|
||||||
| 'project:personalOwner'
|
|
||||||
| 'project:admin'
|
|
||||||
| 'project:editor'
|
|
||||||
| 'project:viewer';
|
|
||||||
export type CredentialSharingRole = 'credential:owner' | 'credential:user';
|
|
||||||
export type WorkflowSharingRole = 'workflow:owner' | 'workflow:editor';
|
|
||||||
|
|
||||||
export type RoleObject<
|
|
||||||
T extends GlobalRole | ProjectRole | CredentialSharingRole | WorkflowSharingRole,
|
|
||||||
> = {
|
|
||||||
role: T;
|
|
||||||
name: string;
|
|
||||||
scopes: Scope[];
|
|
||||||
licensed: boolean;
|
|
||||||
};
|
|
||||||
export type RoleMap = {
|
|
||||||
global: Array<RoleObject<GlobalRole>>;
|
|
||||||
project: Array<RoleObject<ProjectRole>>;
|
|
||||||
credential: Array<RoleObject<CredentialSharingRole>>;
|
|
||||||
workflow: Array<RoleObject<WorkflowSharingRole>>;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { ProjectRole } from '@n8n/permissions';
|
||||||
import { computed, ref, watch, onBeforeMount, onMounted, nextTick } from 'vue';
|
import { computed, ref, watch, onBeforeMount, onMounted, nextTick } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { deepCopy } from 'n8n-workflow';
|
import { deepCopy } from 'n8n-workflow';
|
||||||
@@ -13,7 +14,6 @@ import { VIEWS } from '@/constants';
|
|||||||
import ProjectDeleteDialog from '@/components/Projects/ProjectDeleteDialog.vue';
|
import ProjectDeleteDialog from '@/components/Projects/ProjectDeleteDialog.vue';
|
||||||
import ProjectRoleUpgradeDialog from '@/components/Projects/ProjectRoleUpgradeDialog.vue';
|
import ProjectRoleUpgradeDialog from '@/components/Projects/ProjectRoleUpgradeDialog.vue';
|
||||||
import { useRolesStore } from '@/stores/roles.store';
|
import { useRolesStore } from '@/stores/roles.store';
|
||||||
import type { ProjectRole } from '@/types/roles.types';
|
|
||||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -919,6 +919,10 @@ importers:
|
|||||||
version: 8.4.0(@microsoft/api-extractor@7.52.1(@types/node@18.16.16))(jiti@1.21.0)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.8.2)
|
version: 8.4.0(@microsoft/api-extractor@7.52.1(@types/node@18.16.16))(jiti@1.21.0)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.8.2)
|
||||||
|
|
||||||
packages/@n8n/permissions:
|
packages/@n8n/permissions:
|
||||||
|
dependencies:
|
||||||
|
zod:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 3.24.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@n8n/typescript-config':
|
'@n8n/typescript-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|||||||
Reference in New Issue
Block a user