feat: Add permissions package (no-changelog) (#7650)

Github issue / Community forum post (link here to close automatically):
This commit is contained in:
Val 2023-11-08 15:42:40 +00:00 committed by GitHub
parent 0346b211a7
commit 0468ded0db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 257 additions and 0 deletions

View file

@ -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),
};

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../../jest.config');

View file

@ -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/**/*"
]
}

View file

@ -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));
}

View file

@ -0,0 +1,2 @@
export type * from './types';
export * from './hasScope';

View file

@ -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[]>;

View file

@ -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);
});
});

View file

@ -0,0 +1,10 @@
{
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["test/**"]
}

View file

@ -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"]
}

View file

@ -141,6 +141,8 @@ importers:
specifier: ^0.21.1
version: 0.21.4
packages/@n8n/permissions: {}
packages/@n8n_io/eslint-config:
devDependencies:
'@types/eslint':