feat: RBAC (#8922)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Val <68596159+valya@users.noreply.github.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
Co-authored-by: Valya Bullions <valya@n8n.io>
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: Danny Martini <despair.blue@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: oleg <me@olegivaniv.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: Giulio Andreini <andreini@netseven.it>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Ayato Hayashi <go12limchangyong@gmail.com>
This commit is contained in:
Csaba Tuncsik
2024-05-17 10:53:15 +02:00
committed by GitHub
parent b1f977ebd0
commit 596c472ecc
292 changed files with 14129 additions and 3989 deletions

View File

@@ -0,0 +1,22 @@
import { faker } from '@faker-js/faker';
import type {
ProjectListItem,
ProjectSharingData,
ProjectType,
} from '@/features/projects/projects.types';
export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({
id: faker.string.uuid(),
name: faker.lorem.words({ min: 1, max: 3 }),
type: projectType || 'personal',
});
export const createProjectListItem = (projectType?: ProjectType): ProjectListItem => {
const project = createProjectSharingData(projectType);
return {
...project,
role: 'project:editor',
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
};
};

View File

@@ -0,0 +1,17 @@
import { faker } from '@faker-js/faker';
import type { IUser } from '@/Interface';
import { SignInType } from '@/constants';
export const createUser = (overrides?: Partial<IUser>): IUser => ({
id: faker.string.uuid(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
isDefaultUser: false,
isPending: false,
isPendingUser: false,
hasRecoveryCodesLeft: false,
mfaEnabled: false,
signInType: SignInType.EMAIL,
...overrides,
});

View File

@@ -12,14 +12,8 @@ import type {
import { NodeHelpers, Workflow } from 'n8n-workflow';
import { uuid } from '@jsplumb/util';
import { defaultMockNodeTypes } from '@/__tests__/defaults';
import type {
INodeUi,
ITag,
IUsedCredential,
IUser,
IWorkflowDb,
WorkflowMetadata,
} from '@/Interface';
import type { INodeUi, ITag, IUsedCredential, IWorkflowDb, WorkflowMetadata } from '@/Interface';
import type { ProjectSharingData } from '@/features/projects/projects.types';
export function createTestNodeTypes(data: INodeTypeData = {}): INodeTypes {
const getResolvedKey = (key: string) => {
@@ -81,8 +75,8 @@ export function createTestWorkflow(options: {
settings?: IWorkflowSettings;
tags?: ITag[] | string[];
pinData?: IPinData;
sharedWith?: Array<Partial<IUser>>;
ownedBy?: Partial<IUser>;
sharedWithProjects?: ProjectSharingData[];
homeProject?: ProjectSharingData;
versionId?: string;
usedCredentials?: IUsedCredential[];
meta?: WorkflowMetadata;

View File

@@ -1,60 +1,116 @@
import { parsePermissionsTable } from '@/permissions';
import type { IUser } from '@/Interface';
import { ROLE } from '@/constants';
import {
getVariablesPermissions,
getProjectPermissions,
getCredentialPermissions,
getWorkflowPermissions,
} from '@/permissions';
import type { ICredentialsResponse, IUser, IWorkflowDb } from '@/Interface';
import type { Project } from '@/features/projects/projects.types';
describe('parsePermissionsTable()', () => {
const user: IUser = {
id: '1',
firstName: 'John',
lastName: 'Doe',
isDefaultUser: false,
isPending: false,
isPendingUser: false,
mfaEnabled: false,
hasRecoveryCodesLeft: false,
role: ROLE.Owner,
};
describe('permissions', () => {
it('getVariablesPermissions', () => {
expect(getVariablesPermissions(null)).toEqual({
create: false,
read: false,
update: false,
delete: false,
list: false,
});
it('should return permissions object using generic permissions table', () => {
const permissions = parsePermissionsTable(user, []);
expect(
getVariablesPermissions({
globalScopes: [
'variable:create',
'variable:read',
'variable:update',
'variable:delete',
'variable:list',
],
} as IUser),
).toEqual({
create: true,
read: true,
update: true,
delete: true,
list: true,
});
expect(permissions.isInstanceOwner).toBe(true);
expect(
getVariablesPermissions({
globalScopes: ['variable:read', 'variable:list'],
} as IUser),
).toEqual({
create: false,
read: true,
update: false,
delete: false,
list: true,
});
});
it('should set permission based on permissions table row test function', () => {
const permissions = parsePermissionsTable(user, [
{ name: 'canRead', test: () => true },
{ name: 'canUpdate', test: () => false },
]);
expect(permissions.canRead).toBe(true);
expect(permissions.canUpdate).toBe(false);
it('getProjectPermissions', () => {
expect(
getProjectPermissions({
scopes: [
'project:create',
'project:read',
'project:update',
'project:delete',
'project:list',
],
} as Project),
).toEqual({
create: true,
read: true,
update: true,
delete: true,
list: true,
});
});
it('should set permission based on previously computed permission', () => {
const permissions = parsePermissionsTable(user, [
{ name: 'canRead', test: ['isInstanceOwner'] },
]);
expect(permissions.canRead).toBe(true);
it('getCredentialPermissions', () => {
expect(
getCredentialPermissions({
scopes: [
'credential:create',
'credential:read',
'credential:update',
'credential:delete',
'credential:list',
'credential:share',
],
} as ICredentialsResponse),
).toEqual({
create: true,
read: true,
update: true,
delete: true,
list: true,
share: true,
});
});
it('should set permission based on multiple previously computed permissions', () => {
const permissions = parsePermissionsTable(user, [
{ name: 'isResourceOwner', test: ['isInstanceOwner'] },
{ name: 'canRead', test: ['isInstanceOwner', 'isResourceOwner'] },
]);
expect(permissions.canRead).toBe(true);
});
it('should pass permission to test functions', () => {
const permissions = parsePermissionsTable(user, [
{ name: 'canRead', test: (p) => !!p.isInstanceOwner },
{ name: 'canUpdate', test: (p) => !!p.canRead },
]);
expect(permissions.canRead).toBe(true);
expect(permissions.canUpdate).toBe(true);
it('getWorkflowPermissions', () => {
expect(
getWorkflowPermissions({
scopes: [
'workflow:create',
'workflow:read',
'workflow:update',
'workflow:delete',
'workflow:list',
'workflow:share',
'workflow:execute',
],
} as IWorkflowDb),
).toEqual({
create: true,
read: true,
update: true,
delete: true,
list: true,
share: true,
execute: true,
});
});
});

View File

@@ -31,6 +31,7 @@ describe('router', () => {
test.each([
['/', VIEWS.WORKFLOWS],
['/workflows', VIEWS.WORKFLOWS],
['/workflow', VIEWS.NEW_WORKFLOW],
['/workflow/new', VIEWS.NEW_WORKFLOW],
['/workflow/R9JFXwkUCL1jZBuw', VIEWS.WORKFLOW],

View File

@@ -1,3 +1,5 @@
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import type { ISettingsState } from '@/Interface';
import { UserManagementAuthenticationMethod } from '@/Interface';
import { defaultSettings } from './defaults';
@@ -62,3 +64,17 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
saveManualExecutions: false,
binaryDataMode: 'default',
};
export const getDropdownItems = async (dropdownTriggerParent: HTMLElement) => {
await userEvent.click(within(dropdownTriggerParent).getByRole('textbox'));
const selectTrigger = dropdownTriggerParent.querySelector(
'.select-trigger[aria-describedby]',
) as HTMLElement;
await waitFor(() => expect(selectTrigger).toBeInTheDocument());
const selectDropdownId = selectTrigger.getAttribute('aria-describedby');
const selectDropdown = document.getElementById(selectDropdownId as string) as HTMLElement;
await waitFor(() => expect(selectDropdown).toBeInTheDocument());
return selectDropdown.querySelectorAll('.el-select-dropdown__item');
};