From e2ffd397fc0ab8d88128ba78d02c5df003af4a9d Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 16 Nov 2023 18:08:23 +0100 Subject: [PATCH] fix(editor): Handle permission edge cases (empty scopes) (#7723) --- packages/@n8n/permissions/src/hasScope.ts | 36 ++++++------------- packages/@n8n/permissions/src/types.ts | 10 ++++-- .../@n8n/permissions/test/hasScope.test.ts | 16 +++++++++ packages/cli/src/databases/entities/User.ts | 9 ++--- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/@n8n/permissions/src/hasScope.ts b/packages/@n8n/permissions/src/hasScope.ts index b4fe5b55b3..80cb9e8982 100644 --- a/packages/@n8n/permissions/src/hasScope.ts +++ b/packages/@n8n/permissions/src/hasScope.ts @@ -1,42 +1,28 @@ -import type { Scope, ScopeLevels } from './types'; - -export type HasScopeMode = 'oneOf' | 'allOf'; -export interface HasScopeOptions { - mode: HasScopeMode; -} +import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions } from './types'; +export function hasScope( + scope: Scope | Scope[], + userScopes: GlobalScopes, + options?: ScopeOptions, +): boolean; export function hasScope( scope: Scope | Scope[], userScopes: ScopeLevels, - options?: HasScopeOptions, + options?: ScopeOptions, ): 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' }, + userScopes: unknown, + options: ScopeOptions = { mode: 'oneOf' }, ): boolean { if (!Array.isArray(scope)) { scope = [scope]; } - const userScopeSet = new Set([ - ...userScopes.global, - ...(userScopes.project ?? []), - ...(userScopes.resource ?? []), - ]); + const userScopeSet = new Set(Object.values(userScopes ?? {}).flat()); if (options.mode === 'allOf') { - return scope.every((s) => userScopeSet.has(s)); + return !!scope.length && scope.every((s) => userScopeSet.has(s)); } return scope.some((s) => userScopeSet.has(s)); diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index b90ad9246c..1ef19459ea 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -34,5 +34,11 @@ export type Scope = | SourceControlScope | ExternalSecretStoreScope; -export type ScopeLevel = 'global' | 'project' | 'resource'; -export type ScopeLevels = Record; +export type ScopeLevel = Record; +export type GlobalScopes = ScopeLevel<'global'>; +export type ProjectScopes = ScopeLevel<'project'>; +export type ResourceScopes = ScopeLevel<'resource'>; +export type ScopeLevels = GlobalScopes & (ProjectScopes | (ProjectScopes & ResourceScopes)); + +export type ScopeMode = 'oneOf' | 'allOf'; +export type ScopeOptions = { mode: ScopeMode }; diff --git a/packages/@n8n/permissions/test/hasScope.test.ts b/packages/@n8n/permissions/test/hasScope.test.ts index 2e75b588db..22137d6326 100644 --- a/packages/@n8n/permissions/test/hasScope.test.ts +++ b/packages/@n8n/permissions/test/hasScope.test.ts @@ -80,6 +80,12 @@ describe('hasScope', () => { global: memberPermissions, }), ).toBe(false); + + expect( + hasScope([], { + global: memberPermissions, + }), + ).toBe(false); }); test('should work with allOf mode', () => { @@ -112,5 +118,15 @@ describe('hasScope', () => { { mode: 'allOf' }, ), ).toBe(false); + + expect( + hasScope( + [], + { + global: memberPermissions, + }, + { mode: 'allOf' }, + ), + ).toBe(false); }); }); diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index dfcc6174a1..eb69b2e37f 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -21,7 +21,7 @@ import { WithTimestamps, jsonColumnType } from './AbstractEntity'; import type { IPersonalizationSurveyAnswers } from '@/Interfaces'; import type { AuthIdentity } from './AuthIdentity'; import { ownerPermissions, memberPermissions } from '@/permissions/roles'; -import { hasScope, type HasScopeOptions, type Scope } from '@n8n/permissions'; +import { hasScope, type ScopeOptions, type Scope } from '@n8n/permissions'; export const MIN_PASSWORD_LENGTH = 8; @@ -137,16 +137,13 @@ export class User extends WithTimestamps implements IUser { return STATIC_SCOPE_MAP[this.globalRole?.name] ?? []; } - async hasGlobalScope( - scope: Scope | Scope[], - hasScopeOptions?: HasScopeOptions, - ): Promise { + async hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): Promise { return hasScope( scope, { global: this.globalScopes, }, - hasScopeOptions, + scopeOptions, ); } }