Skip to content

Commit

Permalink
Merge branch 'master' into ADO-2365-update-design-system
Browse files Browse the repository at this point in the history
* master:
  fix(editor): Add workflow scopes when initializing workflow  (#10455)
  feat(editor): Improve node label readability in new canvas (no-changelog) (#10432)
  fix(editor): Fix lazy loaded component not using suspense (no-changelog) (#10454)
  fix(editor): Buffer json chunks in stream response (#10439)
  refactor(editor): Remove `id` param from PATCH /me calls (no-changelog) (#10449)
  fix(core): Fix XSS validation and separate URL validation (#10424)
  fix(Respond to Webhook Node): Fix issue preventing the chat trigger from working (#9886)
  feat(editor): Add `registerCustomAction` to new canvas (no-changelog) (#10359)
  • Loading branch information
MiloradFilipovic committed Aug 16, 2024
2 parents f64e770 + b857c2c commit 60d502a
Show file tree
Hide file tree
Showing 31 changed files with 555 additions and 147 deletions.
21 changes: 21 additions & 0 deletions cypress/e2e/1858-PAY-can-use-context-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';

const WorkflowPage = new WorkflowPageClass();

describe('PAY-1858 context menu', () => {
it('can use context menu on saved workflow', () => {
WorkflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_filter.json', 'test');

WorkflowPage.getters.canvasNodes().should('have.length', 5);
WorkflowPage.actions.deleteNodeFromContextMenu('Then');
WorkflowPage.getters.canvasNodes().should('have.length', 4);

WorkflowPage.actions.hitSaveWorkflow();

cy.reload();
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.actions.deleteNodeFromContextMenu('Code');
WorkflowPage.getters.canvasNodes().should('have.length', 3);
});
});
3 changes: 2 additions & 1 deletion cypress/e2e/33-settings-personal.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ describe('Personal Settings', () => {
successToast().find('.el-notification__closeBtn').click();
});
});
it('not allow malicious values for personal data', () => {
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
it.skip('not allow malicious values for personal data', () => {
cy.visit('/settings/personal');
INVALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"reflect-metadata": "0.2.2",
"replacestream": "4.0.3",
"samlify": "2.8.9",
"sanitize-html": "2.12.1",
"semver": "7.5.4",
"shelljs": "0.8.5",
"simple-git": "3.17.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/GenericHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
UserUpdatePayload,
} from '@/requests';
import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NoXss } from './databases/utils/customValidators';
import { NoXss } from '@/validators/no-xss.validator';

export async function validateEntity(
entity:
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/databases/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IsEmail, IsString, Length } from 'class-validator';
import type { IUser, IUserSettings } from 'n8n-workflow';
import type { SharedWorkflow } from './SharedWorkflow';
import type { SharedCredentials } from './SharedCredentials';
import { NoXss } from '../utils/customValidators';
import { NoXss } from '@/validators/no-xss.validator';
import { objectRetriever, lowerCaser } from '../utils/transformers';
import { WithTimestamps, jsonColumnType } from './AbstractEntity';
import type { IPersonalizationSurveyAnswers } from '@/Interfaces';
Expand All @@ -25,6 +25,7 @@ import {
} from '@/permissions/global-roles';
import { hasScope, type ScopeOptions, type Scope } from '@n8n/permissions';
import type { ProjectRelation } from './ProjectRelation';
import { NoUrl } from '@/validators/no-url.validator';

export type GlobalRole = 'global:owner' | 'global:admin' | 'global:member';
export type AssignableRole = Exclude<GlobalRole, 'global:owner'>;
Expand All @@ -51,12 +52,14 @@ export class User extends WithTimestamps implements IUser {

@Column({ length: 32, nullable: true })
@NoXss()
@NoUrl()
@IsString({ message: 'First name must be of type string.' })
@Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' })
firstName: string;

@Column({ length: 32, nullable: true })
@NoXss()
@NoUrl()
@IsString({ message: 'Last name must be of type string.' })
@Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' })
lastName: string;
Expand Down

This file was deleted.

18 changes: 0 additions & 18 deletions packages/cli/src/databases/utils/customValidators.ts

This file was deleted.

5 changes: 4 additions & 1 deletion packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {

import { Expose } from 'class-transformer';
import { IsBoolean, IsEmail, IsIn, IsOptional, IsString, Length } from 'class-validator';
import { NoXss } from '@db/utils/customValidators';
import { NoXss } from '@/validators/no-xss.validator';
import type { PublicUser, SecretsProvider, SecretsProviderState } from '@/Interfaces';
import { AssignableRole } from '@db/entities/User';
import type { GlobalRole, User } from '@db/entities/User';
Expand All @@ -26,6 +26,7 @@ import type { ProjectRole } from './databases/entities/ProjectRelation';
import type { Scope } from '@n8n/permissions';
import type { ScopesField } from './services/role.service';
import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk';
import { NoUrl } from '@/validators/no-url.validator';

export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
@Expose()
Expand All @@ -34,12 +35,14 @@ export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'la

@Expose()
@NoXss()
@NoUrl()
@IsString({ message: 'First name must be of type string.' })
@Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' })
firstName: string;

@Expose()
@NoXss()
@NoUrl()
@IsString({ message: 'Last name must be of type string.' })
@Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' })
lastName: string;
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/validators/__tests__/no-url.validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NoUrl } from '../no-url.validator';
import { validate } from 'class-validator';

describe('NoUrl', () => {
class Entity {
@NoUrl()
name = '';
}

const entity = new Entity();

describe('URLs', () => {
const URLS = ['http://google.com', 'www.domain.tld'];

for (const str of URLS) {
test(`should block ${str}`, async () => {
entity.name = str;
const errors = await validate(entity);
expect(errors).toHaveLength(1);
const [error] = errors;
expect(error.property).toEqual('name');
expect(error.constraints).toEqual({ NoUrl: 'Potentially malicious string' });
});
}
});
});
72 changes: 72 additions & 0 deletions packages/cli/src/validators/__tests__/no-xss.validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NoXss } from '../no-xss.validator';
import { validate } from 'class-validator';

describe('NoXss', () => {
class Entity {
@NoXss()
name = '';

@NoXss()
timestamp = '';

@NoXss()
version = '';
}

const entity = new Entity();

describe('Scripts', () => {
const XSS_STRINGS = ['<script src/>', "<script>alert('xss')</script>"];

for (const str of XSS_STRINGS) {
test(`should block ${str}`, async () => {
entity.name = str;
const errors = await validate(entity);
expect(errors).toHaveLength(1);
const [error] = errors;
expect(error.property).toEqual('name');
expect(error.constraints).toEqual({ NoXss: 'Potentially malicious string' });
});
}
});

describe('Names', () => {
const VALID_NAMES = [
'Johann Strauß',
'Вагиф Сәмәдоғлу',
'René Magritte',
'সুকুমার রায়',
'མགོན་པོ་རྡོ་རྗེ།',
'عبدالحليم حافظ',
];

for (const name of VALID_NAMES) {
test(`should allow ${name}`, async () => {
entity.name = name;
expect(await validate(entity)).toBeEmptyArray();
});
}
});

describe('ISO-8601 timestamps', () => {
const VALID_TIMESTAMPS = ['2022-01-01T00:00:00.000Z', '2022-01-01T00:00:00.000+02:00'];

for (const timestamp of VALID_TIMESTAMPS) {
test(`should allow ${timestamp}`, async () => {
entity.timestamp = timestamp;
await expect(validate(entity)).resolves.toBeEmptyArray();
});
}
});

describe('Semver versions', () => {
const VALID_VERSIONS = ['1.0.0', '1.0.0-alpha.1'];

for (const version of VALID_VERSIONS) {
test(`should allow ${version}`, async () => {
entity.version = version;
await expect(validate(entity)).resolves.toBeEmptyArray();
});
}
});
});
27 changes: 27 additions & 0 deletions packages/cli/src/validators/no-url.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ValidationOptions, ValidatorConstraintInterface } from 'class-validator';
import { registerDecorator, ValidatorConstraint } from 'class-validator';

const URL_REGEX = /^(https?:\/\/|www\.)/i;

@ValidatorConstraint({ name: 'NoUrl', async: false })
class NoUrlConstraint implements ValidatorConstraintInterface {
validate(value: string) {
return !URL_REGEX.test(value);
}

defaultMessage() {
return 'Potentially malicious string';
}
}

export function NoUrl(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'NoUrl',
target: object.constructor,
propertyName,
options,
validator: NoUrlConstraint,
});
};
}
26 changes: 26 additions & 0 deletions packages/cli/src/validators/no-xss.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ValidationOptions, ValidatorConstraintInterface } from 'class-validator';
import { registerDecorator, ValidatorConstraint } from 'class-validator';
import sanitizeHtml from 'sanitize-html';

@ValidatorConstraint({ name: 'NoXss', async: false })
class NoXssConstraint implements ValidatorConstraintInterface {
validate(value: string) {
return value === sanitizeHtml(value, { allowedTags: [], allowedAttributes: {} });
}

defaultMessage() {
return 'Potentially malicious string';
}
}

export function NoXss(options?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'NoXss',
target: object.constructor,
propertyName,
options,
validator: NoXssConstraint,
});
};
}
2 changes: 1 addition & 1 deletion packages/editor-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import AskAssistantFloatingButton from '@/components/AskAssistant/AskAssistantFl
import { HIRING_BANNER, VIEWS } from '@/constants';
import { loadLanguage } from '@/plugins/i18n';
import useGlobalLinkActions from '@/composables/useGlobalLinkActions';
import { useGlobalLinkActions } from '@/composables/useGlobalLinkActions';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useToast } from '@/composables/useToast';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
Expand Down
31 changes: 30 additions & 1 deletion packages/editor-ui/src/__tests__/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
SET_NODE_TYPE,
STICKY_NODE_TYPE,
} from '@/constants';
import type { INodeUi } from '@/Interface';
import type { INodeUi, IWorkflowDb } from '@/Interface';

export const mockNode = ({
id = uuid(),
Expand Down Expand Up @@ -147,6 +147,35 @@ export function createTestWorkflowObject({
});
}

export function createTestWorkflow({
id = uuid(),
name = 'Test Workflow',
nodes = [],
connections = {},
active = false,
settings = {
timezone: 'DEFAULT',
executionOrder: 'v1',
},
pinData = {},
...rest
}: Partial<IWorkflowDb> = {}): IWorkflowDb {
return {
createdAt: '',
updatedAt: '',
id,
name,
nodes,
connections,
active,
settings,
versionId: '1',
meta: {},
pinData,
...rest,
};
}

export function createTestNode(node: Partial<INode> = {}): INode {
return {
id: uuid(),
Expand Down
Loading

0 comments on commit 60d502a

Please sign in to comment.