diff --git a/src/lib/app.ts b/src/lib/app.ts index a593287947..d9fe211fb8 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -29,6 +29,7 @@ import { unless } from './middleware/unless-middleware'; 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'; export default async function getApp( config: IUnleashConfig, @@ -176,6 +177,7 @@ export default async function getApp( rbacMiddleware(config, stores, services.accessService), ); + app.use(`${baseUriPath}/api/admin`, auditAccessMiddleware(config)); app.use( `${baseUriPath}/api/admin`, maintenanceMiddleware(config, services.maintenanceService), diff --git a/src/lib/middleware/audit-middleware.test.ts b/src/lib/middleware/audit-middleware.test.ts new file mode 100644 index 0000000000..d66a8d730b --- /dev/null +++ b/src/lib/middleware/audit-middleware.test.ts @@ -0,0 +1,42 @@ +import { auditAccessMiddleware } from './audit-middleware'; +import { createTestConfig } from '../../test/config/test-config'; +import express from 'express'; +import noAuthentication from './no-authentication'; +import type { IAuthRequest } from '../routes/unleash-types'; +import type { IAuditUser } from '../types'; +import supertest from 'supertest'; + +const config = createTestConfig(); + +describe('auditMiddleware testing', () => { + test('Adds username and id from an IAuthRequest', async () => { + const middleware = auditAccessMiddleware(config); + const app = express(); + noAuthentication('', app); + app.use('', middleware); + let audit: IAuditUser | undefined; + app.get('/api/admin/test', (req: IAuthRequest, res) => { + audit = req.audit; + res.status(200).end(); + }); + const request = supertest(app); + await request.get('/api/admin/test').expect(200); + expect(audit).toBeDefined(); + expect(audit!.id).toBe(-1); + expect(audit!.username).toBe('unknown'); + expect(audit!.ip).toBe('::ffff:127.0.0.1'); + }); + test('If no auth in place, does not add the audit object', async () => { + const middleware = auditAccessMiddleware(config); + const app = express(); + app.use('', middleware); + let audit: IAuditUser | undefined; + app.get('/api/admin/test', (req: IAuthRequest, res) => { + audit = req.audit; + res.status(200).end(); + }); + const request = supertest(app); + await request.get('/api/admin/test').expect(200); + expect(audit).toBeUndefined(); + }); +}); diff --git a/src/lib/middleware/audit-middleware.ts b/src/lib/middleware/audit-middleware.ts new file mode 100644 index 0000000000..e4adaecd9f --- /dev/null +++ b/src/lib/middleware/audit-middleware.ts @@ -0,0 +1,17 @@ +import type { IUnleashConfig } from '../types'; +import type { IApiRequest, IAuthRequest } from '../routes/unleash-types'; +import { extractAuditInfo } from '../util'; + +export const auditAccessMiddleware = ({ + getLogger, +}: Pick): any => { + const logger = getLogger('/middleware/audit-middleware.ts'); + return (req: IAuthRequest | IApiRequest, _res, next) => { + if (!req.user) { + logger.info('Could not find user'); + } else { + req.audit = extractAuditInfo(req); + } + next(); + }; +}; diff --git a/src/lib/middleware/index.ts b/src/lib/middleware/index.ts index 31b68401ff..34be187403 100644 --- a/src/lib/middleware/index.ts +++ b/src/lib/middleware/index.ts @@ -1,4 +1,5 @@ export * from './api-token-middleware'; +export * from './audit-middleware'; export * from './conditional-middleware'; export * from './content_type_checker'; export * from './cors-origin-middleware'; diff --git a/src/lib/routes/unleash-types.ts b/src/lib/routes/unleash-types.ts index 91515d4c22..4917981b61 100644 --- a/src/lib/routes/unleash-types.ts +++ b/src/lib/routes/unleash-types.ts @@ -1,5 +1,5 @@ import type { Request } from 'express'; -import type { IUser } from '../types/user'; +import type { IAuditUser, IUser } from '../types/user'; import type { IApiUser } from '../types'; export interface IAuthRequest< @@ -11,6 +11,7 @@ export interface IAuthRequest< user: IUser; logout: (() => void) | ((callback: (err?: any) => void) => void); session: any; + audit?: IAuditUser; } export interface IApiRequest< @@ -22,6 +23,7 @@ export interface IApiRequest< user: IApiUser; logout: (() => void) | ((callback: (err?: any) => void) => void); session: any; + audit?: IAuditUser; } export interface RequestBody extends Express.Request { diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 4df951feec..7ac7f8edfc 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -22,7 +22,7 @@ import { SYSTEM_USER, } from './types'; -import User, { type IUser } from './types/user'; +import User, { type IAuditUser, type IUser } from './types/user'; import ApiUser, { type IApiUser } from './types/api-user'; import { type Logger, LogLevel } from './logger'; import AuthenticationRequired from './types/authentication-required'; @@ -220,6 +220,7 @@ export type { IUnleashConfig, IUser, IApiUser, + IAuditUser, IUnleashServices, IAuthRequest, IApiRequest, diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts index dd5dd15ee2..2b3590e177 100644 --- a/src/lib/types/user.ts +++ b/src/lib/types/user.ts @@ -37,6 +37,12 @@ export interface IProjectUser extends IUser { addedAt: Date; } +export interface IAuditUser { + id: number; + username: string; + ip?: string; +} + export default class User implements IUser { isAPI: boolean = false; diff --git a/src/lib/util/extract-user.ts b/src/lib/util/extract-user.ts index 26f617c2db..3c06cb622d 100644 --- a/src/lib/util/extract-user.ts +++ b/src/lib/util/extract-user.ts @@ -3,6 +3,7 @@ import type { IApiRequest, IApiUser, IAuthRequest, + IAuditUser, IUser, } from '../server-impl'; @@ -26,3 +27,11 @@ export const extractUserInfo = (req: IAuthRequest | IApiRequest) => ({ id: extractUserId(req), username: extractUsername(req), }); + +export const extractAuditInfo = ( + req: IAuthRequest | IApiRequest, +): IAuditUser => ({ + id: extractUserId(req), + username: extractUsername(req), + ip: req.ip, +});