fix(editor): Handle permission edge cases (empty scopes) (#7723)

This commit is contained in:
Csaba Tuncsik 2023-11-16 18:08:23 +01:00 committed by GitHub
parent 9d6a68d1c7
commit e2ffd397fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 38 additions and 33 deletions

View file

@ -1,42 +1,28 @@
import type { Scope, ScopeLevels } from './types'; import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions } from './types';
export type HasScopeMode = 'oneOf' | 'allOf';
export interface HasScopeOptions {
mode: HasScopeMode;
}
export function hasScope(
scope: Scope | Scope[],
userScopes: GlobalScopes,
options?: ScopeOptions,
): boolean;
export function hasScope( export function hasScope(
scope: Scope | Scope[], scope: Scope | Scope[],
userScopes: ScopeLevels, userScopes: ScopeLevels,
options?: HasScopeOptions, options?: ScopeOptions,
): boolean; ): boolean;
export function hasScope( export function hasScope(
scope: Scope | Scope[], scope: Scope | Scope[],
userScopes: Pick<ScopeLevels, 'global'>, userScopes: unknown,
options?: HasScopeOptions, options: ScopeOptions = { mode: 'oneOf' },
): 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 { ): boolean {
if (!Array.isArray(scope)) { if (!Array.isArray(scope)) {
scope = [scope]; scope = [scope];
} }
const userScopeSet = new Set([ const userScopeSet = new Set(Object.values(userScopes ?? {}).flat());
...userScopes.global,
...(userScopes.project ?? []),
...(userScopes.resource ?? []),
]);
if (options.mode === 'allOf') { 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)); return scope.some((s) => userScopeSet.has(s));

View file

@ -34,5 +34,11 @@ export type Scope =
| SourceControlScope | SourceControlScope
| ExternalSecretStoreScope; | ExternalSecretStoreScope;
export type ScopeLevel = 'global' | 'project' | 'resource'; export type ScopeLevel<T extends 'global' | 'project' | 'resource'> = Record<T, Scope[]>;
export type ScopeLevels = Record<ScopeLevel, Scope[]>; 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 };

View file

@ -80,6 +80,12 @@ describe('hasScope', () => {
global: memberPermissions, global: memberPermissions,
}), }),
).toBe(false); ).toBe(false);
expect(
hasScope([], {
global: memberPermissions,
}),
).toBe(false);
}); });
test('should work with allOf mode', () => { test('should work with allOf mode', () => {
@ -112,5 +118,15 @@ describe('hasScope', () => {
{ mode: 'allOf' }, { mode: 'allOf' },
), ),
).toBe(false); ).toBe(false);
expect(
hasScope(
[],
{
global: memberPermissions,
},
{ mode: 'allOf' },
),
).toBe(false);
}); });
}); });

View file

@ -21,7 +21,7 @@ import { WithTimestamps, jsonColumnType } from './AbstractEntity';
import type { IPersonalizationSurveyAnswers } from '@/Interfaces'; import type { IPersonalizationSurveyAnswers } from '@/Interfaces';
import type { AuthIdentity } from './AuthIdentity'; import type { AuthIdentity } from './AuthIdentity';
import { ownerPermissions, memberPermissions } from '@/permissions/roles'; 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; export const MIN_PASSWORD_LENGTH = 8;
@ -137,16 +137,13 @@ export class User extends WithTimestamps implements IUser {
return STATIC_SCOPE_MAP[this.globalRole?.name] ?? []; return STATIC_SCOPE_MAP[this.globalRole?.name] ?? [];
} }
async hasGlobalScope( async hasGlobalScope(scope: Scope | Scope[], scopeOptions?: ScopeOptions): Promise<boolean> {
scope: Scope | Scope[],
hasScopeOptions?: HasScopeOptions,
): Promise<boolean> {
return hasScope( return hasScope(
scope, scope,
{ {
global: this.globalScopes, global: this.globalScopes,
}, },
hasScopeOptions, scopeOptions,
); );
} }
} }