Skip to content

Commit

Permalink
feat: Add permissions package (no-changelog) (#7650)
Browse files Browse the repository at this point in the history
Github issue / Community forum post (link here to close automatically):
  • Loading branch information
valya authored Nov 8, 2023
1 parent 0346b21 commit 0468ded
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 0 deletions.
10 changes: 10 additions & 0 deletions packages/@n8n/permissions/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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),
};
2 changes: 2 additions & 0 deletions packages/@n8n/permissions/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../../jest.config');
22 changes: 22 additions & 0 deletions packages/@n8n/permissions/package.json
Original file line number Diff line number Diff line change
@@ -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/**/*"
]
}
43 changes: 43 additions & 0 deletions packages/@n8n/permissions/src/hasScope.ts
Original file line number Diff line number Diff line change
@@ -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<ScopeLevels, 'global'>,
options?: HasScopeOptions,
): boolean;
export function hasScope(
scope: Scope | Scope[],
userScopes: Omit<ScopeLevels, 'resource'>,
options?: HasScopeOptions,
): boolean;
export function hasScope(
scope: Scope | Scope[],
userScopes: Pick<ScopeLevels, 'global'> & Partial<ScopeLevels>,
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));
}
2 changes: 2 additions & 0 deletions packages/@n8n/permissions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type * from './types';
export * from './hasScope';
35 changes: 35 additions & 0 deletions packages/@n8n/permissions/src/types.ts
Original file line number Diff line number Diff line change
@@ -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<ScopeLevel, Scope[]>;
116 changes: 116 additions & 0 deletions packages/@n8n/permissions/test/hasScope.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
10 changes: 10 additions & 0 deletions packages/@n8n/permissions/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["test/**"]
}
15 changes: 15 additions & 0 deletions packages/@n8n/permissions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0468ded

Please sign in to comment.