Skip to content

Commit

Permalink
feat: Add AND mask to hasScope (#8379)
Browse files Browse the repository at this point in the history
  • Loading branch information
valya committed Jan 22, 2024
1 parent 1aa35b1 commit 54ac2df
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 2 deletions.
20 changes: 18 additions & 2 deletions packages/@n8n/permissions/src/hasScope.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions } from './types';
import type { Scope, ScopeLevels, GlobalScopes, ScopeOptions, MaskLevels } from './types';

export function hasScope(
scope: Scope | Scope[],
userScopes: GlobalScopes,
masks?: MaskLevels,
options?: ScopeOptions,
): boolean;
export function hasScope(
scope: Scope | Scope[],
userScopes: ScopeLevels,
masks?: MaskLevels,
options?: ScopeOptions,
): boolean;
export function hasScope(
scope: Scope | Scope[],
userScopes: GlobalScopes | ScopeLevels,
masks?: MaskLevels,
options: ScopeOptions = { mode: 'oneOf' },
): boolean {
if (!Array.isArray(scope)) {
scope = [scope];
}

const userScopeSet = new Set(Object.values(userScopes).flat());
const maskedScopes: GlobalScopes | ScopeLevels = Object.fromEntries(
Object.entries(userScopes).map((e) => [e[0], [...e[1]]]),
) as GlobalScopes | ScopeLevels;

if (masks?.sharing) {
if ('project' in maskedScopes) {
maskedScopes.project = maskedScopes.project.filter((v) => masks.sharing.includes(v));
}
if ('resource' in maskedScopes) {
maskedScopes.resource = maskedScopes.resource.filter((v) => masks.sharing.includes(v));
}
}

const userScopeSet = new Set(Object.values(maskedScopes).flat());

if (options.mode === 'allOf') {
return !!scope.length && scope.every((s) => userScopeSet.has(s));
Expand Down
5 changes: 5 additions & 0 deletions packages/@n8n/permissions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,10 @@ export type ProjectScopes = GetScopeLevel<'project'>;
export type ResourceScopes = GetScopeLevel<'resource'>;
export type ScopeLevels = GlobalScopes & (ProjectScopes | (ProjectScopes & ResourceScopes));

export type MaskLevel = 'sharing';
export type GetMaskLevel<T extends MaskLevel> = Record<T, Scope[]>;
export type SharingMasks = GetMaskLevel<'sharing'>;
export type MaskLevels = SharingMasks;

export type ScopeMode = 'oneOf' | 'allOf';
export type ScopeOptions = { mode: ScopeMode };
126 changes: 126 additions & 0 deletions packages/@n8n/permissions/test/hasScope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'oneOf' },
),
).toBe(true);
Expand All @@ -43,6 +44,7 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'allOf' },
),
).toBe(true);
Expand All @@ -53,6 +55,7 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'oneOf' },
),
).toBe(false);
Expand All @@ -63,6 +66,7 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'allOf' },
),
).toBe(false);
Expand Down Expand Up @@ -95,6 +99,7 @@ describe('hasScope', () => {
{
global: ownerPermissions,
},
undefined,
{ mode: 'allOf' },
),
).toBe(true);
Expand All @@ -105,6 +110,7 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'allOf' },
),
).toBe(false);
Expand All @@ -115,6 +121,7 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'allOf' },
),
).toBe(false);
Expand All @@ -125,8 +132,127 @@ describe('hasScope', () => {
{
global: memberPermissions,
},
undefined,
{ mode: 'allOf' },
),
).toBe(false);
});
});

describe('hasScope masking', () => {
test('should return true without mask when scopes present', () => {
expect(
hasScope('workflow:read', {
global: ['user:list'],
project: ['workflow:read'],
resource: [],
}),
).toBe(true);
});

test('should return false without mask when scopes are not present', () => {
expect(
hasScope('workflow:update', {
global: ['user:list'],
project: ['workflow:read'],
resource: [],
}),
).toBe(false);
});

test('should return false when mask does not include scope but scopes list does contain required scope', () => {
expect(
hasScope(
'workflow:update',
{
global: ['user:list'],
project: ['workflow:read', 'workflow:update'],
resource: [],
},
{
sharing: ['workflow:read'],
},
),
).toBe(false);
});

test('should return true when mask does include scope and scope list includes scope', () => {
expect(
hasScope(
'workflow:update',
{
global: ['user:list'],
project: ['workflow:read', 'workflow:update'],
resource: [],
},
{
sharing: ['workflow:read', 'workflow:update'],
},
),
).toBe(true);
});

test('should return true when mask does include scope and scopes list includes scope on multiple levels', () => {
expect(
hasScope(
'workflow:update',
{
global: ['user:list'],
project: ['workflow:read', 'workflow:update'],
resource: ['workflow:update'],
},
{
sharing: ['workflow:read', 'workflow:update'],
},
),
).toBe(true);
});

test('should not mask out global scopes', () => {
expect(
hasScope(
'workflow:update',
{
global: ['workflow:read', 'workflow:update'],
project: ['workflow:read'],
resource: ['workflow:read'],
},
{
sharing: ['workflow:read'],
},
),
).toBe(true);
});

test('should return false when scope is not in mask or scope list', () => {
expect(
hasScope(
'workflow:update',
{
global: ['workflow:read'],
project: ['workflow:read'],
resource: ['workflow:read'],
},
{
sharing: ['workflow:read'],
},
),
).toBe(false);
});

test('should return false when scope is in mask or not scope list', () => {
expect(
hasScope(
'workflow:update',
{
global: ['workflow:read'],
project: ['workflow:read'],
resource: ['workflow:read'],
},
{
sharing: ['workflow:read', 'workflow:update'],
},
),
).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/cli/src/databases/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export class User extends WithTimestamps implements IUser {
{
global: this.globalScopes,
},
undefined,
scopeOptions,
);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/editor-ui/src/stores/__tests__/rbac.store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('RBAC store', () => {
resource: [],
},
undefined,
undefined,
);
});

Expand All @@ -105,6 +106,7 @@ describe('RBAC store', () => {
resource: [],
},
undefined,
undefined,
);
});

Expand All @@ -123,6 +125,7 @@ describe('RBAC store', () => {
resource: expect.arrayContaining([newScope]),
},
undefined,
undefined,
);
});

Expand All @@ -146,6 +149,7 @@ describe('RBAC store', () => {
resource: expect.arrayContaining([newScope]),
},
undefined,
undefined,
);
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/editor-ui/src/stores/rbac.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
? scopesByResourceId.value[context.resourceType][context.resourceId]
: [],
},
undefined,
options,
);
}
Expand Down

0 comments on commit 54ac2df

Please sign in to comment.