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:
22
packages/editor-ui/src/__tests__/data/projects.ts
Normal file
22
packages/editor-ui/src/__tests__/data/projects.ts
Normal 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(),
|
||||
};
|
||||
};
|
||||
17
packages/editor-ui/src/__tests__/data/users.ts
Normal file
17
packages/editor-ui/src/__tests__/data/users.ts
Normal 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,
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user