From 50167d4f9eca015454706f7bd55de94fff124a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 30 Jul 2024 10:42:50 +0100 Subject: [PATCH] chore: origin middleware (#7695) https://linear.app/unleash/issue/2-2489/create-a-first-iteration-of-an-origin-middleware-that-logs-ui-and-api Small spike around what the first iteration of an "origin middleware" would look like. No strong feelings all around, so feel free to tell me this is all wrong and we should go a different route. However diving a little bit into it personally helps me wrap my head around it, so it may help you too. --- .../__snapshots__/create-config.test.ts.snap | 1 + src/lib/app.ts | 3 + src/lib/middleware/origin-middleware.test.ts | 66 +++++++++++++++++++ src/lib/middleware/origin-middleware.ts | 29 ++++++++ src/lib/types/experimental.ts | 7 +- src/server-dev.ts | 1 + 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/lib/middleware/origin-middleware.test.ts create mode 100644 src/lib/middleware/origin-middleware.ts diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index aa6eb447c1..4dc64bfa8b 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -144,6 +144,7 @@ exports[`should create default config 1`] = ` }, "migrationLock": true, "navigationSidebar": true, + "originMiddleware": false, "outdatedSdksBanner": false, "parseProjectFromSession": false, "personalAccessTokensKillSwitch": false, diff --git a/src/lib/app.ts b/src/lib/app.ts index d2843ee535..6de9ec9fc4 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -30,6 +30,7 @@ import { catchAllErrorHandler } from './middleware/catch-all-error-handler'; import NotFoundError from './error/notfound-error'; import { bearerTokenMiddleware } from './middleware/bearer-token-middleware'; import { auditAccessMiddleware } from './middleware'; +import { originMiddleware } from './middleware/origin-middleware'; export default async function getApp( config: IUnleashConfig, @@ -177,6 +178,8 @@ export default async function getApp( rbacMiddleware(config, stores, services.accessService), ); + app.use(`${baseUriPath}/api/admin`, originMiddleware(config)); + app.use(`${baseUriPath}/api/admin`, auditAccessMiddleware(config)); app.use( `${baseUriPath}/api/admin`, diff --git a/src/lib/middleware/origin-middleware.test.ts b/src/lib/middleware/origin-middleware.test.ts new file mode 100644 index 0000000000..dd51b8fb95 --- /dev/null +++ b/src/lib/middleware/origin-middleware.test.ts @@ -0,0 +1,66 @@ +import { originMiddleware } from './origin-middleware'; +import type { IUnleashConfig } from '../types'; +import { createTestConfig } from '../../test/config/test-config'; +import type { Request, Response } from 'express'; + +const TEST_UNLEASH_TOKEN = 'TEST_UNLEASH_TOKEN'; +const TEST_USER_AGENT = 'TEST_USER_AGENT'; + +describe('originMiddleware', () => { + const req = { headers: {}, path: '' } as Request; + const res = {} as Response; + const next = jest.fn(); + const loggerMock = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + }; + const getLogger = jest.fn(() => loggerMock); + + let config: IUnleashConfig; + + beforeEach(() => { + config = createTestConfig({ + getLogger, + experimental: { + flags: { + originMiddleware: true, + }, + }, + }); + }); + + it('should call next', () => { + const middleware = originMiddleware(config); + + middleware(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it('should log UI request', () => { + const middleware = originMiddleware(config); + + middleware(req, res, next); + + expect(loggerMock.debug).toHaveBeenCalledWith('UI request', { + method: req.method, + }); + }); + + it('should log API request', () => { + const middleware = originMiddleware(config); + + req.headers.authorization = TEST_UNLEASH_TOKEN; + req.headers['user-agent'] = TEST_USER_AGENT; + + middleware(req, res, next); + + expect(loggerMock.debug).toHaveBeenCalledWith('API request', { + method: req.method, + userAgent: TEST_USER_AGENT, + }); + }); +}); diff --git a/src/lib/middleware/origin-middleware.ts b/src/lib/middleware/origin-middleware.ts new file mode 100644 index 0000000000..dee875a97d --- /dev/null +++ b/src/lib/middleware/origin-middleware.ts @@ -0,0 +1,29 @@ +import type { Request, Response, NextFunction } from 'express'; +import type { IUnleashConfig } from '../types'; + +export const originMiddleware = ({ + getLogger, + flagResolver, +}: Pick) => { + const logger = getLogger('/middleware/origin-middleware.ts'); + logger.debug('Enabling origin middleware'); + + return (req: Request, _: Response, next: NextFunction) => { + if (!flagResolver.isEnabled('originMiddleware')) { + return next(); + } + + const isUI = !req.headers.authorization; + + if (isUI) { + logger.debug('UI request', { method: req.method }); + } else { + logger.debug('API request', { + method: req.method, + userAgent: req.headers['user-agent'], + }); + } + + next(); + }; +}; diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index f045dcc441..4d09867b7b 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -70,7 +70,8 @@ export type IFlagKey = | 'insightsV2' | 'integrationEvents' | 'featureCollaborators' - | 'improveCreateFlagFlow'; + | 'improveCreateFlagFlow' + | 'originMiddleware'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -339,6 +340,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_IMPROVE_CREATE_FLAG_FLOW, false, ), + originMiddleware: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_ORIGIN_MIDDLEWARE, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/server-dev.ts b/src/server-dev.ts index 0ef6ff71bd..ba772be3d0 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -60,6 +60,7 @@ process.nextTick(async () => { integrationEvents: true, featureCollaborators: true, improveCreateFlagFlow: true, + originMiddleware: true, }, }, authentication: {