Skip to content

Commit

Permalink
feat(core): Use WebCrypto to generate all random numbers and strings (n…
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy authored and adrian-martinez-onestic committed Jun 20, 2024
1 parent 7225014 commit 16d3cfe
Show file tree
Hide file tree
Showing 49 changed files with 254 additions and 214 deletions.
9 changes: 5 additions & 4 deletions cypress/e2e/6-code-node.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { nanoid } from 'nanoid';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV } from '../pages/ndv';
import { successToast } from '../pages/notifications';
Expand Down Expand Up @@ -85,22 +86,22 @@ describe('Code node', () => {
cy.getByTestId('ask-ai-cta-tooltip-no-prompt').should('exist');
cy.getByTestId('ask-ai-prompt-input')
// Type random 14 character string
.type([...Array(14)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''));
.type(nanoid(14));

cy.getByTestId('ask-ai-cta').realHover();
cy.getByTestId('ask-ai-cta-tooltip-prompt-too-short').should('exist');

cy.getByTestId('ask-ai-prompt-input')
.clear()
// Type random 15 character string
.type([...Array(15)].map(() => ((Math.random() * 36) | 0).toString(36)).join(''));
.type(nanoid(15));
cy.getByTestId('ask-ai-cta').should('be.enabled');

cy.getByTestId('ask-ai-prompt-counter').should('contain.text', '15 / 600');
});

it('should send correct schema and replace code', () => {
const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();

Expand Down Expand Up @@ -130,7 +131,7 @@ describe('Code node', () => {
});

it('should show error based on status code', () => {
const prompt = [...Array(20)].map(() => ((Math.random() * 36) | 0).toString(36)).join('');
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();

Expand Down
3 changes: 2 additions & 1 deletion cypress/utils/executions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { nanoid } from 'nanoid';
import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow';
import { clickExecuteWorkflowButton } from '../composables/workflow';

Expand Down Expand Up @@ -85,7 +86,7 @@ export function runMockWorkflowExecution({
runData: Array<ReturnType<typeof createMockNodeExecutionData>>;
workflowExecutionData?: ReturnType<typeof createMockWorkflowExecutionData>;
}) {
const executionId = Math.random().toString(36).substring(4);
const executionId = nanoid(8);

cy.intercept('POST', '/rest/workflows/**/run', {
statusCode: 201,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
globalSetup: '<rootDir>/test/setup.ts',
globalTeardown: '<rootDir>/test/teardown.ts',
setupFilesAfterEnv: [
'n8n-workflow/test/setup.ts',
'<rootDir>/test/setup-test-folder.ts',
'<rootDir>/test/setup-mocks.ts',
'<rootDir>/test/extend-expect.ts',
Expand Down
13 changes: 4 additions & 9 deletions packages/cli/src/Ldap/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { Entry as LdapUser } from 'ldapts';
import { Filter } from 'ldapts/filters/Filter';
import { Container } from 'typedi';
import { validate } from 'jsonschema';
import { randomString } from 'n8n-workflow';

import * as Db from '@/Db';
import config from '@/config';
import { User } from '@db/entities/User';
Expand Down Expand Up @@ -38,13 +40,6 @@ export const getLdapLoginLabel = (): string => config.getEnv(LDAP_LOGIN_LABEL);
*/
export const isLdapLoginEnabled = (): boolean => config.getEnv(LDAP_LOGIN_ENABLED);

/**
* Return a random password to be assigned to the LDAP users
*/
export const randomPassword = (): string => {
return Math.random().toString(36).slice(-8);
};

/**
* Validate the structure of the LDAP configuration schema
*/
Expand Down Expand Up @@ -161,7 +156,7 @@ export const mapLdapUserToDbUser = (
Object.assign(user, data);
if (toCreate) {
user.role = 'global:member';
user.password = randomPassword();
user.password = randomString(8);
user.disabled = false;
} else {
user.disabled = true;
Expand Down Expand Up @@ -278,7 +273,7 @@ export const createLdapAuthIdentity = async (user: User, ldapId: string) => {

export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => {
const { user } = await Container.get(UserRepository).createUserWithProject({
password: randomPassword(),
password: randomString(8),
role: 'global:member',
...data,
});
Expand Down
9 changes: 2 additions & 7 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createReadStream, createWriteStream, existsSync } from 'fs';
import { pipeline } from 'stream/promises';
import replaceStream from 'replacestream';
import glob from 'fast-glob';
import { jsonParse } from 'n8n-workflow';
import { jsonParse, randomString } from 'n8n-workflow';

import config from '@/config';
import { ActiveExecutions } from '@/ActiveExecutions';
Expand Down Expand Up @@ -265,12 +265,7 @@ export class Start extends BaseCommand {

if (tunnelSubdomain === '') {
// When no tunnel subdomain did exist yet create a new random one
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
tunnelSubdomain = Array.from({ length: 24 })
.map(() =>
availableCharacters.charAt(Math.floor(Math.random() * availableCharacters.length)),
)
.join('');
tunnelSubdomain = randomString(24).toLowerCase();

this.instanceSettings.update({ tunnelSubdomain });
}
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/databases/utils/generators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { customAlphabet } from 'nanoid';
import { ALPHABET } from 'n8n-workflow';
import type { N8nInstanceType } from '@/Interfaces';

const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 16);
const nanoid = customAlphabet(ALPHABET, 16);

export function generateNanoId() {
return nanoid();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import Container from 'typedi';
import { stringify } from 'flatted';
import { NodeConnectionType, randomInt } from 'n8n-workflow';

import { mockInstance } from '@test/mocking';
import { randomInteger } from '@test-integration/random';
import { createWorkflow } from '@test-integration/db/workflows';
import { createExecution } from '@test-integration/db/executions';
import * as testDb from '@test-integration/testDb';

import { NodeConnectionType } from 'n8n-workflow';
import { mock } from 'jest-mock-extended';
import { OrchestrationService } from '@/services/orchestration.service';
import config from '@/config';
import { ExecutionRecoveryService } from '@/executions/execution-recovery.service';
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
import { InternalHooks } from '@/InternalHooks';
import { Push } from '@/push';
import { ARTIFICIAL_TASK_DATA } from '@/constants';
import { NodeCrashedError } from '@/errors/node-crashed.error';
import { WorkflowCrashedError } from '@/errors/workflow-crashed.error';
import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode';
import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow';

import type { EventMessageTypes as EventMessage } from '@/eventbus/EventMessageClasses';
import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity';
import type { Logger } from '@/Logger';

/**
Expand Down Expand Up @@ -301,7 +299,7 @@ describe('ExecutionRecoveryService', () => {
/**
* Arrange
*/
const inexistentExecutionId = randomInteger(100).toString();
const inexistentExecutionId = randomInt(100).toString();
const noMessages: EventMessage[] = [];

/**
Expand Down Expand Up @@ -373,7 +371,7 @@ describe('ExecutionRecoveryService', () => {
/**
* Arrange
*/
const inexistentExecutionId = randomInteger(100).toString();
const inexistentExecutionId = randomInt(100).toString();
const messages = setupMessages(inexistentExecutionId, 'Some workflow');

/**
Expand Down
44 changes: 13 additions & 31 deletions packages/cli/src/sso/saml/samlHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Container } from 'typedi';
import type { FlowResult } from 'samlify/types/src/flow';
import { randomString } from 'n8n-workflow';

import config from '@/config';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { User } from '@db/entities/User';
import { UserRepository } from '@db/repositories/user.repository';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { AuthError } from '@/errors/response-errors/auth.error';
import { License } from '@/License';
import { PasswordUtility } from '@/services/password.utility';

import type { SamlPreferences } from './types/samlPreferences';
import type { SamlUserAttributes } from './types/samlUserAttributes';
import type { FlowResult } from 'samlify/types/src/flow';
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
import { SAML_LOGIN_ENABLED, SAML_LOGIN_LABEL } from './constants';
import {
Expand All @@ -17,10 +24,6 @@ import {
} from '../ssoHelpers';
import { getServiceProviderConfigTestReturnUrl } from './serviceProvider.ee';
import type { SamlConfiguration } from './types/requests';
import { UserRepository } from '@db/repositories/user.repository';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { AuthError } from '@/errors/response-errors/auth.error';

/**
* Check whether the SAML feature is licensed and enabled in the instance
Expand Down Expand Up @@ -73,39 +76,18 @@ export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferen
);
};

export function generatePassword(): string {
const length = 18;
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const charsetNoNumbers = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const randomNumber = Math.floor(Math.random() * 10);
const randomUpper = charset.charAt(Math.floor(Math.random() * charsetNoNumbers.length));
const randomNumberPosition = Math.floor(Math.random() * length);
const randomUpperPosition = Math.floor(Math.random() * length);
let password = '';
for (let i = 0, n = charset.length; i < length; ++i) {
password += charset.charAt(Math.floor(Math.random() * n));
}
password =
password.substring(0, randomNumberPosition) +
randomNumber.toString() +
password.substring(randomNumberPosition);
password =
password.substring(0, randomUpperPosition) +
randomUpper +
password.substring(randomUpperPosition);
return password;
}

export async function createUserFromSamlAttributes(attributes: SamlUserAttributes): Promise<User> {
return await Container.get(UserRepository).manager.transaction(async (trx) => {
const { user } = await Container.get(UserRepository).createUserWithProject(
const randomPassword = randomString(18);
const userRepository = Container.get(UserRepository);
return await userRepository.manager.transaction(async (trx) => {
const { user } = await userRepository.createUserWithProject(
{
email: attributes.email.toLowerCase(),
firstName: attributes.firstName,
lastName: attributes.lastName,
role: 'global:member',
// generates a password that is not used or known to the user
password: await Container.get(PasswordUtility).hash(generatePassword()),
password: await Container.get(PasswordUtility).hash(randomPassword),
},
trx,
);
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { CliWorkflowOperationError, SubworkflowOperationError } from 'n8n-workflow';
import type { INode } from 'n8n-workflow';
import { STARTING_NODES } from './constants';
import { STARTING_NODES } from '@/constants';

/**
* Returns if the given id is a valid workflow id
Expand Down
10 changes: 3 additions & 7 deletions packages/cli/test/integration/PermissionChecker.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { v4 as uuid } from 'uuid';
import { Container } from 'typedi';
import type { INode, WorkflowSettings } from 'n8n-workflow';
import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
import { SubworkflowOperationError, Workflow, randomInt } from 'n8n-workflow';

import config from '@/config';
import type { User } from '@db/entities/User';
Expand All @@ -15,11 +15,7 @@ import { OwnershipService } from '@/services/ownership.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';

import { mockInstance } from '../shared/mocking';
import {
randomCredentialPayload as randomCred,
randomName,
randomPositiveDigit,
} from '../integration/shared/random';
import { randomCredentialPayload as randomCred, randomName } from '../integration/shared/random';
import { LicenseMocker } from '../integration/shared/license';
import * as testDb from '../integration/shared/testDb';
import type { SaveCredentialFunction } from '../integration/shared/types';
Expand Down Expand Up @@ -77,7 +73,7 @@ const ownershipService = mockInstance(OwnershipService);

const createWorkflow = async (nodes: INode[], workflowOwner?: User): Promise<WorkflowEntity> => {
const workflowDetails = {
id: randomPositiveDigit().toString(),
id: randomInt(1, 10).toString(),
name: 'test',
active: false,
connections: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Container } from 'typedi';
import type { Scope } from '@sentry/node';
import { Credentials } from 'n8n-core';
import { randomString } from 'n8n-workflow';

import type { ListQuery } from '@/requests';
import type { User } from '@db/entities/User';
Expand All @@ -16,7 +17,6 @@ import {
randomCredentialPayload as payload,
randomCredentialPayload,
randomName,
randomString,
} from '../shared/random';
import {
saveCredential,
Expand Down
9 changes: 2 additions & 7 deletions packages/cli/test/integration/me.api.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { Container } from 'typedi';
import { IsNull } from '@n8n/typeorm';
import validator from 'validator';
import { randomString } from 'n8n-workflow';

import config from '@/config';
import type { User } from '@db/entities/User';
import { UserRepository } from '@db/repositories/user.repository';
import { ProjectRepository } from '@db/repositories/project.repository';

import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import {
randomApiKey,
randomEmail,
randomName,
randomString,
randomValidPassword,
} from './shared/random';
import { randomApiKey, randomEmail, randomName, randomValidPassword } from './shared/random';
import * as testDb from './shared/testDb';
import * as utils from './shared/utils/';
import { addApiKey, createOwner, createUser, createUserShell } from './shared/db/users';
Expand Down
20 changes: 4 additions & 16 deletions packages/cli/test/integration/mfa/mfa.api.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import Container from 'typedi';
import { randomInt, randomString } from 'n8n-workflow';

import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import type { User } from '@db/entities/User';
import { AuthUserRepository } from '@db/repositories/authUser.repository';
import { randomPassword } from '@/Ldap/helpers';
import { TOTPService } from '@/Mfa/totp.service';

import * as testDb from '../shared/testDb';
import * as utils from '../shared/utils';
import { randomDigit, randomString, randomValidPassword, uniqueId } from '../shared/random';
import { randomValidPassword, uniqueId } from '../shared/random';
import { createUser, createUserWithMfaEnabled } from '../shared/db/users';

jest.mock('@/telemetry');
Expand Down Expand Up @@ -150,18 +150,6 @@ describe('Disable MFA setup', () => {
});

describe('Change password with MFA enabled', () => {
test('PATCH /me/password should fail due to missing MFA token', async () => {
const { user, rawPassword } = await createUserWithMfaEnabled();

const newPassword = randomPassword();

await testServer
.authAgentFor(user)
.patch('/me/password')
.send({ currentPassword: rawPassword, newPassword })
.expect(400);
});

test('POST /change-password should fail due to missing MFA token', async () => {
await createUserWithMfaEnabled();

Expand All @@ -185,7 +173,7 @@ describe('Change password with MFA enabled', () => {
.send({
password: newPassword,
token: resetPasswordToken,
mfaToken: randomDigit(),
mfaToken: randomInt(10),
})
.expect(404);
});
Expand Down Expand Up @@ -226,7 +214,7 @@ describe('Change password with MFA enabled', () => {

describe('Login', () => {
test('POST /login with email/password should succeed when mfa is disabled', async () => {
const password = randomPassword();
const password = randomString(8);

const user = await createUser({ password });

Expand Down
Loading

0 comments on commit 16d3cfe

Please sign in to comment.