diff --git a/packages/cli/src/PublicApi/v1/index.ts b/packages/cli/src/PublicApi/v1/index.ts index 2c601905e4..97eca7b8d5 100644 --- a/packages/cli/src/PublicApi/v1/index.ts +++ b/packages/cli/src/PublicApi/v1/index.ts @@ -9,6 +9,9 @@ import path = require('path'); import express = require('express'); +import { OpenAPIV3 } from 'express-openapi-validator/dist/framework/types'; +import { Db } from '../..'; + export interface N8nApp { app: Application; } @@ -23,6 +26,29 @@ export const getRoutes = (): express.Router => { operationHandlers: path.join(__dirname), validateRequests: true, // (default) validateApiSpec: true, + validateSecurity: { + handlers: { + ApiKeyAuth: async (req, scopes, schema: OpenAPIV3.ApiKeySecurityScheme) => { + + const apiKey = req.headers[schema.name.toLowerCase()]; + + const user = await Db.collections.User!.find({ + where: { + apiKey, + }, + relations: ['globalRole'], + }); + + if (!user.length) { + return false; + } + + req.user = user[0]; + + return true; + }, + }, + }, })); //add error handler diff --git a/packages/cli/src/PublicApi/v1/routes/Users/index.ts b/packages/cli/src/PublicApi/v1/routes/Users/index.ts index baab16d0cd..4ff4d7da66 100644 --- a/packages/cli/src/PublicApi/v1/routes/Users/index.ts +++ b/packages/cli/src/PublicApi/v1/routes/Users/index.ts @@ -7,7 +7,6 @@ export = { res.json({ success: true}); }, deleteUser: async (req: UserRequest.Delete, res: express.Response) => { - console.log('aja') res.json({ success: true }); }, getUser: async (req: UserRequest.Get, res: express.Response) => { diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 45b9109546..42a1adf9e2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -57,7 +57,7 @@ import * as clientOAuth1 from 'oauth-1.0a'; import { RequestOptions } from 'oauth-1.0a'; import * as csrf from 'csrf'; import * as requestPromise from 'request-promise-native'; -import { createHmac } from 'crypto'; +import { createHmac, randomBytes } from 'crypto'; // IMPORTANT! Do not switch to anther bcrypt library unless really necessary and // tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ... import { compare } from 'bcryptjs'; @@ -564,6 +564,24 @@ class App { // Public API // ---------------------------------------- + + //test routes to create/regenerate/delete token + //NOTE: Only works with admin role + //This should be within the user's management user scope + this.app.post('/token', async (req: express.Request, res: express.Response) => { + const ramdonToken = randomBytes(20).toString('hex'); + //@ts-ignore + await Db.collections.User!.update({ globalRole: 1 }, { apiKey: ramdonToken }); + return ResponseHelper.sendSuccessResponse(res, { token: ramdonToken }, true, 200); + }); + + this.app.delete('/token', async (req: express.Request, res: express.Response) => { + //@ts-ignore + await Db.collections.User!.update({ globalRole: 1 }, { apiKey: null }); + return ResponseHelper.sendSuccessResponse(res, {}, true, 204); + }); + + this.app.use(`/${this.publicApiEndpoint}`, publicApiv1Routes.getRoutes()); // Parse cookies for easier access diff --git a/packages/cli/src/UserManagement/auth/jwt.ts b/packages/cli/src/UserManagement/auth/jwt.ts index 2dca697801..83bc4ca79f 100644 --- a/packages/cli/src/UserManagement/auth/jwt.ts +++ b/packages/cli/src/UserManagement/auth/jwt.ts @@ -6,7 +6,7 @@ import { Response } from 'express'; import { createHash } from 'crypto'; import { Db } from '../..'; import { AUTH_COOKIE_NAME } from '../../constants'; -import { JwtToken, JwtPayload } from '../Interfaces'; +import { JwtPayload, JwtToken } from '../Interfaces'; import { User } from '../../databases/entities/User'; import config = require('../../../config'); diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index b345b26534..1133fe6b6d 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -124,6 +124,9 @@ export class User { this.updatedAt = new Date(); } + @Column({ type: String, nullable: true }) + apiKey?: string | null; + /** * Whether the user is pending setup completion. */ diff --git a/packages/cli/src/databases/sqlite/migrations/1647888658687-AddAPIKeyColumn.ts b/packages/cli/src/databases/sqlite/migrations/1647888658687-AddAPIKeyColumn.ts new file mode 100644 index 0000000000..6927e7f8f5 --- /dev/null +++ b/packages/cli/src/databases/sqlite/migrations/1647888658687-AddAPIKeyColumn.ts @@ -0,0 +1,19 @@ +import {MigrationInterface, QueryRunner} from 'typeorm'; +import * as config from '../../../../config'; + +export class AddAPIKeyColumn1647888658687 implements MigrationInterface { + name = 'AddAPIKeyColumn1647888658687'; + + async up(queryRunner: QueryRunner): Promise { + const tablePrefix = config.get('database.tablePrefix'); + await queryRunner.query(`ALTER TABLE "${tablePrefix}user" ADD apiKey varchar`); + } + + async down(queryRunner: QueryRunner): Promise { + const tablePrefix = config.get('database.tablePrefix'); + await queryRunner.query(`ALTER TABLE "${tablePrefix}user" RENAME TO "temporary_user"`); + await queryRunner.query(`CREATE TABLE "${tablePrefix}user" ("id" varchar PRIMARY KEY NOT NULL, "email" varchar(255), "firstName" varchar(32), "lastName" varchar(32), "password" varchar, "resetPasswordToken" varchar, "resetPasswordTokenExpiration" integer DEFAULT (NULL), "personalizationAnswers" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "globalRoleId" integer NOT NULL, CONSTRAINT "FK_f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES "role" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "${tablePrefix}user"("id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId") SELECT "id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId" FROM "temporary_user"`); + await queryRunner.query(`DROP TABLE "temporary_user"`); + } +} diff --git a/packages/cli/src/databases/sqlite/migrations/index.ts b/packages/cli/src/databases/sqlite/migrations/index.ts index fe2a71b682..504a5b562b 100644 --- a/packages/cli/src/databases/sqlite/migrations/index.ts +++ b/packages/cli/src/databases/sqlite/migrations/index.ts @@ -11,6 +11,7 @@ import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn'; import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWorkflowCredentials'; import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes'; import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement'; +import { AddAPIKeyColumn1647888658687 } from './1647888658687-AddAPIKeyColumn'; const sqliteMigrations = [ InitialMigration1588102412422, @@ -24,6 +25,7 @@ const sqliteMigrations = [ UpdateWorkflowCredentials1630330987096, AddExecutionEntityIndexes1644421939510, CreateUserManagement1646992772331, + AddAPIKeyColumn1647888658687, ]; export { sqliteMigrations };