From 0468ded0db0a7832250101ca2f908fcae5c8ca0c Mon Sep 17 00:00:00 2001 From: Val <68596159+valya@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:42:40 +0000 Subject: [PATCH] feat: Add permissions package (no-changelog) (#7650) Github issue / Community forum post (link here to close automatically): --- packages/@n8n/permissions/.eslintrc.js | 10 ++ packages/@n8n/permissions/jest.config.js | 2 + packages/@n8n/permissions/package.json | 22 ++++ packages/@n8n/permissions/src/hasScope.ts | 43 +++++++ packages/@n8n/permissions/src/index.ts | 2 + packages/@n8n/permissions/src/types.ts | 35 ++++++ .../@n8n/permissions/test/hasScope.test.ts | 116 ++++++++++++++++++ packages/@n8n/permissions/tsconfig.build.json | 10 ++ packages/@n8n/permissions/tsconfig.json | 15 +++ pnpm-lock.yaml | 2 + 10 files changed, 257 insertions(+) create mode 100644 packages/@n8n/permissions/.eslintrc.js create mode 100644 packages/@n8n/permissions/jest.config.js create mode 100644 packages/@n8n/permissions/package.json create mode 100644 packages/@n8n/permissions/src/hasScope.ts create mode 100644 packages/@n8n/permissions/src/index.ts create mode 100644 packages/@n8n/permissions/src/types.ts create mode 100644 packages/@n8n/permissions/test/hasScope.test.ts create mode 100644 packages/@n8n/permissions/tsconfig.build.json create mode 100644 packages/@n8n/permissions/tsconfig.json diff --git a/packages/@n8n/permissions/.eslintrc.js b/packages/@n8n/permissions/.eslintrc.js new file mode 100644 index 0000000000000..f60180296c7f9 --- /dev/null +++ b/packages/@n8n/permissions/.eslintrc.js @@ -0,0 +1,10 @@ +const sharedOptions = require('@n8n_io/eslint-config/shared'); + +/** + * @type {import('@types/eslint').ESLint.ConfigData} + */ +module.exports = { + extends: ['@n8n_io/eslint-config/base'], + + ...sharedOptions(__dirname), +}; diff --git a/packages/@n8n/permissions/jest.config.js b/packages/@n8n/permissions/jest.config.js new file mode 100644 index 0000000000000..d6c48554a79a4 --- /dev/null +++ b/packages/@n8n/permissions/jest.config.js @@ -0,0 +1,2 @@ +/** @type {import('jest').Config} */ +module.exports = require('../../../jest.config'); diff --git a/packages/@n8n/permissions/package.json b/packages/@n8n/permissions/package.json new file mode 100644 index 0000000000000..da7abe07aba1d --- /dev/null +++ b/packages/@n8n/permissions/package.json @@ -0,0 +1,22 @@ +{ + "name": "@n8n/permissions", + "version": "0.0.1", + "scripts": { + "clean": "rimraf dist .turbo", + "dev": "pnpm watch", + "typecheck": "tsc", + "build": "tsc -p tsconfig.build.json", + "format": "prettier --write . --ignore-path ../../../.prettierignore", + "lint": "eslint . --quiet", + "lintfix": "eslint . --fix", + "watch": "tsc -p tsconfig.build.json --watch", + "test": "jest", + "test:dev": "jest --watch" + }, + "main": "dist/index.js", + "module": "src/index.ts", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ] +} diff --git a/packages/@n8n/permissions/src/hasScope.ts b/packages/@n8n/permissions/src/hasScope.ts new file mode 100644 index 0000000000000..b4fe5b55b301d --- /dev/null +++ b/packages/@n8n/permissions/src/hasScope.ts @@ -0,0 +1,43 @@ +import type { Scope, ScopeLevels } from './types'; + +export type HasScopeMode = 'oneOf' | 'allOf'; +export interface HasScopeOptions { + mode: HasScopeMode; +} + +export function hasScope( + scope: Scope | Scope[], + userScopes: ScopeLevels, + options?: HasScopeOptions, +): boolean; +export function hasScope( + scope: Scope | Scope[], + userScopes: Pick, + options?: HasScopeOptions, +): boolean; +export function hasScope( + scope: Scope | Scope[], + userScopes: Omit, + options?: HasScopeOptions, +): boolean; +export function hasScope( + scope: Scope | Scope[], + userScopes: Pick & Partial, + options: HasScopeOptions = { mode: 'oneOf' }, +): boolean { + if (!Array.isArray(scope)) { + scope = [scope]; + } + + const userScopeSet = new Set([ + ...userScopes.global, + ...(userScopes.project ?? []), + ...(userScopes.resource ?? []), + ]); + + if (options.mode === 'allOf') { + return scope.every((s) => userScopeSet.has(s)); + } + + return scope.some((s) => userScopeSet.has(s)); +} diff --git a/packages/@n8n/permissions/src/index.ts b/packages/@n8n/permissions/src/index.ts new file mode 100644 index 0000000000000..5934473ce7835 --- /dev/null +++ b/packages/@n8n/permissions/src/index.ts @@ -0,0 +1,2 @@ +export type * from './types'; +export * from './hasScope'; diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts new file mode 100644 index 0000000000000..32e5cef2d42e9 --- /dev/null +++ b/packages/@n8n/permissions/src/types.ts @@ -0,0 +1,35 @@ +export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list'; +export type Resource = + | 'workflow' + | 'user' + | 'credential' + | 'variable' + | 'sourceControl' + | 'externalSecretsStore'; + +export type ResourceScope< + R extends Resource, + Operations extends string = DefaultOperations, +> = `${R}:${Operations}`; +export type WildcardScope = `${Resource}:*` | '*'; + +export type WorkflowScope = ResourceScope<'workflow'>; +export type UserScope = ResourceScope<'user'>; +export type CredentialScope = ResourceScope<'credential'>; +export type VariableScope = ResourceScope<'variable'>; +export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>; +export type ExternalSecretStoreScope = ResourceScope< + 'externalSecretsStore', + DefaultOperations | 'refresh' +>; + +export type Scope = + | WorkflowScope + | UserScope + | CredentialScope + | VariableScope + | SourceControlScope + | ExternalSecretStoreScope; + +export type ScopeLevel = 'global' | 'project' | 'resource'; +export type ScopeLevels = Record; diff --git a/packages/@n8n/permissions/test/hasScope.test.ts b/packages/@n8n/permissions/test/hasScope.test.ts new file mode 100644 index 0000000000000..2e75b588db196 --- /dev/null +++ b/packages/@n8n/permissions/test/hasScope.test.ts @@ -0,0 +1,116 @@ +import { hasScope } from '@/hasScope'; +import type { Scope } from '@/types'; + +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, + }, + { mode: 'oneOf' }, + ), + ).toBe(true); + + expect( + hasScope( + 'user:list', + { + global: memberPermissions, + }, + { mode: 'allOf' }, + ), + ).toBe(true); + + expect( + hasScope( + 'workflow:read', + { + global: memberPermissions, + }, + { mode: 'oneOf' }, + ), + ).toBe(false); + + expect( + hasScope( + 'workflow:read', + { + global: memberPermissions, + }, + { 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); + }); + + test('should work with allOf mode', () => { + expect( + hasScope( + ['workflow:create', 'workflow:read'], + { + global: ownerPermissions, + }, + { mode: 'allOf' }, + ), + ).toBe(true); + + expect( + hasScope( + ['workflow:create', 'workflow:read'], + { + global: memberPermissions, + }, + { mode: 'allOf' }, + ), + ).toBe(false); + + expect( + hasScope( + ['workflow:create', 'user:list'], + { + global: memberPermissions, + }, + { mode: 'allOf' }, + ), + ).toBe(false); + }); +}); diff --git a/packages/@n8n/permissions/tsconfig.build.json b/packages/@n8n/permissions/tsconfig.build.json new file mode 100644 index 0000000000000..30910a3bf1ebb --- /dev/null +++ b/packages/@n8n/permissions/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/build.tsbuildinfo" + }, + "include": ["src/**/*.ts"], + "exclude": ["test/**"] +} diff --git a/packages/@n8n/permissions/tsconfig.json b/packages/@n8n/permissions/tsconfig.json new file mode 100644 index 0000000000000..a693815582818 --- /dev/null +++ b/packages/@n8n/permissions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["node", "jest"], + "composite": true, + "noEmit": true, + "baseUrl": "src", + "paths": { + "@/*": ["./*"] + }, + "tsBuildInfoFile": "dist/typecheck.tsbuildinfo" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbaeec28499b8..c9729932125af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,8 @@ importers: specifier: ^0.21.1 version: 0.21.4 + packages/@n8n/permissions: {} + packages/@n8n_io/eslint-config: devDependencies: '@types/eslint':