first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { baseConfig } from '@n8n/eslint-config/base';
|
||||
|
||||
export default defineConfig(baseConfig, {
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||
|
||||
// TODO: Remove this
|
||||
'import-x/order': 'warn',
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@n8n/permissions",
|
||||
"version": "0.52.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Scope Information ensure scopes are defined correctly 1`] = `
|
||||
[
|
||||
"aiAssistant:manage",
|
||||
"aiAssistant:*",
|
||||
"annotationTag:create",
|
||||
"annotationTag:read",
|
||||
"annotationTag:update",
|
||||
"annotationTag:delete",
|
||||
"annotationTag:list",
|
||||
"annotationTag:*",
|
||||
"auditLogs:manage",
|
||||
"auditLogs:*",
|
||||
"banner:dismiss",
|
||||
"banner:*",
|
||||
"community:register",
|
||||
"community:*",
|
||||
"communityPackage:install",
|
||||
"communityPackage:uninstall",
|
||||
"communityPackage:update",
|
||||
"communityPackage:list",
|
||||
"communityPackage:manage",
|
||||
"communityPackage:*",
|
||||
"credential:share",
|
||||
"credential:unshare",
|
||||
"credential:shareGlobally",
|
||||
"credential:move",
|
||||
"credential:create",
|
||||
"credential:read",
|
||||
"credential:update",
|
||||
"credential:delete",
|
||||
"credential:list",
|
||||
"credential:*",
|
||||
"externalSecretsProvider:sync",
|
||||
"externalSecretsProvider:create",
|
||||
"externalSecretsProvider:read",
|
||||
"externalSecretsProvider:update",
|
||||
"externalSecretsProvider:delete",
|
||||
"externalSecretsProvider:list",
|
||||
"externalSecretsProvider:*",
|
||||
"externalSecret:list",
|
||||
"externalSecret:*",
|
||||
"eventBusDestination:test",
|
||||
"eventBusDestination:create",
|
||||
"eventBusDestination:read",
|
||||
"eventBusDestination:update",
|
||||
"eventBusDestination:delete",
|
||||
"eventBusDestination:list",
|
||||
"eventBusDestination:*",
|
||||
"ldap:sync",
|
||||
"ldap:manage",
|
||||
"ldap:*",
|
||||
"license:manage",
|
||||
"license:*",
|
||||
"logStreaming:manage",
|
||||
"logStreaming:*",
|
||||
"orchestration:read",
|
||||
"orchestration:list",
|
||||
"orchestration:*",
|
||||
"project:create",
|
||||
"project:read",
|
||||
"project:update",
|
||||
"project:delete",
|
||||
"project:list",
|
||||
"project:*",
|
||||
"saml:manage",
|
||||
"saml:*",
|
||||
"securityAudit:generate",
|
||||
"securityAudit:*",
|
||||
"securitySettings:manage",
|
||||
"securitySettings:*",
|
||||
"sourceControl:pull",
|
||||
"sourceControl:push",
|
||||
"sourceControl:manage",
|
||||
"sourceControl:*",
|
||||
"tag:create",
|
||||
"tag:read",
|
||||
"tag:update",
|
||||
"tag:delete",
|
||||
"tag:list",
|
||||
"tag:*",
|
||||
"user:resetPassword",
|
||||
"user:changeRole",
|
||||
"user:enforceMfa",
|
||||
"user:generateInviteLink",
|
||||
"user:create",
|
||||
"user:read",
|
||||
"user:update",
|
||||
"user:delete",
|
||||
"user:list",
|
||||
"user:*",
|
||||
"variable:create",
|
||||
"variable:read",
|
||||
"variable:update",
|
||||
"variable:delete",
|
||||
"variable:list",
|
||||
"variable:*",
|
||||
"projectVariable:create",
|
||||
"projectVariable:read",
|
||||
"projectVariable:update",
|
||||
"projectVariable:delete",
|
||||
"projectVariable:list",
|
||||
"projectVariable:*",
|
||||
"workersView:manage",
|
||||
"workersView:*",
|
||||
"workflow:share",
|
||||
"workflow:unshare",
|
||||
"workflow:execute",
|
||||
"workflow:execute-chat",
|
||||
"workflow:move",
|
||||
"workflow:activate",
|
||||
"workflow:deactivate",
|
||||
"workflow:publish",
|
||||
"workflow:unpublish",
|
||||
"workflow:updateRedactionSetting",
|
||||
"workflow:create",
|
||||
"workflow:read",
|
||||
"workflow:update",
|
||||
"workflow:delete",
|
||||
"workflow:list",
|
||||
"workflow:*",
|
||||
"folder:create",
|
||||
"folder:read",
|
||||
"folder:update",
|
||||
"folder:delete",
|
||||
"folder:list",
|
||||
"folder:move",
|
||||
"folder:*",
|
||||
"insights:list",
|
||||
"insights:*",
|
||||
"oidc:manage",
|
||||
"oidc:*",
|
||||
"provisioning:manage",
|
||||
"provisioning:*",
|
||||
"dataTable:create",
|
||||
"dataTable:read",
|
||||
"dataTable:update",
|
||||
"dataTable:delete",
|
||||
"dataTable:list",
|
||||
"dataTable:readRow",
|
||||
"dataTable:writeRow",
|
||||
"dataTable:listProject",
|
||||
"dataTable:*",
|
||||
"execution:delete",
|
||||
"execution:read",
|
||||
"execution:retry",
|
||||
"execution:list",
|
||||
"execution:get",
|
||||
"execution:reveal",
|
||||
"execution:*",
|
||||
"workflowTags:update",
|
||||
"workflowTags:list",
|
||||
"workflowTags:*",
|
||||
"role:manage",
|
||||
"role:*",
|
||||
"mcp:manage",
|
||||
"mcp:oauth",
|
||||
"mcp:*",
|
||||
"mcpApiKey:create",
|
||||
"mcpApiKey:rotate",
|
||||
"mcpApiKey:*",
|
||||
"chatHub:manage",
|
||||
"chatHub:message",
|
||||
"chatHub:*",
|
||||
"chatHubAgent:create",
|
||||
"chatHubAgent:read",
|
||||
"chatHubAgent:update",
|
||||
"chatHubAgent:delete",
|
||||
"chatHubAgent:list",
|
||||
"chatHubAgent:*",
|
||||
"breakingChanges:list",
|
||||
"breakingChanges:*",
|
||||
"apiKey:manage",
|
||||
"apiKey:*",
|
||||
"credentialResolver:create",
|
||||
"credentialResolver:read",
|
||||
"credentialResolver:update",
|
||||
"credentialResolver:delete",
|
||||
"credentialResolver:list",
|
||||
"credentialResolver:*",
|
||||
"*",
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
PROJECT_ADMIN_ROLE_SLUG,
|
||||
PROJECT_EDITOR_ROLE_SLUG,
|
||||
PROJECT_OWNER_ROLE_SLUG,
|
||||
PROJECT_VIEWER_ROLE_SLUG,
|
||||
} from '@/constants.ee';
|
||||
|
||||
import {
|
||||
roleNamespaceSchema,
|
||||
globalRoleSchema,
|
||||
assignableGlobalRoleSchema,
|
||||
systemProjectRoleSchema,
|
||||
credentialSharingRoleSchema,
|
||||
workflowSharingRoleSchema,
|
||||
customProjectRoleSchema,
|
||||
} 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: 'valid role: global:chatUser', value: 'global:chatUser', 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: 'valid role: global:chatUser', value: 'global:chatUser', expected: true },
|
||||
{ name: 'object value', value: {}, expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = assignableGlobalRoleSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('systemProjectRoleSchema', () => {
|
||||
test.each([
|
||||
{
|
||||
name: `valid role: ${PROJECT_OWNER_ROLE_SLUG}`,
|
||||
value: PROJECT_OWNER_ROLE_SLUG,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: `valid role: ${PROJECT_ADMIN_ROLE_SLUG}`,
|
||||
value: PROJECT_ADMIN_ROLE_SLUG,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: `valid role: ${PROJECT_EDITOR_ROLE_SLUG}`,
|
||||
value: PROJECT_EDITOR_ROLE_SLUG,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: `valid role: ${PROJECT_VIEWER_ROLE_SLUG}`,
|
||||
value: PROJECT_VIEWER_ROLE_SLUG,
|
||||
expected: true,
|
||||
},
|
||||
{ name: 'invalid role', value: 'invalid-role', expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = systemProjectRoleSchema.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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customProjectRoleSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid role: custom:role', value: 'custom:role', expected: true },
|
||||
{ name: 'undefined value', value: undefined, expected: false },
|
||||
{ name: 'empty string', value: '', expected: false },
|
||||
{ name: 'system role', value: PROJECT_ADMIN_ROLE_SLUG, expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = customProjectRoleSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ALL_SCOPES } from '@/scope-information';
|
||||
|
||||
describe('Scope Information', () => {
|
||||
it('ensure scopes are defined correctly', () => {
|
||||
expect(ALL_SCOPES).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
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',
|
||||
'execution:reveal',
|
||||
'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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
export const DEFAULT_OPERATIONS = ['create', 'read', 'update', 'delete', 'list'] as const;
|
||||
|
||||
export const RESOURCES = {
|
||||
aiAssistant: ['manage'] as const,
|
||||
annotationTag: [...DEFAULT_OPERATIONS] as const,
|
||||
auditLogs: ['manage'] as const,
|
||||
banner: ['dismiss'] as const,
|
||||
community: ['register'] as const,
|
||||
communityPackage: ['install', 'uninstall', 'update', 'list', 'manage'] as const,
|
||||
credential: ['share', 'unshare', 'shareGlobally', 'move', ...DEFAULT_OPERATIONS] as const,
|
||||
externalSecretsProvider: ['sync', ...DEFAULT_OPERATIONS] as const,
|
||||
externalSecret: ['list'] as const,
|
||||
eventBusDestination: ['test', ...DEFAULT_OPERATIONS] as const,
|
||||
ldap: ['sync', 'manage'] as const,
|
||||
license: ['manage'] as const,
|
||||
logStreaming: ['manage'] as const,
|
||||
orchestration: ['read', 'list'] as const,
|
||||
project: [...DEFAULT_OPERATIONS] as const,
|
||||
saml: ['manage'] as const,
|
||||
securityAudit: ['generate'] as const,
|
||||
securitySettings: ['manage'] as const,
|
||||
sourceControl: ['pull', 'push', 'manage'] as const,
|
||||
tag: [...DEFAULT_OPERATIONS] as const,
|
||||
user: [
|
||||
'resetPassword',
|
||||
'changeRole',
|
||||
'enforceMfa',
|
||||
'generateInviteLink',
|
||||
...DEFAULT_OPERATIONS,
|
||||
] as const,
|
||||
variable: [...DEFAULT_OPERATIONS] as const,
|
||||
projectVariable: [...DEFAULT_OPERATIONS] as const,
|
||||
workersView: ['manage'] as const,
|
||||
workflow: [
|
||||
'share',
|
||||
'unshare',
|
||||
'execute',
|
||||
'execute-chat',
|
||||
'move',
|
||||
'activate',
|
||||
'deactivate',
|
||||
'publish',
|
||||
'unpublish',
|
||||
'updateRedactionSetting',
|
||||
...DEFAULT_OPERATIONS,
|
||||
] as const,
|
||||
folder: [...DEFAULT_OPERATIONS, 'move'] as const,
|
||||
insights: ['list'] as const,
|
||||
oidc: ['manage'] as const,
|
||||
provisioning: ['manage'] as const,
|
||||
dataTable: [...DEFAULT_OPERATIONS, 'readRow', 'writeRow', 'listProject'] as const,
|
||||
execution: ['delete', 'read', 'retry', 'list', 'get', 'reveal'] as const,
|
||||
workflowTags: ['update', 'list'] as const,
|
||||
role: ['manage'] as const,
|
||||
mcp: ['manage', 'oauth'] as const,
|
||||
mcpApiKey: ['create', 'rotate'] as const,
|
||||
chatHub: ['manage', 'message'] as const,
|
||||
chatHubAgent: [...DEFAULT_OPERATIONS] as const,
|
||||
breakingChanges: ['list'] as const,
|
||||
apiKey: ['manage'] as const,
|
||||
credentialResolver: [...DEFAULT_OPERATIONS] as const,
|
||||
} as const;
|
||||
|
||||
export const API_KEY_RESOURCES = {
|
||||
tag: [...DEFAULT_OPERATIONS] as const,
|
||||
workflow: [...DEFAULT_OPERATIONS, 'move', 'activate', 'deactivate'] as const,
|
||||
variable: ['create', 'update', 'delete', 'list'] as const,
|
||||
securityAudit: ['generate'] as const,
|
||||
project: ['create', 'update', 'delete', 'list'] as const,
|
||||
user: ['read', 'list', 'create', 'changeRole', 'delete', 'enforceMfa'] as const,
|
||||
execution: ['delete', 'read', 'retry', 'list', 'get', 'stop'] as const,
|
||||
credential: ['create', 'update', 'move', 'delete', 'list'] as const,
|
||||
sourceControl: ['pull'] as const,
|
||||
workflowTags: ['update', 'list'] as const,
|
||||
executionTags: ['update', 'list'] as const,
|
||||
dataTable: ['create', 'read', 'update', 'delete', 'list'] as const,
|
||||
dataTableRow: ['create', 'read', 'update', 'delete', 'upsert'] as const,
|
||||
} as const;
|
||||
|
||||
export const PROJECT_OWNER_ROLE_SLUG = 'project:personalOwner';
|
||||
export const PROJECT_ADMIN_ROLE_SLUG = 'project:admin';
|
||||
export const PROJECT_EDITOR_ROLE_SLUG = 'project:editor';
|
||||
export const PROJECT_VIEWER_ROLE_SLUG = 'project:viewer';
|
||||
export const PROJECT_CHAT_USER_ROLE_SLUG = 'project:chatUser';
|
||||
export const PERSONAL_SPACE_PUBLISHING_SETTING = {
|
||||
key: 'security.personalSpacePublishing',
|
||||
scopes: ['workflow:publish'],
|
||||
};
|
||||
export const PERSONAL_SPACE_SHARING_SETTING = {
|
||||
key: 'security.personalSpaceSharing',
|
||||
scopes: ['workflow:share', 'credential:share'],
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
export * from './types.ee';
|
||||
export * from './constants.ee';
|
||||
|
||||
export * from './roles/scopes/global-scopes.ee';
|
||||
export * from './scope-information';
|
||||
export * from './roles/role-maps.ee';
|
||||
export * from './roles/all-roles';
|
||||
|
||||
export {
|
||||
systemProjectRoleSchema,
|
||||
assignableProjectRoleSchema,
|
||||
assignableGlobalRoleSchema,
|
||||
projectRoleSchema,
|
||||
teamRoleSchema,
|
||||
roleSchema,
|
||||
type Role,
|
||||
scopeSchema,
|
||||
} from './schemas.ee';
|
||||
|
||||
export { hasScope } from './utilities/has-scope.ee';
|
||||
export { hasGlobalScope } from './utilities/has-global-scope.ee';
|
||||
export { combineScopes } from './utilities/combine-scopes.ee';
|
||||
export { staticRolesWithScope } from './utilities/static-roles-with-scope.ee';
|
||||
export { getGlobalScopes } from './utilities/get-global-scopes.ee';
|
||||
export { getRoleScopes, getAuthPrincipalScopes } from './utilities/get-role-scopes.ee';
|
||||
export { getResourcePermissions } from './utilities/get-resource-permissions.ee';
|
||||
export type { PermissionsRecord } from './utilities/get-resource-permissions.ee';
|
||||
export * from './public-api-permissions.ee';
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
isApiKeyScope,
|
||||
type ApiKeyScope,
|
||||
type AuthPrincipal,
|
||||
type GlobalRole,
|
||||
type Scope,
|
||||
} from './types.ee';
|
||||
|
||||
export const OWNER_API_KEY_SCOPES: ApiKeyScope[] = [
|
||||
'user:read',
|
||||
'user:list',
|
||||
'user:create',
|
||||
'user:changeRole',
|
||||
'user:delete',
|
||||
'user:enforceMfa',
|
||||
'sourceControl:pull',
|
||||
'securityAudit:generate',
|
||||
'project:create',
|
||||
'project:update',
|
||||
'project:delete',
|
||||
'project:list',
|
||||
'variable:create',
|
||||
'variable:delete',
|
||||
'variable:list',
|
||||
'variable:update',
|
||||
'tag:create',
|
||||
'tag:read',
|
||||
'tag:update',
|
||||
'tag:delete',
|
||||
'tag:list',
|
||||
'workflowTags:update',
|
||||
'workflowTags:list',
|
||||
'executionTags:update',
|
||||
'executionTags:list',
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:move',
|
||||
'workflow:activate',
|
||||
'workflow:deactivate',
|
||||
'execution:delete',
|
||||
'execution:read',
|
||||
'execution:retry',
|
||||
'execution:stop',
|
||||
'execution:list',
|
||||
'credential:create',
|
||||
'credential:update',
|
||||
'credential:move',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'dataTable:create',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:delete',
|
||||
'dataTable:list',
|
||||
'dataTableRow:create',
|
||||
'dataTableRow:read',
|
||||
'dataTableRow:update',
|
||||
'dataTableRow:delete',
|
||||
'dataTableRow:upsert',
|
||||
];
|
||||
|
||||
export const ADMIN_API_KEY_SCOPES: ApiKeyScope[] = OWNER_API_KEY_SCOPES;
|
||||
|
||||
export const MEMBER_API_KEY_SCOPES: ApiKeyScope[] = [
|
||||
'tag:create',
|
||||
'tag:read',
|
||||
'tag:update',
|
||||
'tag:list',
|
||||
'workflowTags:update',
|
||||
'workflowTags:list',
|
||||
'executionTags:update',
|
||||
'executionTags:list',
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:move',
|
||||
'workflow:activate',
|
||||
'workflow:deactivate',
|
||||
'execution:delete',
|
||||
'execution:read',
|
||||
'execution:retry',
|
||||
'execution:stop',
|
||||
'execution:list',
|
||||
'credential:create',
|
||||
'credential:update',
|
||||
'credential:move',
|
||||
'credential:delete',
|
||||
'dataTable:create',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:delete',
|
||||
'dataTable:list',
|
||||
'dataTableRow:create',
|
||||
'dataTableRow:read',
|
||||
'dataTableRow:update',
|
||||
'dataTableRow:delete',
|
||||
'dataTableRow:upsert',
|
||||
];
|
||||
|
||||
export const CHAT_USER_API_KEY_SCOPES: ApiKeyScope[] = [];
|
||||
|
||||
/**
|
||||
* This is a bit of a mess, because we are handing out scopes in API keys that are only
|
||||
* valid for the personal project, which is enforced in the public API, because the workflows,
|
||||
* execution endpoints are limited to the personal project.
|
||||
* This is a temporary solution until we have a better way to handle personal projects and API key scopes!
|
||||
*/
|
||||
export const API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT: ApiKeyScope[] = [
|
||||
'workflowTags:update',
|
||||
'workflowTags:list',
|
||||
'executionTags:update',
|
||||
'executionTags:list',
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:move',
|
||||
'workflow:activate',
|
||||
'workflow:deactivate',
|
||||
'execution:delete',
|
||||
'execution:read',
|
||||
'execution:retry',
|
||||
'execution:stop',
|
||||
'execution:list',
|
||||
'credential:create',
|
||||
'credential:update',
|
||||
'credential:move',
|
||||
'credential:delete',
|
||||
'dataTable:create',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:delete',
|
||||
'dataTable:list',
|
||||
'dataTableRow:create',
|
||||
'dataTableRow:read',
|
||||
'dataTableRow:update',
|
||||
'dataTableRow:delete',
|
||||
'dataTableRow:upsert',
|
||||
];
|
||||
|
||||
const MAP_ROLE_SCOPES: Record<GlobalRole, ApiKeyScope[]> = {
|
||||
'global:owner': OWNER_API_KEY_SCOPES,
|
||||
'global:admin': ADMIN_API_KEY_SCOPES,
|
||||
'global:member': MEMBER_API_KEY_SCOPES,
|
||||
'global:chatUser': CHAT_USER_API_KEY_SCOPES,
|
||||
};
|
||||
|
||||
export const getApiKeyScopesForRole = (user: AuthPrincipal) => {
|
||||
if (user.role.slug === 'global:chatUser') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
(user.role.scopes.map((scope) => scope.slug) as Array<Scope | ApiKeyScope>)
|
||||
.concat(API_KEY_SCOPES_FOR_IMPLICIT_PERSONAL_PROJECT)
|
||||
.filter(isApiKeyScope),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export const getOwnerOnlyApiKeyScopes = () => {
|
||||
const ownerScopes = new Set<ApiKeyScope>(MAP_ROLE_SCOPES['global:owner']);
|
||||
const memberScopes = new Set<ApiKeyScope>(MAP_ROLE_SCOPES['global:member']);
|
||||
memberScopes.forEach((item) => ownerScopes.delete(item));
|
||||
return Array.from(ownerScopes);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import {
|
||||
PROJECT_ADMIN_ROLE_SLUG,
|
||||
PROJECT_EDITOR_ROLE_SLUG,
|
||||
PROJECT_OWNER_ROLE_SLUG,
|
||||
PROJECT_VIEWER_ROLE_SLUG,
|
||||
PROJECT_CHAT_USER_ROLE_SLUG,
|
||||
} from '../constants.ee';
|
||||
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/get-role-scopes.ee';
|
||||
|
||||
const ROLE_NAMES: Record<AllRoleTypes, string> = {
|
||||
'global:owner': 'Owner',
|
||||
'global:admin': 'Admin',
|
||||
'global:member': 'Member',
|
||||
'global:chatUser': 'Chat User',
|
||||
[PROJECT_OWNER_ROLE_SLUG]: 'Project Owner',
|
||||
[PROJECT_ADMIN_ROLE_SLUG]: 'Project Admin',
|
||||
[PROJECT_EDITOR_ROLE_SLUG]: 'Project Editor',
|
||||
[PROJECT_VIEWER_ROLE_SLUG]: 'Project Viewer',
|
||||
[PROJECT_CHAT_USER_ROLE_SLUG]: 'Project Chat User',
|
||||
'credential:user': 'Credential User',
|
||||
'credential:owner': 'Credential Owner',
|
||||
'workflow:owner': 'Workflow Owner',
|
||||
'workflow:editor': 'Workflow Editor',
|
||||
};
|
||||
|
||||
const ROLE_DESCRIPTIONS: Record<AllRoleTypes, string> = {
|
||||
'global:owner': 'Owner',
|
||||
'global:admin': 'Admin',
|
||||
'global:member': 'Member',
|
||||
'global:chatUser': 'Chat User',
|
||||
[PROJECT_OWNER_ROLE_SLUG]: 'Project Owner',
|
||||
[PROJECT_ADMIN_ROLE_SLUG]:
|
||||
'Full control of settings, members, workflows, credentials and executions',
|
||||
[PROJECT_EDITOR_ROLE_SLUG]: 'Create, edit, and delete workflows, credentials, and executions',
|
||||
[PROJECT_VIEWER_ROLE_SLUG]: 'Read-only access to workflows, credentials, and executions',
|
||||
[PROJECT_CHAT_USER_ROLE_SLUG]:
|
||||
'Chat-only access to chatting with workflows that have n8n Chat enabled',
|
||||
'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[]>,
|
||||
roleType: 'global' | 'project' | 'credential' | 'workflow',
|
||||
) =>
|
||||
(Object.keys(roles) as T[]).map((role) => ({
|
||||
slug: role,
|
||||
displayName: ROLE_NAMES[role],
|
||||
scopes: getRoleScopes(role),
|
||||
description: ROLE_DESCRIPTIONS[role],
|
||||
licensed: false,
|
||||
systemRole: true,
|
||||
roleType,
|
||||
}));
|
||||
|
||||
export const ALL_ROLES: AllRolesMap = Object.freeze({
|
||||
global: mapToRoleObject(GLOBAL_SCOPE_MAP, 'global'),
|
||||
project: mapToRoleObject(PROJECT_SCOPE_MAP, 'project'),
|
||||
credential: mapToRoleObject(CREDENTIALS_SHARING_SCOPE_MAP, 'credential'),
|
||||
workflow: mapToRoleObject(WORKFLOW_SHARING_SCOPE_MAP, 'workflow'),
|
||||
});
|
||||
|
||||
export const isBuiltInRole = (role: string): role is AllRoleTypes => {
|
||||
return Object.prototype.hasOwnProperty.call(ROLE_NAMES, role);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import type {
|
||||
CredentialSharingRole,
|
||||
GlobalRole,
|
||||
ProjectRole,
|
||||
Scope,
|
||||
WorkflowSharingRole,
|
||||
} from '../types.ee';
|
||||
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,
|
||||
GLOBAL_CHAT_USER_SCOPES,
|
||||
} from './scopes/global-scopes.ee';
|
||||
import {
|
||||
REGULAR_PROJECT_ADMIN_SCOPES,
|
||||
PERSONAL_PROJECT_OWNER_SCOPES,
|
||||
PROJECT_EDITOR_SCOPES,
|
||||
PROJECT_VIEWER_SCOPES,
|
||||
PROJECT_CHAT_USER_SCOPES,
|
||||
} from './scopes/project-scopes.ee';
|
||||
import {
|
||||
WORKFLOW_SHARING_OWNER_SCOPES,
|
||||
WORKFLOW_SHARING_EDITOR_SCOPES,
|
||||
} from './scopes/workflow-sharing-scopes.ee';
|
||||
|
||||
export const GLOBAL_SCOPE_MAP: Record<GlobalRole, Scope[]> = {
|
||||
'global:owner': GLOBAL_OWNER_SCOPES,
|
||||
'global:admin': GLOBAL_ADMIN_SCOPES,
|
||||
'global:member': GLOBAL_MEMBER_SCOPES,
|
||||
'global:chatUser': GLOBAL_CHAT_USER_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,
|
||||
'project:chatUser': PROJECT_CHAT_USER_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,12 @@
|
||||
import type { Scope } from '../../types.ee';
|
||||
|
||||
export const CREDENTIALS_SHARING_OWNER_SCOPES: Scope[] = [
|
||||
'credential:read',
|
||||
'credential:update',
|
||||
'credential:delete',
|
||||
'credential:share',
|
||||
'credential:unshare',
|
||||
'credential:move',
|
||||
];
|
||||
|
||||
export const CREDENTIALS_SHARING_USER_SCOPES: Scope[] = ['credential:read'];
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { Scope } from '../../types.ee';
|
||||
|
||||
export const GLOBAL_OWNER_SCOPES: Scope[] = [
|
||||
'aiAssistant:manage',
|
||||
'annotationTag:create',
|
||||
'annotationTag:read',
|
||||
'annotationTag:update',
|
||||
'annotationTag:delete',
|
||||
'annotationTag:list',
|
||||
'auditLogs:manage',
|
||||
'banner:dismiss',
|
||||
'credential:create',
|
||||
'credential:read',
|
||||
'credential:update',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:share',
|
||||
'credential:unshare',
|
||||
'credential:shareGlobally',
|
||||
'credential:move',
|
||||
'community:register',
|
||||
'communityPackage:install',
|
||||
'communityPackage:uninstall',
|
||||
'communityPackage:update',
|
||||
'communityPackage:list',
|
||||
'eventBusDestination:create',
|
||||
'eventBusDestination:read',
|
||||
'eventBusDestination:update',
|
||||
'eventBusDestination:delete',
|
||||
'eventBusDestination:list',
|
||||
'eventBusDestination:test',
|
||||
'externalSecretsProvider:create',
|
||||
'externalSecretsProvider:read',
|
||||
'externalSecretsProvider:update',
|
||||
'externalSecretsProvider:delete',
|
||||
'externalSecretsProvider:list',
|
||||
'externalSecretsProvider:sync',
|
||||
'externalSecret:list',
|
||||
'ldap:manage',
|
||||
'ldap:sync',
|
||||
'license:manage',
|
||||
'logStreaming:manage',
|
||||
'orchestration:read',
|
||||
'saml:manage',
|
||||
'securityAudit:generate',
|
||||
'securitySettings:manage',
|
||||
'sourceControl:pull',
|
||||
'sourceControl:push',
|
||||
'sourceControl:manage',
|
||||
'tag:create',
|
||||
'tag:read',
|
||||
'tag:update',
|
||||
'tag:delete',
|
||||
'tag:list',
|
||||
'user:create',
|
||||
'user:read',
|
||||
'user:update',
|
||||
'user:delete',
|
||||
'user:list',
|
||||
'user:resetPassword',
|
||||
'user:generateInviteLink',
|
||||
'user:changeRole',
|
||||
'user:enforceMfa',
|
||||
'variable:create',
|
||||
'variable:read',
|
||||
'variable:update',
|
||||
'variable:delete',
|
||||
'variable:list',
|
||||
'projectVariable:create',
|
||||
'projectVariable:read',
|
||||
'projectVariable:update',
|
||||
'projectVariable:delete',
|
||||
'projectVariable:list',
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:publish',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:share',
|
||||
'workflow:unshare',
|
||||
'workflow:execute',
|
||||
'workflow:execute-chat',
|
||||
'workflow:move',
|
||||
'workflow:updateRedactionSetting',
|
||||
'workersView:manage',
|
||||
'project:list',
|
||||
'project:create',
|
||||
'project:read',
|
||||
'project:update',
|
||||
'project:delete',
|
||||
'insights:list',
|
||||
'folder:move',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
'folder:delete',
|
||||
'folder:create',
|
||||
'folder:list',
|
||||
'oidc:manage',
|
||||
'provisioning:manage',
|
||||
'dataTable:create',
|
||||
'dataTable:delete',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:list',
|
||||
'dataTable:listProject',
|
||||
'dataTable:readRow',
|
||||
'dataTable:writeRow',
|
||||
'role:manage',
|
||||
'mcp:manage',
|
||||
'mcp:oauth',
|
||||
'mcpApiKey:create',
|
||||
'mcpApiKey:rotate',
|
||||
'chatHub:manage',
|
||||
'chatHub:message',
|
||||
'chatHubAgent:create',
|
||||
'chatHubAgent:read',
|
||||
'chatHubAgent:update',
|
||||
'chatHubAgent:delete',
|
||||
'chatHubAgent:list',
|
||||
'breakingChanges:list',
|
||||
'execution:reveal',
|
||||
'apiKey:manage',
|
||||
'credentialResolver:create',
|
||||
'credentialResolver:read',
|
||||
'credentialResolver:update',
|
||||
'credentialResolver:delete',
|
||||
'credentialResolver:list',
|
||||
];
|
||||
|
||||
export const GLOBAL_ADMIN_SCOPES = GLOBAL_OWNER_SCOPES.concat();
|
||||
|
||||
export const GLOBAL_MEMBER_SCOPES: Scope[] = [
|
||||
'annotationTag:create',
|
||||
'annotationTag:read',
|
||||
'annotationTag:update',
|
||||
'annotationTag:delete',
|
||||
'annotationTag:list',
|
||||
'eventBusDestination:list',
|
||||
'eventBusDestination:test',
|
||||
'tag:create',
|
||||
'tag:read',
|
||||
'tag:update',
|
||||
'tag:list',
|
||||
'user:list',
|
||||
'variable:list',
|
||||
'variable:read',
|
||||
'dataTable:list',
|
||||
'mcp:oauth',
|
||||
'mcpApiKey:create',
|
||||
'mcpApiKey:rotate',
|
||||
'chatHub:message',
|
||||
'chatHubAgent:create',
|
||||
'chatHubAgent:read',
|
||||
'chatHubAgent:update',
|
||||
'chatHubAgent:delete',
|
||||
'chatHubAgent:list',
|
||||
'apiKey:manage',
|
||||
];
|
||||
|
||||
export const GLOBAL_CHAT_USER_SCOPES: Scope[] = [
|
||||
'chatHub:message',
|
||||
'chatHubAgent:create',
|
||||
'chatHubAgent:read',
|
||||
'chatHubAgent:update',
|
||||
'chatHubAgent:delete',
|
||||
'chatHubAgent:list',
|
||||
];
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { Scope } from '../../types.ee';
|
||||
|
||||
/**
|
||||
* Diff between admin in personal project and admin in other projects:
|
||||
* - You cannot rename your personal project.
|
||||
* - You cannot invite people to your personal project.
|
||||
*/
|
||||
|
||||
export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:publish',
|
||||
'workflow:unpublish',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:execute',
|
||||
'workflow:execute-chat',
|
||||
'workflow:move',
|
||||
'workflow:updateRedactionSetting',
|
||||
'credential:create',
|
||||
'credential:read',
|
||||
'credential:update',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:share',
|
||||
'credential:unshare',
|
||||
'execution:reveal',
|
||||
'project:list',
|
||||
'project:read',
|
||||
'project:update',
|
||||
'project:delete',
|
||||
'folder:create',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
'folder:delete',
|
||||
'folder:list',
|
||||
'folder:move',
|
||||
'sourceControl:push',
|
||||
'dataTable:create',
|
||||
'dataTable:delete',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:listProject',
|
||||
'dataTable:readRow',
|
||||
'dataTable:writeRow',
|
||||
'projectVariable:list',
|
||||
'projectVariable:read',
|
||||
'projectVariable:create',
|
||||
'projectVariable:update',
|
||||
'projectVariable:delete',
|
||||
];
|
||||
|
||||
export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:execute',
|
||||
'workflow:execute-chat',
|
||||
'workflow:move',
|
||||
'workflow:unpublish',
|
||||
'workflow:unshare',
|
||||
'workflow:updateRedactionSetting',
|
||||
'credential:create',
|
||||
'credential:read',
|
||||
'credential:update',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:unshare',
|
||||
'execution:reveal',
|
||||
'project:list',
|
||||
'project:read',
|
||||
'folder:create',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
'folder:delete',
|
||||
'folder:list',
|
||||
'folder:move',
|
||||
'dataTable:create',
|
||||
'dataTable:delete',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:listProject',
|
||||
'dataTable:readRow',
|
||||
'dataTable:writeRow',
|
||||
];
|
||||
|
||||
export const PROJECT_EDITOR_SCOPES: Scope[] = [
|
||||
'workflow:create',
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:publish',
|
||||
'workflow:unpublish',
|
||||
'workflow:delete',
|
||||
'workflow:list',
|
||||
'workflow:execute',
|
||||
'workflow:execute-chat',
|
||||
'credential:create',
|
||||
'credential:read',
|
||||
'credential:update',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'project:list',
|
||||
'project:read',
|
||||
'folder:create',
|
||||
'folder:read',
|
||||
'folder:update',
|
||||
'folder:delete',
|
||||
'folder:list',
|
||||
'dataTable:create',
|
||||
'dataTable:delete',
|
||||
'dataTable:read',
|
||||
'dataTable:update',
|
||||
'dataTable:listProject',
|
||||
'dataTable:readRow',
|
||||
'dataTable:writeRow',
|
||||
'projectVariable:list',
|
||||
'projectVariable:read',
|
||||
'projectVariable:create',
|
||||
'projectVariable:update',
|
||||
'projectVariable:delete',
|
||||
];
|
||||
|
||||
export const PROJECT_VIEWER_SCOPES: Scope[] = [
|
||||
'credential:list',
|
||||
'credential:read',
|
||||
'project:list',
|
||||
'project:read',
|
||||
'workflow:list',
|
||||
'workflow:read',
|
||||
'workflow:execute-chat',
|
||||
'folder:read',
|
||||
'folder:list',
|
||||
'dataTable:listProject',
|
||||
'dataTable:read',
|
||||
'dataTable:readRow',
|
||||
'projectVariable:list',
|
||||
'projectVariable:read',
|
||||
];
|
||||
|
||||
export const PROJECT_CHAT_USER_SCOPES: Scope[] = ['workflow:execute-chat'];
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Scope } from '../../types.ee';
|
||||
|
||||
export const WORKFLOW_SHARING_OWNER_SCOPES: Scope[] = [
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:publish',
|
||||
'workflow:unpublish',
|
||||
'workflow:delete',
|
||||
'workflow:execute',
|
||||
'workflow:share',
|
||||
'workflow:unshare',
|
||||
'workflow:move',
|
||||
'workflow:execute-chat',
|
||||
];
|
||||
|
||||
export const WORKFLOW_SHARING_EDITOR_SCOPES: Scope[] = [
|
||||
'workflow:read',
|
||||
'workflow:update',
|
||||
'workflow:publish',
|
||||
'workflow:unpublish',
|
||||
'workflow:execute',
|
||||
'workflow:execute-chat',
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ALL_SCOPES } from './scope-information';
|
||||
|
||||
export const roleNamespaceSchema = z.enum(['global', 'project', 'credential', 'workflow']);
|
||||
|
||||
export const globalRoleSchema = z.enum([
|
||||
'global:owner',
|
||||
'global:admin',
|
||||
'global:member',
|
||||
'global:chatUser',
|
||||
]);
|
||||
|
||||
const customGlobalRoleSchema = z
|
||||
.string()
|
||||
.nonempty()
|
||||
.refine((val) => !globalRoleSchema.safeParse(val).success, {
|
||||
message: 'This global role value is not assignable',
|
||||
});
|
||||
|
||||
export const assignableGlobalRoleSchema = z.union([
|
||||
globalRoleSchema.exclude([
|
||||
'global:owner', // Owner cannot be changed
|
||||
]),
|
||||
customGlobalRoleSchema,
|
||||
]);
|
||||
|
||||
export const personalRoleSchema = z.enum([
|
||||
'project:personalOwner', // personalOwner is only used for personal projects
|
||||
]);
|
||||
|
||||
// Those are the system roles for projects assignable to a user
|
||||
export const teamRoleSchema = z.enum([
|
||||
'project:admin',
|
||||
'project:editor',
|
||||
'project:viewer',
|
||||
'project:chatUser',
|
||||
]);
|
||||
|
||||
// Custom project role can be anything but the system roles
|
||||
export const customProjectRoleSchema = z
|
||||
.string()
|
||||
.nonempty()
|
||||
.refine((val) => !systemProjectRoleSchema.safeParse(val).success, {
|
||||
message: 'This global role value is not assignable',
|
||||
});
|
||||
|
||||
// Those are all the system roles for projects
|
||||
export const systemProjectRoleSchema = z.union([personalRoleSchema, teamRoleSchema]);
|
||||
|
||||
// Those are the roles that can be assigned to a user for a project (all roles except personalOwner)
|
||||
export const assignableProjectRoleSchema = z.union([teamRoleSchema, customProjectRoleSchema]);
|
||||
|
||||
export const projectRoleSchema = z.union([systemProjectRoleSchema, customProjectRoleSchema]);
|
||||
|
||||
export const credentialSharingRoleSchema = z.enum(['credential:owner', 'credential:user']);
|
||||
|
||||
export const workflowSharingRoleSchema = z.enum(['workflow:owner', 'workflow:editor']);
|
||||
|
||||
const ALL_SCOPES_LOOKUP_SET = new Set(ALL_SCOPES as string[]);
|
||||
|
||||
export const scopeSchema = z.string().refine((val) => ALL_SCOPES_LOOKUP_SET.has(val), {
|
||||
message: 'Invalid scope',
|
||||
});
|
||||
|
||||
export const roleSchema = z.object({
|
||||
slug: z.string().min(1),
|
||||
displayName: z.string().min(1),
|
||||
description: z.string().nullable(),
|
||||
systemRole: z.boolean(),
|
||||
roleType: roleNamespaceSchema,
|
||||
licensed: z.boolean(),
|
||||
scopes: z.array(scopeSchema),
|
||||
createdAt: z.date().optional(),
|
||||
updatedAt: z.date().optional(),
|
||||
usedByUsers: z.number().optional(),
|
||||
usedByProjects: z.number().optional(),
|
||||
});
|
||||
|
||||
export type Role = z.infer<typeof roleSchema>;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { API_KEY_RESOURCES, RESOURCES } from './constants.ee';
|
||||
import type { ApiKeyScope, Scope, ScopeInformation } from './types.ee';
|
||||
|
||||
function buildResourceScopes() {
|
||||
const resourceScopes = Object.entries(RESOURCES).flatMap(([resource, operations]) => [
|
||||
...operations.map((op) => `${resource}:${op}` as const),
|
||||
`${resource}:*` as const,
|
||||
]) as Scope[];
|
||||
|
||||
resourceScopes.push('*' as const); // Global wildcard
|
||||
return resourceScopes;
|
||||
}
|
||||
|
||||
function buildApiKeyScopes() {
|
||||
const apiKeyScopes = Object.entries(API_KEY_RESOURCES).flatMap(([resource, operations]) => [
|
||||
...operations.map((op) => `${resource}:${op}` as const),
|
||||
]) as ApiKeyScope[];
|
||||
|
||||
return new Set(apiKeyScopes);
|
||||
}
|
||||
|
||||
export const ALL_SCOPES = buildResourceScopes();
|
||||
|
||||
export const ALL_API_KEY_SCOPES = buildApiKeyScopes();
|
||||
|
||||
export const scopeInformation: Partial<Record<Scope, ScopeInformation>> = {
|
||||
'aiAssistant:manage': {
|
||||
displayName: 'Manage AI Usage',
|
||||
description: 'Allows managing AI Usage settings.',
|
||||
},
|
||||
'annotationTag:create': {
|
||||
displayName: 'Create Annotation Tag',
|
||||
description: 'Allows creating new annotation tags.',
|
||||
},
|
||||
'workflow:publish': {
|
||||
displayName: 'Publish Workflow',
|
||||
description: 'Allows publishing workflows.',
|
||||
},
|
||||
'workflow:unpublish': {
|
||||
displayName: 'Unpublish Workflow',
|
||||
description: 'Allows unpublishing workflows.',
|
||||
},
|
||||
'workflow:unshare': {
|
||||
displayName: 'Unshare Workflow',
|
||||
description: 'Allows removing workflow shares.',
|
||||
},
|
||||
'credential:unshare': {
|
||||
displayName: 'Unshare Credential',
|
||||
description: 'Allows removing credential shares.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { RESOURCES, API_KEY_RESOURCES } from './constants.ee';
|
||||
import type {
|
||||
assignableGlobalRoleSchema,
|
||||
credentialSharingRoleSchema,
|
||||
globalRoleSchema,
|
||||
Role,
|
||||
systemProjectRoleSchema,
|
||||
roleNamespaceSchema,
|
||||
teamRoleSchema,
|
||||
workflowSharingRoleSchema,
|
||||
assignableProjectRoleSchema,
|
||||
} from './schemas.ee';
|
||||
import { PROJECT_OWNER_ROLE_SLUG } from './constants.ee';
|
||||
import { ALL_API_KEY_SCOPES } from './scope-information';
|
||||
|
||||
export type ScopeInformation = {
|
||||
displayName: string;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
/** Represents a resource that can have permissions applied to it */
|
||||
export type Resource = keyof typeof RESOURCES;
|
||||
|
||||
/** A permission scope for a specific resource + operation combination */
|
||||
type ResourceScope<
|
||||
R extends Resource,
|
||||
Operation extends (typeof RESOURCES)[R][number] = (typeof RESOURCES)[R][number],
|
||||
> = `${R}:${Operation}`;
|
||||
|
||||
/** A wildcard scope applies to all operations on a resource or all resources */
|
||||
type WildcardScope = `${Resource}:*` | '*';
|
||||
|
||||
// This is purely an intermediary type.
|
||||
// If we tried to do use `ResourceScope<Resource>` directly we'd end
|
||||
// up with all resources having all scopes (e.g. `ldap:uninstall`).
|
||||
type AllScopesObject = {
|
||||
[R in Resource]: ResourceScope<R>;
|
||||
};
|
||||
|
||||
/** A permission scope in the system, either a specific resource:operation or a wildcard */
|
||||
export type Scope = AllScopesObject[Resource] | WildcardScope;
|
||||
|
||||
export type ScopeLevels = {
|
||||
global: Scope[];
|
||||
project?: Scope[];
|
||||
resource?: Scope[];
|
||||
};
|
||||
|
||||
export type MaskLevels = {
|
||||
sharing: Scope[];
|
||||
};
|
||||
|
||||
export type ScopeOptions = { mode: 'oneOf' | 'allOf' };
|
||||
|
||||
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 TeamProjectRole = z.infer<typeof teamRoleSchema>;
|
||||
export type ProjectRole = z.infer<typeof systemProjectRoleSchema>;
|
||||
export type AssignableProjectRole = z.infer<typeof assignableProjectRoleSchema>;
|
||||
|
||||
/**
|
||||
* Type guard for assignable project role slugs.
|
||||
*
|
||||
* Custom project roles are supported. We consider any slug that:
|
||||
* - starts with the `project:` prefix, and
|
||||
* - is not the personal owner role
|
||||
* to be an assignable project role.
|
||||
*/
|
||||
export function isAssignableProjectRoleSlug(slug: string): slug is AssignableProjectRole {
|
||||
return slug.startsWith('project:') && slug !== PROJECT_OWNER_ROLE_SLUG;
|
||||
}
|
||||
|
||||
/** Union of all possible role types in the system */
|
||||
export type AllRoleTypes = GlobalRole | ProjectRole | WorkflowSharingRole | CredentialSharingRole;
|
||||
|
||||
export type AllRolesMap = {
|
||||
global: Role[];
|
||||
project: Role[];
|
||||
credential: Role[];
|
||||
workflow: Role[];
|
||||
};
|
||||
|
||||
export type DbScope = {
|
||||
slug: Scope;
|
||||
};
|
||||
|
||||
export type DbRole = {
|
||||
slug: string;
|
||||
scopes: DbScope[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: DbRole;
|
||||
};
|
||||
|
||||
// #region Public API
|
||||
type PublicApiKeyResources = keyof typeof API_KEY_RESOURCES;
|
||||
|
||||
type ApiKeyResourceScope<
|
||||
R extends PublicApiKeyResources,
|
||||
Operation extends (typeof API_KEY_RESOURCES)[R][number] = (typeof API_KEY_RESOURCES)[R][number],
|
||||
> = `${R}:${Operation}`;
|
||||
|
||||
// This is purely an intermediary type.
|
||||
// If we tried to do use `ResourceScope<Resource>` directly we'd end
|
||||
// up with all resources having all scopes.
|
||||
type AllApiKeyScopesObject = {
|
||||
[R in PublicApiKeyResources]: ApiKeyResourceScope<R>;
|
||||
};
|
||||
|
||||
export type ApiKeyScope = AllApiKeyScopesObject[PublicApiKeyResources];
|
||||
|
||||
export function isApiKeyScope(scope: Scope | ApiKeyScope): scope is ApiKeyScope {
|
||||
// We are casting with as for runtime type checking
|
||||
return ALL_API_KEY_SCOPES.has(scope as ApiKeyScope);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Scope, ScopeLevels, MaskLevels } from '../../types.ee';
|
||||
import { combineScopes } from '../combine-scopes.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 { getGlobalScopes } from '../get-global-scopes.ee';
|
||||
import { createAuthPrincipal } from './utils';
|
||||
|
||||
describe('getGlobalScopes', () => {
|
||||
test.each(['global:owner', 'global:admin', 'global:member', 'global:chatUser'] as const)(
|
||||
'should return correct scopes for %s',
|
||||
(role) => {
|
||||
const scopes = getGlobalScopes(createAuthPrincipal(role));
|
||||
|
||||
expect(scopes).toEqual(GLOBAL_SCOPE_MAP[role]);
|
||||
},
|
||||
);
|
||||
|
||||
test('should return empty array for non-existent role', () => {
|
||||
const scopes = getGlobalScopes(createAuthPrincipal('non:existent'));
|
||||
|
||||
expect(scopes).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { Scope } from '@/types.ee';
|
||||
|
||||
import type { PermissionsRecord } from '../get-resource-permissions.ee';
|
||||
import { getResourcePermissions } from '../get-resource-permissions.ee';
|
||||
|
||||
describe('permissions', () => {
|
||||
it('getResourcePermissions for empty scopes', () => {
|
||||
expect(getResourcePermissions()).toEqual({
|
||||
aiAssistant: {},
|
||||
annotationTag: {},
|
||||
auditLogs: {},
|
||||
banner: {},
|
||||
community: {},
|
||||
communityPackage: {},
|
||||
credential: {},
|
||||
externalSecretsProvider: {},
|
||||
externalSecret: {},
|
||||
eventBusDestination: {},
|
||||
execution: {},
|
||||
ldap: {},
|
||||
license: {},
|
||||
logStreaming: {},
|
||||
oidc: {},
|
||||
orchestration: {},
|
||||
project: {},
|
||||
saml: {},
|
||||
provisioning: {},
|
||||
securityAudit: {},
|
||||
securitySettings: {},
|
||||
sourceControl: {},
|
||||
tag: {},
|
||||
user: {},
|
||||
variable: {},
|
||||
projectVariable: {},
|
||||
workersView: {},
|
||||
workflow: {},
|
||||
workflowTags: {},
|
||||
folder: {},
|
||||
insights: {},
|
||||
dataTable: {},
|
||||
mcp: {},
|
||||
mcpApiKey: {},
|
||||
role: {},
|
||||
chatHub: {},
|
||||
chatHubAgent: {},
|
||||
breakingChanges: {},
|
||||
apiKey: {},
|
||||
credentialResolver: {},
|
||||
});
|
||||
});
|
||||
it('getResourcePermissions', () => {
|
||||
const scopes: Scope[] = [
|
||||
'credential:create',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:read',
|
||||
'credential:share',
|
||||
'credential:update',
|
||||
'eventBusDestination:list',
|
||||
'eventBusDestination:test',
|
||||
'project:list',
|
||||
'project:read',
|
||||
'tag:create',
|
||||
'tag:list',
|
||||
'tag:read',
|
||||
'tag:update',
|
||||
'user:list',
|
||||
'variable:list',
|
||||
'variable:read',
|
||||
'projectVariable:list',
|
||||
'projectVariable:read',
|
||||
'workflow:create',
|
||||
'workflow:delete',
|
||||
'workflow:execute',
|
||||
'workflow:list',
|
||||
'workflow:move',
|
||||
'workflow:read',
|
||||
'workflow:share',
|
||||
'workflow:update',
|
||||
'folder:create',
|
||||
'insights:list',
|
||||
'breakingChanges:list',
|
||||
'apiKey:manage',
|
||||
];
|
||||
|
||||
const permissionRecord: PermissionsRecord = {
|
||||
aiAssistant: {},
|
||||
annotationTag: {},
|
||||
auditLogs: {},
|
||||
banner: {},
|
||||
community: {},
|
||||
communityPackage: {},
|
||||
credential: {
|
||||
create: true,
|
||||
delete: true,
|
||||
list: true,
|
||||
move: true,
|
||||
read: true,
|
||||
share: true,
|
||||
update: true,
|
||||
},
|
||||
eventBusDestination: {
|
||||
list: true,
|
||||
test: true,
|
||||
},
|
||||
externalSecret: {},
|
||||
externalSecretsProvider: {},
|
||||
ldap: {},
|
||||
license: {},
|
||||
logStreaming: {},
|
||||
orchestration: {},
|
||||
project: {
|
||||
list: true,
|
||||
read: true,
|
||||
},
|
||||
saml: {},
|
||||
oidc: {},
|
||||
provisioning: {},
|
||||
mcp: {},
|
||||
mcpApiKey: {},
|
||||
securityAudit: {},
|
||||
securitySettings: {},
|
||||
sourceControl: {},
|
||||
tag: {
|
||||
create: true,
|
||||
list: true,
|
||||
read: true,
|
||||
update: true,
|
||||
},
|
||||
user: {
|
||||
list: true,
|
||||
},
|
||||
variable: {
|
||||
list: true,
|
||||
read: true,
|
||||
},
|
||||
projectVariable: {
|
||||
list: true,
|
||||
read: true,
|
||||
},
|
||||
workersView: {},
|
||||
workflow: {
|
||||
create: true,
|
||||
delete: true,
|
||||
execute: true,
|
||||
list: true,
|
||||
move: true,
|
||||
read: true,
|
||||
share: true,
|
||||
update: true,
|
||||
},
|
||||
folder: {
|
||||
create: true,
|
||||
},
|
||||
insights: {
|
||||
list: true,
|
||||
},
|
||||
dataTable: {},
|
||||
execution: {},
|
||||
workflowTags: {},
|
||||
role: {},
|
||||
chatHub: {},
|
||||
chatHubAgent: {},
|
||||
breakingChanges: {
|
||||
list: true,
|
||||
},
|
||||
apiKey: {
|
||||
manage: true,
|
||||
},
|
||||
credentialResolver: {},
|
||||
};
|
||||
|
||||
expect(getResourcePermissions(scopes)).toEqual(permissionRecord);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { AllRoleTypes, Resource } from '../../types.ee';
|
||||
import { getRoleScopes, COMBINED_ROLE_MAP } from '../get-role-scopes.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,52 @@
|
||||
import type { GlobalRole, Scope } from '../../types.ee';
|
||||
import { hasGlobalScope } from '../has-global-scope.ee';
|
||||
import { createAuthPrincipal } from './utils';
|
||||
|
||||
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: 'global:chatUser', 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(createAuthPrincipal(role), scope)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('multiple scopes', () => {
|
||||
test('oneOf mode (default)', () => {
|
||||
expect(
|
||||
hasGlobalScope(createAuthPrincipal('global:member'), [
|
||||
'tag:create',
|
||||
'user:list',
|
||||
// a member cannot create users
|
||||
'user:create',
|
||||
]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('allOf mode', () => {
|
||||
expect(
|
||||
hasGlobalScope(
|
||||
createAuthPrincipal('global:member'),
|
||||
[
|
||||
'tag:create',
|
||||
'user:list',
|
||||
// a member cannot create users
|
||||
'user:create',
|
||||
],
|
||||
{ mode: 'allOf' },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('edge cases', () => {
|
||||
expect(hasGlobalScope(createAuthPrincipal('global:owner'), [])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Scope, ScopeLevels } from '../../types.ee';
|
||||
import { hasScope } from '../has-scope.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,28 @@
|
||||
import type { GlobalRole, Scope } from '../../types.ee';
|
||||
import { staticRolesWithScope } from '../static-roles-with-scope.ee';
|
||||
|
||||
describe('rolesWithScope', () => {
|
||||
describe('global roles', () => {
|
||||
test.each([
|
||||
['workflow:create', ['global:owner', 'global:admin']],
|
||||
['user:list', ['global:owner', 'global:admin', 'global:member']],
|
||||
['chatHub:message', ['global:owner', 'global:admin', 'global:member', 'global:chatUser']],
|
||||
['invalid:scope', []],
|
||||
] as Array<[Scope, GlobalRole[]]>)('%s -> %s', (scope, expected) => {
|
||||
expect(staticRolesWithScope('global', scope)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple scopes', () => {
|
||||
test('returns roles with all scopes', () => {
|
||||
expect(
|
||||
staticRolesWithScope('global', [
|
||||
// all global roles have this scope
|
||||
'tag:create',
|
||||
// only owner and admin have this scope
|
||||
'user:delete',
|
||||
]),
|
||||
).toEqual(['global:owner', 'global:admin']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { GLOBAL_SCOPE_MAP } from '@/roles/role-maps.ee';
|
||||
import { globalRoleSchema } from '@/schemas.ee';
|
||||
import type { AuthPrincipal, GlobalRole, Scope } from '@/types.ee';
|
||||
|
||||
function createBuildInAuthPrincipal(role: GlobalRole): AuthPrincipal {
|
||||
return {
|
||||
role: {
|
||||
slug: role,
|
||||
scopes:
|
||||
GLOBAL_SCOPE_MAP[role].map((scope) => {
|
||||
return {
|
||||
slug: scope,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createAuthPrincipal(role: string, scopes: Scope[] = []): AuthPrincipal {
|
||||
try {
|
||||
const isGlobalRole = globalRoleSchema.parse(role);
|
||||
if (isGlobalRole) {
|
||||
return createBuildInAuthPrincipal(isGlobalRole);
|
||||
}
|
||||
} catch (error) {
|
||||
// If the role is not a valid global role, we proceed
|
||||
// to create a custom role with the provided scopes.
|
||||
}
|
||||
return {
|
||||
role: {
|
||||
slug: role,
|
||||
scopes:
|
||||
scopes.map((scope) => {
|
||||
return {
|
||||
slug: scope,
|
||||
};
|
||||
}) || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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 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) =>
|
||||
principal.role.scopes.map((scope) => scope.slug) ?? [];
|
||||
@@ -0,0 +1,32 @@
|
||||
import { RESOURCES } from '../constants.ee';
|
||||
import type { Scope } from '../types.ee';
|
||||
|
||||
type ExtractScopePrefixSuffix<T> = T extends `${infer Prefix}:${infer Suffix}`
|
||||
? [Prefix, Suffix]
|
||||
: never;
|
||||
type ActionBooleans<T extends readonly string[]> = {
|
||||
[K in T[number]]?: boolean;
|
||||
};
|
||||
export type PermissionsRecord = {
|
||||
[K in keyof typeof RESOURCES]: ActionBooleans<(typeof RESOURCES)[K]>;
|
||||
};
|
||||
|
||||
export const getResourcePermissions = (resourceScopes: readonly Scope[] = []): PermissionsRecord =>
|
||||
Object.keys(RESOURCES).reduce(
|
||||
(permissions, key) => ({
|
||||
...permissions,
|
||||
[key]: resourceScopes.reduce((resourcePermissions, scope) => {
|
||||
const [prefix, suffix] = scope.split(':') as ExtractScopePrefixSuffix<Scope>;
|
||||
|
||||
if (prefix === key) {
|
||||
return {
|
||||
...resourcePermissions,
|
||||
[suffix]: true,
|
||||
};
|
||||
}
|
||||
|
||||
return resourcePermissions;
|
||||
}, {}),
|
||||
}),
|
||||
{} as PermissionsRecord,
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
|
||||
import type { AllRoleTypes, AuthPrincipal, 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
|
||||
*
|
||||
* @deprecated Use the 'getRoleScopes' from the AuthRolesService instead.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets scopes for an auth principal, optionally filtered by resource types.
|
||||
* @param user - The auth principal to search scopes for
|
||||
* @param filters - Optional resources to filter scopes by
|
||||
* @returns Array of matching scopes
|
||||
*/
|
||||
export function getAuthPrincipalScopes(user: AuthPrincipal, filters?: Resource[]): Scope[] {
|
||||
if (!user.role) {
|
||||
const e = new Error('AuthPrincipal does not have a role defined');
|
||||
console.error('AuthPrincipal does not have a role defined', e);
|
||||
throw e;
|
||||
}
|
||||
let scopes = user.role.scopes.map((s) => s.slug);
|
||||
if (filters) {
|
||||
scopes = scopes.filter((s) => filters.includes(s.split(':')[0] as Resource));
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { hasScope } from './has-scope.ee';
|
||||
import type { AuthPrincipal, Scope, ScopeOptions } from '../types.ee';
|
||||
import { getAuthPrincipalScopes } from './get-role-scopes.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 = getAuthPrincipalScopes(principal);
|
||||
return hasScope(scope, { global }, undefined, scopeOptions);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { combineScopes } from './combine-scopes.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));
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ALL_ROLE_MAPS } from '../roles/role-maps.ee';
|
||||
import type { RoleNamespace, Scope } from '../types.ee';
|
||||
|
||||
/**
|
||||
* Retrieves roles within a specific namespace that have all the given scopes.
|
||||
*
|
||||
* This is only valid for static roles defined in ALL_ROLE_MAPS, with custom roles
|
||||
* being handled in the RoleService.
|
||||
*
|
||||
* @param namespace - The role namespace to search in
|
||||
* @param scopes - Scope(s) to filter by
|
||||
*/
|
||||
export function staticRolesWithScope(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),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/__tests__/**"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["node", "jest"],
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user