Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ ServiceNow basic auth and bug fix #2712

Merged
merged 23 commits into from
Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
075b6cf
✨ Support basic auth for ServiceNow
pemontto Jan 20, 2022
a1f1036
🐛 Support ServiceNow sysparm_fields as string
pemontto Jan 20, 2022
f39e628
Merge branch 'master' of https://github.com/n8n-io/n8n into serviceno…
michael-radency Mar 31, 2022
0e255d3
:zap: credential test for basic auth
michael-radency Mar 31, 2022
c3c4434
Merge branch 'master' of https://github.com/n8n-io/n8n into serviceno…
michael-radency Apr 1, 2022
9d703e3
fix(Google Tasks Node): Fix "Show Completed" option and hide title fi…
that-one-tom Apr 1, 2022
c89d2b1
feat(Mocean Node): Add "Delivery Report URL" option and credential te…
RicardoE105 Apr 1, 2022
0b08be1
feat(Emelia Node): Add Campaign > Duplicate functionality (#3000)
chlec Apr 1, 2022
b66d5ab
:zap: fix nodelinter issues, added hint to field option
michael-radency Apr 1, 2022
8a94f1e
fix(GraphQL Node)!: Correctly report errors returned by the API (#3071)
michael-radency Apr 1, 2022
39a6f41
feat(FTP Node): Add option to recursively create directories on renam…
rhyswilliamsza Apr 1, 2022
984f62d
feat(Microsoft Teams Node): Add chat message support (#2635)
pemontto Apr 1, 2022
0a75539
feat(Mautic Node): Add credential test and allow trailing slash in ho…
Joffcom Apr 1, 2022
7625421
test: Fix randomly failing UM tests (#3061)
ivov Apr 1, 2022
5f44b0d
fix(NocoDB Node): Fix pagination (#3081)
Joffcom Apr 1, 2022
6bbb4df
feat(Strava Node): Add "Get Streams" operation (#2582)
lfcipriani Apr 1, 2022
4182285
:zap: Improvements
RicardoE105 Apr 1, 2022
c50d04a
fix(core): Fix crash on webhook when last node did not return data
janober Apr 2, 2022
1018146
fix(Salesforce Node): Fix issue that "status" did not get used for Ca…
Apr 2, 2022
754c44a
:bug: Fix issue with credentials
RicardoE105 Apr 2, 2022
121da6d
:twisted_rightwards_arrows: Merge branch 'servicenow-basicauth' of ht…
janober Apr 2, 2022
2dce248
:zap: Fix basicAuth
janober Apr 2, 2022
68c2fc6
:zap: Reset default
janober Apr 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 99 additions & 74 deletions packages/cli/BREAKING-CHANGES.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/cli/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ const config = convict({
logs: {
level: {
doc: 'Log output level',
format: ['error', 'warn', 'info', 'verbose', 'debug'],
format: ['error', 'warn', 'info', 'verbose', 'debug', 'silent'],
default: 'info',
env: 'N8N_LOG_LEVEL',
},
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"start:default": "cd bin && ./n8n",
"start:windows": "cd bin && n8n",
"test": "npm run test:sqlite",
"test:sqlite": "export DB_TYPE=sqlite && jest --forceExit",
"test:sqlite": "export N8N_LOG_LEVEL='silent'; export DB_TYPE=sqlite; jest",
"test:postgres": "export DB_TYPE=postgresdb && jest",
"test:mysql": "export DB_TYPE=mysqldb && jest",
"watch": "tsc --watch",
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ class Logger implements ILogger {
private logger: winston.Logger;

constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const level = config.get('logs.level');
const level = config.get('logs.level') as string;

// eslint-disable-next-line @typescript-eslint/no-shadow
const output = (config.get('logs.output') as string).split(',').map((output) => output.trim());

this.logger = winston.createLogger({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
level,
silent: level === 'silent',
});

if (output.includes('console')) {
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/ResponseHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,13 @@ export function sendSuccessResponse(
}
}

export function sendErrorResponse(res: Response, error: ResponseError, shouldLog = true) {
export function sendErrorResponse(res: Response, error: ResponseError) {
let httpStatusCode = 500;
if (error.httpStatusCode) {
httpStatusCode = error.httpStatusCode;
}

shouldLog = !process.argv[1].split('/').includes('jest');

if (process.env.NODE_ENV !== 'production' && shouldLog) {
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
console.error('ERROR RESPONSE');
console.error(error);
}
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/src/UserManagement/UserManagementHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import { Workflow } from 'n8n-workflow';
import { In, IsNull, Not } from 'typeorm';
import express = require('express');
import { compare } from 'bcryptjs';

import { PublicUser } from './Interfaces';
import { Db, GenericHelpers, ResponseHelper } from '..';
import { Db, ResponseHelper } from '..';
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, User } from '../databases/entities/User';
import { Role } from '../databases/entities/Role';
import { AuthenticatedRequest } from '../requests';
Expand Down Expand Up @@ -216,3 +218,20 @@ export function isPostUsersId(req: express.Request, restEndpoint: string): boole
export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest {
return request.user !== undefined;
}

// ----------------------------------
// hashing
// ----------------------------------

export async function compareHash(str: string, hash: string): Promise<boolean | undefined> {
try {
return await compare(str, hash);
} catch (error) {
if (error instanceof Error && error.message.includes('Invalid salt version')) {
error.message +=
'. Comparison against unhashed string. Please check that the value compared against has been hashed.';
}

throw new Error(error);
}
}
6 changes: 3 additions & 3 deletions packages/cli/src/UserManagement/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Request, Response } from 'express';
import { compare } from 'bcryptjs';
import { IDataObject } from 'n8n-workflow';
import { Db, ResponseHelper } from '../..';
import { AUTH_COOKIE_NAME } from '../../constants';
import { issueCookie, resolveJwt } from '../auth/jwt';
import { N8nApp, PublicUser } from '../Interfaces';
import { isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper';
import { compareHash, isInstanceOwnerSetup, sanitizeUser } from '../UserManagementHelper';
import { User } from '../../databases/entities/User';
import type { LoginRequest } from '../../requests';

Expand Down Expand Up @@ -43,7 +42,8 @@ export function authenticationMethods(this: N8nApp): void {
} catch (error) {
throw new Error('Unable to access database.');
}
if (!user || !user.password || !(await compare(req.body.password, user.password))) {

if (!user || !user.password || !(await compareHash(req.body.password, user.password))) {
// password is empty until user signs up
const error = new Error('Wrong username or password. Do you have caps lock on?');
// @ts-ignore
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/WebhookHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ export async function executeWebhook(
if (returnData.data!.main[0]![0] === undefined) {
responseCallback(new Error('No item to return got found.'), {});
didSendResponse = true;
return undefined;
}

data = returnData.data!.main[0]![0].json;
Expand Down Expand Up @@ -602,11 +603,13 @@ export async function executeWebhook(
if (data === undefined) {
responseCallback(new Error('No item to return got found.'), {});
didSendResponse = true;
return undefined;
}

if (data.binary === undefined) {
responseCallback(new Error('No binary data to return got found.'), {});
didSendResponse = true;
return undefined;
}

const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/integration/auth.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { randomEmail, randomValidPassword, randomName } from './shared/random';
import { getGlobalOwnerRole } from './shared/testDb';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let globalOwnerRole: Role;

let app: express.Application;
Expand All @@ -35,7 +37,7 @@ beforeEach(async () => {
email: TEST_USER.email,
firstName: TEST_USER.firstName,
lastName: TEST_USER.lastName,
password: hashSync(TEST_USER.password, genSaltSync(10)),
password: TEST_USER.password,
globalRole: globalOwnerRole,
});

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/integration/auth.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import express = require('express');

import * as request from 'supertest';
import {
REST_PATH_SEGMENT,
ROUTES_REQUIRING_AUTHORIZATION,
ROUTES_REQUIRING_AUTHENTICATION,
} from './shared/constants';

import * as utils from './shared/utils';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/integration/credentials.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Role } from '../../src/databases/entities/Role';
import { User } from '../../src/databases/entities/User';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';
let saveCredential: SaveCredentialFunction;
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/test/integration/me.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Role } from '../../src/databases/entities/Role';
import { randomValidPassword, randomEmail, randomName, randomString } from './shared/random';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';
let globalOwnerRole: Role;
Expand Down Expand Up @@ -275,7 +277,7 @@ describe('Member', () => {
test('PATCH /me/password should succeed with valid inputs', async () => {
const memberPassword = randomValidPassword();
const member = await testDb.createUser({
password: hashSync(memberPassword, genSaltSync(10)),
password: memberPassword,
});
const authMemberAgent = utils.createAgent(app, { auth: true, user: member });

Expand Down
6 changes: 4 additions & 2 deletions packages/cli/test/integration/owner.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
randomInvalidPassword,
} from './shared/random';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';

Expand Down Expand Up @@ -82,7 +84,7 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()

test('POST /owner should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) {
const response = await authOwnerAgent.post('/owner').send(invalidPayload);
Expand All @@ -92,7 +94,7 @@ test('POST /owner should fail with invalid inputs', async () => {

test('POST /owner/skip-setup should persist skipping setup to the DB', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

const response = await authOwnerAgent.post('/owner/skip-setup').send();

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/integration/passwordReset.endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import { Role } from '../../src/databases/entities/Role';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let globalOwnerRole: Role;
let testDbName = '';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/integration/shared/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const randomDigit = () => Math.floor(Math.random() * 10);
const randomUppercaseLetter = () => chooseRandomly('ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''));

export const randomValidPassword = () =>
randomString(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH) + randomUppercaseLetter() + randomDigit();
randomString(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH - 2) + randomUppercaseLetter() + randomDigit();

export const randomInvalidPassword = () =>
chooseRandomly([
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/test/integration/shared/testDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { sqliteMigrations } from '../../../src/databases/sqlite/migrations';
import type { Role } from '../../../src/databases/entities/Role';
import type { User } from '../../../src/databases/entities/User';
import type { CredentialPayload } from './types';
import { genSaltSync, hashSync } from 'bcryptjs';

/**
* Initialize one test DB per suite run, with bootstrap connection if needed.
Expand Down Expand Up @@ -188,7 +189,7 @@ export async function createUser(attributes: Partial<User> = {}): Promise<User>
const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
const user = {
email: email ?? randomEmail(),
password: password ?? randomValidPassword(),
password: hashSync(password ?? randomValidPassword(), genSaltSync(10)),
firstName: firstName ?? randomName(),
lastName: lastName ?? randomName(),
globalRole: globalRole ?? (await getGlobalMemberRole()),
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/integration/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ export function initTestServer({
return testServer.app;
}

/**
* Pre-requisite: Mock the telemetry module before calling.
*/
export function initTestTelemetry() {
const mockNodeTypes = { nodeTypes: {} } as INodeTypes;

void InternalHooksManager.init('test-instance-id', 'test-version', mockNodeTypes);

jest.spyOn(Telemetry.prototype, 'track').mockResolvedValue();
}

/**
Expand All @@ -117,10 +118,9 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
// ----------------------------------

/**
* Initialize a silent logger for test runs.
* Initialize a logger for test runs.
*/
export function initTestLogger() {
config.set('logs.output', 'file'); // declutter console output
LoggerProxy.init(getLogger());
}

Expand Down
16 changes: 9 additions & 7 deletions packages/cli/test/integration/users.endpoints.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express = require('express');
import validator from 'validator';
import { v4 as uuid } from 'uuid';
import { compare } from 'bcryptjs';
import { compare, genSaltSync, hashSync } from 'bcryptjs';

import { Db } from '../../src';
import config = require('../../config');
Expand All @@ -18,6 +18,8 @@ import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
import * as utils from './shared/utils';
import * as testDb from './shared/testDb';

jest.mock('../../src/telemetry');

let app: express.Application;
let testDbName = '';
let globalOwnerRole: Role;
Expand Down Expand Up @@ -404,7 +406,7 @@ test('POST /users/:id should fail with invalid inputs', async () => {
}
});

test.skip('POST /users/:id should fail with already accepted invite', async () => {
test('POST /users/:id should fail with already accepted invite', async () => {
const authlessAgent = utils.createAgent(app);

const globalMemberRole = await Db.collections.Role!.findOneOrFail({
Expand All @@ -414,7 +416,7 @@ test.skip('POST /users/:id should fail with already accepted invite', async () =

const shell = await Db.collections.User!.save({
email: randomEmail(),
password: randomValidPassword(), // simulate accepted invite
password: hashSync(randomValidPassword(), genSaltSync(10)), // simulate accepted invite
globalRole: globalMemberRole,
});

Expand All @@ -424,7 +426,7 @@ test.skip('POST /users/:id should fail with already accepted invite', async () =
inviterId: INITIAL_TEST_USER.id,
firstName: randomName(),
lastName: randomName(),
password: newPassword,
password: randomValidPassword(),
});

expect(response.statusCode).toBe(400);
Expand Down Expand Up @@ -458,7 +460,7 @@ test('POST /users should fail if user management is disabled', async () => {
expect(response.statusCode).toBe(500);
});

test.skip('POST /users should email invites and create user shells', async () => {
test('POST /users should email invites and create user shells', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

Expand Down Expand Up @@ -502,7 +504,7 @@ test.skip('POST /users should email invites and create user shells', async () =>
}
});

test.skip('POST /users should fail with invalid inputs', async () => {
test('POST /users should fail with invalid inputs', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

Expand All @@ -525,7 +527,7 @@ test.skip('POST /users should fail with invalid inputs', async () => {
}
});

test.skip('POST /users should ignore an empty payload', async () => {
test('POST /users should ignore an empty payload', async () => {
const owner = await Db.collections.User!.findOneOrFail();
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export class MauticOAuth2Api implements ICredentialType {
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: '={{$self["url"]}}/oauth/v2/authorize',
default: '={{$self["url"].endsWith("/") ? $self["url"].slice(0, -1) : $self["url"]}}/oauth/v2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: '={{$self["url"]}}/oauth/v2/token',
default: '={{$self["url"].endsWith("/") ? $self["url"].slice(0, -1) : $self["url"]}}/oauth/v2/token',
required: true,
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class MicrosoftTeamsOAuth2Api implements ICredentialType {
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'openid offline_access User.ReadWrite.All Group.ReadWrite.All',
default: 'openid offline_access User.ReadWrite.All Group.ReadWrite.All Chat.ReadWrite',
},
];
}
Loading