From 3db23e512bf5e71df07f300804446e0fefea01f4 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 10 Dec 2024 10:07:11 -0600 Subject: [PATCH] apply requested changes Signed-off-by: Paul Schultz --- docs/backend-system/core-services/auditor.md | 35 +-- .../src/entrypoints/auditor/Auditor.test.ts | 130 ++--------- .../src/entrypoints/auditor/Auditor.ts | 207 ++++++++---------- .../auditor/auditorServiceFactory.ts | 95 +++----- .../src/entrypoints/auditor/index.ts | 6 +- packages/backend-plugin-api/report.api.md | 1 - .../services/definitions/AuditorService.ts | 13 +- .../next/services/MockAuditorService.test.ts | 116 +++++----- .../src/next/services/MockAuditorService.ts | 40 +++- .../src/next/services/mockServices.ts | 21 +- plugins/catalog-backend/README.md | 6 + .../src/service/createRouter.ts | 43 ++-- plugins/scaffolder-backend/README.md | 3 + .../src/scaffolder/tasks/StorageTaskBroker.ts | 4 +- .../src/scaffolder/tasks/TaskWorker.ts | 2 +- .../scaffolder-backend/src/service/router.ts | 32 ++- 16 files changed, 316 insertions(+), 438 deletions(-) diff --git a/docs/backend-system/core-services/auditor.md b/docs/backend-system/core-services/auditor.md index 1a4ed43f76..c356e43bd2 100644 --- a/docs/backend-system/core-services/auditor.md +++ b/docs/backend-system/core-services/auditor.md @@ -30,10 +30,9 @@ The `auditorServiceFactory` creates an `Auditor` instance for the root context a The Auditor Service is designed for recording security-relevant events that require special attention or are subject to compliance regulations. These events often involve actions like: -- User authentication and authorization +- User session management - Data access and modification - System configuration changes -- Security policy enforcement For general application logging that is not security-critical, you should use the standard `LoggerService` provided by Backstage. This helps to keep your audit logs focused and relevant. @@ -63,10 +62,11 @@ export async function createRouter( // ... process the request await auditorEvent.success(); - res.status(200).send('Success!'); + res.status(200).json({ message: 'Succeeded!' }); } catch (error) { await auditorEvent.fail({ error }); - res.status(500).send('Error!'); + res.status(500).json({ message: 'Failed!' }); + throw error; } }); @@ -102,30 +102,3 @@ backend: ``` By default, console logging is enabled. You can disable it by setting the `enabled` flag to `false`. - -## Advanced Usage - -### Customizing the Auditor Service Factory - -The `auditorServiceFactoryWithOptions` function allows you to create an auditor service factory with custom transports and formats. This is useful if you need to integrate with a different logging system or modify the default logging behavior. - -Here's an example of how to create a custom auditor service factory: - -```typescript -import { auditorServiceFactoryWithOptions } from '@backstage/backend-defaults/auditor'; -import winston from 'winston'; - -const myAuditorServiceFactory = auditorServiceFactoryWithOptions({ - transports: () => { - return [new winston.transports.File({ filename: 'my-audit.log' })]; - }, - format: () => { - return winston.format.combine( - winston.format.timestamp(), - winston.format.json(), - ); - }, -}); -``` - -This example creates a factory that logs to a file named `my-audit.log` and uses a JSON format for the log messages. You can then use this factory in your plugin to create an auditor service with the desired configuration. diff --git a/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts b/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts index 95d16c648a..4324b4aa6b 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts @@ -18,66 +18,24 @@ import { mockServices } from '@backstage/backend-test-utils'; import { format } from 'logform'; import { MESSAGE } from 'triple-beam'; import Transport from 'winston-transport'; -import { Auditor } from './Auditor'; +import { DefaultAuditorService, DefaultRootAuditorService } from './Auditor'; describe('Auditor', () => { it('creates a auditor instance with default options', () => { - const auditor = Auditor.create(); - expect(auditor).toBeInstanceOf(Auditor); + const auditor = DefaultRootAuditorService.create(); + expect(auditor).toBeInstanceOf(DefaultRootAuditorService); }); it('creates a child logger', () => { - const auditor = Auditor.create(); - const childLogger = auditor.child({ plugin: 'test-plugin' }); - expect(childLogger).toBeInstanceOf(Auditor); - }); - - it('should error without plugin service', async () => { - const auditor = Auditor.create(); - await expect( - auditor.createEvent({ - eventId: 'test-event', - }), - ).rejects.toThrow( - `The core service 'plugin' was not provided during the auditor's instantiation`, - ); - }); - - it('should error without auth service', async () => { - const pluginId = 'test-plugin'; - - const auditor = Auditor.create({ - plugin: { - getId: () => pluginId, - }, - }); - - await expect( - auditor.createEvent({ - eventId: 'test-event', - }), - ).rejects.toThrow( - `The core service 'auth' was not provided during the auditor's instantiation`, - ); - }); - - it('should error without httpAuth service', async () => { - const pluginId = 'test-plugin'; - - const auditor = Auditor.create({ - plugin: { - getId: () => pluginId, - }, + const auditor = DefaultRootAuditorService.create(); + const childLogger = auditor.forPlugin({ auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + plugin: { + getId: () => 'test-plugin', + }, }); - - await expect( - auditor.createEvent({ - eventId: 'test-event', - }), - ).rejects.toThrow( - `The core service 'httpAuth' was not provided during the auditor's instantiation`, - ); + expect(childLogger).toBeInstanceOf(DefaultAuditorService); }); it('should log', async () => { @@ -88,14 +46,15 @@ describe('Auditor', () => { const pluginId = 'test-plugin'; - const auditor = Auditor.create({ + const auditor = DefaultRootAuditorService.create({ + format: format.json(), + transports: [mockTransport], + }).forPlugin({ auth: mockServices.auth.mock(), httpAuth: mockServices.httpAuth.mock(), plugin: { getId: () => pluginId, }, - format: format.json(), - transports: [mockTransport], }); await auditor.createEvent({ @@ -117,63 +76,10 @@ describe('Auditor', () => { ); }); - it('should redact nested object', async () => { - const mockTransport = new Transport({ - log: jest.fn(), - logv: jest.fn(), - }); - - const pluginId = 'test-plugin'; - - const auditor = Auditor.create({ - auth: mockServices.auth.mock(), - httpAuth: mockServices.httpAuth.mock(), - plugin: { - getId: () => pluginId, - }, - format: format.json(), - transports: [mockTransport], - }); - - auditor.addRedactions(['hello']); - - await auditor.createEvent({ - eventId: 'test-event', - meta: { - null: null, - nested: 'hello (world) from nested object', - nullProto: Object.create(null, { - foo: { value: 'hello foo', enumerable: true }, - }), - }, - }); - - expect(mockTransport.log).toHaveBeenCalledWith( - expect.objectContaining({ - [MESSAGE]: JSON.stringify({ - actor: {}, - isAuditorEvent: true, - level: 'info', - message: 'test-plugin.test-event', - meta: { - nested: '*** (world) from nested object', - null: null, - nullProto: { - foo: '*** foo', - }, - }, - severityLevel: 'low', - status: 'initiated', - }), - }), - expect.any(Function), - ); - }); - it('should log a status "initiated" using createEvent', async () => { const pluginId = 'test-plugin'; - const auditor = Auditor.create({ + const auditor = DefaultRootAuditorService.create().forPlugin({ auth: mockServices.auth.mock(), httpAuth: mockServices.httpAuth.mock(), plugin: { @@ -196,7 +102,7 @@ describe('Auditor', () => { it('should log a status "succeeded" using createEvent', async () => { const pluginId = 'test-plugin'; - const auditor = Auditor.create({ + const auditor = DefaultRootAuditorService.create().forPlugin({ auth: mockServices.auth.mock(), httpAuth: mockServices.httpAuth.mock(), plugin: { @@ -222,7 +128,7 @@ describe('Auditor', () => { it('should log a status "failed"', async () => { const pluginId = 'test-plugin'; - const auditor = Auditor.create({ + const auditor = DefaultRootAuditorService.create().forPlugin({ auth: mockServices.auth.mock(), httpAuth: mockServices.httpAuth.mock(), plugin: { @@ -250,7 +156,7 @@ describe('Auditor', () => { it('should use root meta', async () => { const pluginId = 'test-plugin'; - const auditor = Auditor.create({ + const auditor = DefaultRootAuditorService.create().forPlugin({ auth: mockServices.auth.mock(), httpAuth: mockServices.httpAuth.mock(), plugin: { diff --git a/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts b/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts index 490488082a..68eb5035ea 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts @@ -23,14 +23,13 @@ import type { HttpAuthService, PluginMetadataService, } from '@backstage/backend-plugin-api'; -import { ForwardedError, ServiceUnavailableError } from '@backstage/errors'; +import { ForwardedError } from '@backstage/errors'; import type { JsonObject } from '@backstage/types'; import type { Request } from 'express'; import type { Format } from 'logform'; import * as winston from 'winston'; import { colorFormat } from '../../lib/colorFormat'; import { defaultConsoleTransport } from '../../lib/defaultConsoleTransport'; -import { redacterFormat } from '../../lib/redacterFormat'; /** @public */ export type AuditorEventActorDetails = { @@ -99,7 +98,6 @@ export const defaultProdFormat = winston.format.combine( winston.format.errors({ stack: true }), winston.format.splat(), winston.format.json(), - redacterFormat().format, ); /** @@ -112,10 +110,7 @@ export const auditorFieldFormat = winston.format(info => { })(); /** @public */ -export interface AuditorOptions { - auth?: AuthService; - httpAuth?: HttpAuthService; - plugin?: PluginMetadataService; +export interface RootAuditorOptions { meta?: JsonObject; format?: Format; transports?: winston.transport[]; @@ -126,85 +121,45 @@ export interface AuditorOptions { * * @public */ -export class Auditor implements AuditorService { - readonly #winstonLogger: winston.Logger; - readonly #auth?: AuthService; - readonly #httpAuth?: HttpAuthService; - readonly #plugin?: PluginMetadataService; - readonly #addRedactions?: (redactions: Iterable) => void; - - /** - * Creates a {@link Auditor} instance. - */ - static create(options?: AuditorOptions): Auditor { - const redacter = Auditor.redacter(); - const defaultFormatter = - process.env.NODE_ENV === 'production' - ? defaultProdFormat - : Auditor.colorFormat(); - - let auditor = winston.createLogger({ - level: 'info', - format: winston.format.combine( - auditorFieldFormat, - options?.format ?? defaultFormatter, - redacter.format, - ), - transports: options?.transports ?? defaultConsoleTransport, - }); - - if (options?.meta) { - auditor = auditor.child(options.meta); - } - return new Auditor( - auditor, - { - auth: options?.auth, - httpAuth: options?.httpAuth, - plugin: options?.plugin, - }, - redacter.add, - ); - } - - /** - * Creates a winston log formatter for redacting secrets. - */ - static redacter(): { - format: Format; - add: (redactions: Iterable) => void; - } { - return redacterFormat(); - } - - /** - * Creates a pretty printed winston log formatter. - */ - static colorFormat(): Format { - return colorFormat(); - } +export class DefaultAuditorService implements AuditorService { + private readonly impl: DefaultRootAuditorService; + private readonly auth: AuthService; + private readonly httpAuth: HttpAuthService; + private readonly plugin: PluginMetadataService; private constructor( - winstonLogger: winston.Logger, - deps?: { - auth?: AuthService; - httpAuth?: HttpAuthService; - plugin?: PluginMetadataService; + impl: DefaultRootAuditorService, + deps: { + auth: AuthService; + httpAuth: HttpAuthService; + plugin: PluginMetadataService; }, - addRedactions?: (redactions: Iterable) => void, ) { - this.#winstonLogger = winstonLogger; - this.#auth = deps?.auth; - this.#httpAuth = deps?.httpAuth; - this.#plugin = deps?.plugin; - this.#addRedactions = addRedactions; + this.impl = impl; + this.auth = deps.auth; + this.httpAuth = deps.httpAuth; + this.plugin = deps.plugin; + } + + /** + * Creates a {@link DefaultAuditorService} instance. + */ + static create( + impl: DefaultRootAuditorService, + deps: { + auth: AuthService; + httpAuth: HttpAuthService; + plugin: PluginMetadataService; + }, + ): DefaultAuditorService { + return new DefaultAuditorService(impl, deps); } private async log( options: AuditorEventOptions, ): Promise { const auditEvent = await this.reshapeAuditorEvent(options); - this.#winstonLogger.info(...auditEvent); + this.impl.log(auditEvent); } async createEvent( @@ -247,56 +202,25 @@ export class Auditor implements AuditorService { }; } - child( - meta: JsonObject, - deps?: { - auth?: AuthService; - httpAuth?: HttpAuthService; - plugin?: PluginMetadataService; - }, - ): AuditorService { - return new Auditor(this.#winstonLogger.child(meta), { - auth: deps?.auth ?? this.#auth, - httpAuth: deps?.httpAuth ?? this.#httpAuth, - plugin: deps?.plugin ?? this.#plugin, - }); - } - - addRedactions(redactions: Iterable) { - this.#addRedactions?.(redactions); - } - private async getActorId( request?: Request, ): Promise { - if (!this.#auth) { - throw new ServiceUnavailableError( - `The core service 'auth' was not provided during the auditor's instantiation`, - ); - } - - if (!this.#httpAuth) { - throw new ServiceUnavailableError( - `The core service 'httpAuth' was not provided during the auditor's instantiation`, - ); - } - let credentials: BackstageCredentials = - await this.#auth.getOwnServiceCredentials(); + await this.auth.getOwnServiceCredentials(); if (request) { try { - credentials = await this.#httpAuth.credentials(request); + credentials = await this.httpAuth.credentials(request); } catch (error) { throw new ForwardedError('Could not resolve credentials', error); } } - if (this.#auth.isPrincipal(credentials, 'user')) { + if (this.auth.isPrincipal(credentials, 'user')) { return credentials.principal.userEntityRef; } - if (this.#auth.isPrincipal(credentials, 'service')) { + if (this.auth.isPrincipal(credentials, 'service')) { return credentials.principal.subject; } @@ -308,14 +232,8 @@ export class Auditor implements AuditorService { ): Promise { const { eventId, severityLevel = 'low', request, ...rest } = options; - if (!this.#plugin) { - throw new ServiceUnavailableError( - `The core service 'plugin' was not provided during the auditor's instantiation`, - ); - } - const auditEvent: AuditorEvent = [ - `${this.#plugin.getId()}.${eventId}`, + `${this.plugin.getId()}.${eventId}`, { severityLevel, actor: { @@ -337,3 +255,56 @@ export class Auditor implements AuditorService { return auditEvent; } } + +/** @public */ +export class DefaultRootAuditorService { + private readonly impl: winston.Logger; + + private constructor(impl: winston.Logger) { + this.impl = impl; + } + + /** + * Creates a {@link DefaultRootAuditorService} instance. + */ + static create(options?: RootAuditorOptions): DefaultRootAuditorService { + const defaultFormatter = + process.env.NODE_ENV === 'production' + ? defaultProdFormat + : DefaultRootAuditorService.colorFormat(); + + let auditor = winston.createLogger({ + level: 'info', + format: winston.format.combine( + auditorFieldFormat, + options?.format ?? defaultFormatter, + ), + transports: options?.transports ?? defaultConsoleTransport, + }); + + if (options?.meta) { + auditor = auditor.child(options.meta); + } + return new DefaultRootAuditorService(auditor); + } + + /** + * Creates a pretty printed winston log formatter. + */ + static colorFormat(): Format { + return colorFormat(); + } + + async log(auditEvent: AuditorEvent): Promise { + this.impl.info(...auditEvent); + } + + forPlugin(deps: { + auth: AuthService; + httpAuth: HttpAuthService; + plugin: PluginMetadataService; + }): AuditorService { + const impl = new DefaultRootAuditorService(this.impl.child({})); + return DefaultAuditorService.create(impl, deps); + } +} diff --git a/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts index cfff97a2bf..3a467c81b2 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts @@ -21,7 +21,11 @@ import { import type { Config } from '@backstage/config'; import * as winston from 'winston'; import { defaultConsoleTransport } from '../../lib/defaultConsoleTransport'; -import { Auditor, auditorFieldFormat, defaultProdFormat } from './Auditor'; +import { + DefaultRootAuditorService, + auditorFieldFormat, + defaultProdFormat, +} from './Auditor'; const transports = { auditorConsole: (config?: Config) => { @@ -32,20 +36,6 @@ const transports = { }, }; -/** - * Access to static configuration. - * - * See {@link @backstage/code-plugin-api#AuditorService} - * and {@link https://backstage.io/docs/backend-system/core-services/auditor | the service docs} - * for more information. - * - * @public - */ -export interface AuditorFactoryOptions { - transports: (config?: Config) => winston.transport[]; - format: (config?: Config) => winston.Logform.Format; -} - /** * Plugin-level auditing. * @@ -55,52 +45,33 @@ export interface AuditorFactoryOptions { * * @public */ -export const auditorServiceFactoryWithOptions = ( - options?: AuditorFactoryOptions, -) => - createServiceFactory({ - service: coreServices.auditor, - deps: { - config: coreServices.rootConfig, - auth: coreServices.auth, - httpAuth: coreServices.httpAuth, - plugin: coreServices.pluginMetadata, - }, - async createRootContext({ config }) { - const auditorConfig = config.getOptionalConfig('backend.auditor'); +export const auditorServiceFactory = createServiceFactory({ + service: coreServices.auditor, + deps: { + config: coreServices.rootConfig, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + plugin: coreServices.pluginMetadata, + }, + async createRootContext({ config }) { + const auditorConfig = config.getOptionalConfig('backend.auditor'); - const auditor = Auditor.create({ - meta: { - service: 'backstage', - }, - format: - options?.format(auditorConfig) ?? - winston.format.combine( - auditorFieldFormat, - process.env.NODE_ENV === 'production' - ? defaultProdFormat - : Auditor.colorFormat(), - ), - transports: [ - ...transports.auditorConsole(auditorConfig), - ...(options?.transports?.(auditorConfig) ?? []), - ], - }); + const auditor = DefaultRootAuditorService.create({ + meta: { + service: 'backstage', + }, + format: winston.format.combine( + auditorFieldFormat, + process.env.NODE_ENV === 'production' + ? defaultProdFormat + : DefaultRootAuditorService.colorFormat(), + ), + transports: [...transports.auditorConsole(auditorConfig)], + }); - return auditor; - }, - factory({ plugin, auth, httpAuth }, rootAuditor) { - return rootAuditor.child( - { plugin: plugin.getId() }, - { auth, httpAuth, plugin }, - ); - }, - }); - -/** - * @public - */ -export const auditorServiceFactory = Object.assign( - auditorServiceFactoryWithOptions, - auditorServiceFactoryWithOptions(), -); + return auditor; + }, + factory({ plugin, auth, httpAuth }, rootAuditor) { + return rootAuditor.forPlugin({ auth, httpAuth, plugin }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/auditor/index.ts b/packages/backend-defaults/src/entrypoints/auditor/index.ts index a4e3d4766b..563561dbfb 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/index.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/index.ts @@ -15,8 +15,4 @@ */ export * from './Auditor'; -export { - auditorServiceFactory, - auditorServiceFactoryWithOptions, - type AuditorFactoryOptions, -} from './auditorServiceFactory'; +export { auditorServiceFactory } from './auditorServiceFactory'; diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index bf91d2fe6f..84a8de8243 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -29,7 +29,6 @@ import type { Response as Response_2 } from 'express'; // @public (undocumented) export type AuditorCreateEvent = (options: { eventId: string; - subEventId?: string; severityLevel?: AuditorEventSeverityLevel; request?: Request_2; meta?: TRootMeta; diff --git a/packages/backend-plugin-api/src/services/definitions/AuditorService.ts b/packages/backend-plugin-api/src/services/definitions/AuditorService.ts index e678196847..f7e62b6c1a 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuditorService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuditorService.ts @@ -35,20 +35,17 @@ export type AuditorCreateEvent = (options: { */ eventId: string; - /** - * Use kebab-case to name sub-events (e.g., "by-id", "by-user"). - * - * (Optional) The ID for a sub-event or related action within the main event. This allows further categorization of events within a logical group. For example, if the `eventId` is "fetch", the `subEventId` could be "by-id" or "by-location" to specify the method used for fetching. - */ - subEventId?: string; - /** (Optional) The severity level for the audit event. */ severityLevel?: AuditorEventSeverityLevel; /** (Optional) The associated HTTP request, if applicable. */ request?: Request; - /** (Optional) Additional metadata relevant to the event, structured as a JSON object. */ + /** + * (Optional) Additional metadata relevant to the event, structured as a JSON object. + * This could include a `queryType` field, using kebab-case, for variations within the main event (e.g., "by-id", "by-user"). + * For example, if the `eventId` is "fetch", the `queryType` in `meta` could be "by-id" or "by-location". + */ meta?: TRootMeta; /** (Optional) Suppresses the automatic initial event. */ diff --git a/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts b/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts index 1d8a895eae..5dfbe560e2 100644 --- a/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts @@ -17,8 +17,8 @@ import type { ErrorLike } from '@backstage/errors'; import { MockAuditorService } from './MockAuditorService'; import { MockAuthService } from './MockAuthService'; -import { mockCredentials } from './mockCredentials'; import { MockHttpAuthService } from './MockHttpAuthService'; +import { mockCredentials } from './mockCredentials'; describe('MockAuditorService', () => { afterEach(() => { @@ -39,11 +39,14 @@ describe('MockAuditorService', () => { it('should error without auth service', async () => { const pluginId = 'test-plugin'; - const auditor = MockAuditorService.create({ - plugin: { - getId: () => pluginId, + const auditor = MockAuditorService.create().child( + {}, + { + plugin: { + getId: () => pluginId, + }, }, - }); + ); await expect( auditor.createEvent({ @@ -57,15 +60,18 @@ describe('MockAuditorService', () => { it('should error without httpAuth service', async () => { const pluginId = 'test-plugin'; - const auditor = MockAuditorService.create({ - plugin: { - getId: () => pluginId, + const auditor = MockAuditorService.create().child( + {}, + { + plugin: { + getId: () => pluginId, + }, + auth: new MockAuthService({ + pluginId, + disableDefaultAuthPolicy: false, + }), }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - }); + ); await expect( auditor.createEvent({ @@ -81,16 +87,19 @@ describe('MockAuditorService', () => { const pluginId = 'test-plugin'; - const auditor = MockAuditorService.create({ - plugin: { - getId: () => pluginId, + const auditor = MockAuditorService.create().child( + {}, + { + plugin: { + getId: () => pluginId, + }, + auth: new MockAuthService({ + pluginId, + disableDefaultAuthPolicy: false, + }), + httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); + ); await auditor.createEvent({ eventId: 'test-event', @@ -104,16 +113,19 @@ describe('MockAuditorService', () => { const pluginId = 'test-plugin'; - const auditor = MockAuditorService.create({ - plugin: { - getId: () => pluginId, + const auditor = MockAuditorService.create().child( + {}, + { + plugin: { + getId: () => pluginId, + }, + auth: new MockAuthService({ + pluginId, + disableDefaultAuthPolicy: false, + }), + httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); + ); await auditor.createEvent({ eventId: 'test-event', @@ -127,16 +139,19 @@ describe('MockAuditorService', () => { const pluginId = 'test-plugin'; - const auditor = MockAuditorService.create({ - plugin: { - getId: () => pluginId, + const auditor = MockAuditorService.create().child( + {}, + { + plugin: { + getId: () => pluginId, + }, + auth: new MockAuthService({ + pluginId, + disableDefaultAuthPolicy: false, + }), + httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); + ); const auditorEvent = await auditor.createEvent({ eventId: 'test-event', @@ -152,16 +167,19 @@ describe('MockAuditorService', () => { const pluginId = 'test-plugin'; - const auditor = MockAuditorService.create({ - plugin: { - getId: () => pluginId, + const auditor = MockAuditorService.create().child( + {}, + { + plugin: { + getId: () => pluginId, + }, + auth: new MockAuthService({ + pluginId, + disableDefaultAuthPolicy: false, + }), + httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); + ); const auditorEvent = await auditor.createEvent({ eventId: 'test-event', diff --git a/packages/backend-test-utils/src/next/services/MockAuditorService.ts b/packages/backend-test-utils/src/next/services/MockAuditorService.ts index 8d8ce79588..3e99986758 100644 --- a/packages/backend-test-utils/src/next/services/MockAuditorService.ts +++ b/packages/backend-test-utils/src/next/services/MockAuditorService.ts @@ -32,7 +32,11 @@ import type { Request } from 'express'; import type { mockServices } from './mockServices'; export class MockAuditorService implements AuditorService { - readonly #options: mockServices.auditor.Options; + readonly #options: mockServices.auditor.Options & { + auth?: AuthService; + httpAuth?: HttpAuthService; + plugin?: PluginMetadataService; + }; static create(options?: mockServices.auditor.Options): MockAuditorService { return new MockAuditorService(options ?? {}); @@ -54,17 +58,31 @@ export class MockAuditorService implements AuditorService { return { success: async params => { + // return undefined if both objects are empty; otherwise, merge the objects + const meta = + Object.keys(options.meta ?? {}).length === 0 && + Object.keys(params?.meta ?? {}).length === 0 + ? undefined + : { ...options.meta, ...params?.meta }; + await this.log({ ...options, - meta: { ...options.meta, ...params?.meta }, + meta, status: 'succeeded', }); }, fail: async params => { + // return undefined if both objects are empty; otherwise, merge the objects + const meta = + Object.keys(options.meta ?? {}).length === 0 && + Object.keys(params.meta ?? {}).length === 0 + ? undefined + : { ...options.meta, ...params.meta }; + await this.log({ ...options, ...params, - meta: { ...options.meta, ...params.meta }, + meta, status: 'failed', }); }, @@ -76,14 +94,14 @@ export class MockAuditorService implements AuditorService { deps?: { auth?: AuthService; httpAuth?: HttpAuthService; - plugin: PluginMetadataService; + plugin?: PluginMetadataService; }, ): AuditorService { return new MockAuditorService({ ...this.#options, - auth: deps?.auth ?? this.#options.auth, - httpAuth: deps?.httpAuth ?? this.#options.httpAuth, - plugin: deps?.plugin ?? this.#options.plugin, + auth: deps?.auth, + httpAuth: deps?.httpAuth, + plugin: deps?.plugin, meta: { ...this.#options.meta, ...meta, @@ -162,7 +180,13 @@ export class MockAuditorService implements AuditorService { return auditEvent; } - private constructor(options: mockServices.auditor.Options) { + private constructor( + options: mockServices.auditor.Options & { + auth?: AuthService; + httpAuth?: HttpAuthService; + plugin?: PluginMetadataService; + }, + ) { this.#options = options; } diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index b729ef3e0d..ad1ee24e5b 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { AuditorOptions } from '@backstage/backend-defaults/auditor'; +import type { RootAuditorOptions } from '@backstage/backend-defaults/auditor'; import { cacheServiceFactory } from '@backstage/backend-defaults/cache'; import { databaseServiceFactory } from '@backstage/backend-defaults/database'; import { HostDiscovery } from '@backstage/backend-defaults/discovery'; @@ -234,15 +234,14 @@ export namespace mockServices { ): AuditorService { const service = 'backstage'; const pluginId = options?.pluginId ?? 'test'; - const mockAuth = - options?.auth ?? - new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }); - const mockHttpAuth = - options?.httpAuth ?? - new MockHttpAuthService(pluginId, mockCredentials.user()); + const mockAuth = new MockAuthService({ + pluginId, + disableDefaultAuthPolicy: false, + }); + const mockHttpAuth = new MockHttpAuthService( + pluginId, + mockCredentials.user(), + ); const mockPlugin = { getId: () => pluginId, @@ -259,7 +258,7 @@ export namespace mockServices { } export namespace auditor { - export type Options = Omit; + export type Options = Omit; export const factory = (options?: auditor.Options) => createServiceFactory({ diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 6a1972ef17..680adc0bcd 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -95,6 +95,8 @@ The Catalog backend emits audit events for various operations. Events are groupe - **`entity-fetch`**: Retrieves entities. + Filter on `queryType`. + - **`all`**: Fetching all entities. (GET `/entities`) - **`by-id`**: Fetching a single entity using its UID. (GET `/entities/by-uid/:uid`) - **`by-name`**: Fetching a single entity using its kind, namespace, and name. (GET `/entities/by-name/:kind/:namespace/:name`) @@ -104,6 +106,8 @@ The Catalog backend emits audit events for various operations. Events are groupe - **`entity-mutate`**: Modifies entities. + Filter on `actionType`. + - **`delete`**: Deleting a single entity. Note: this will not be a permanent deletion and the entity will be restored if the parent location is still present in the catalog. (DELETE `/entities/by-uid/:uid`) - **`refresh`**: Scheduling an entity refresh. (POST `/entities/refresh`) @@ -115,6 +119,8 @@ The Catalog backend emits audit events for various operations. Events are groupe - **`location-fetch`**: Retrieves locations. + Filter on `actionType`. + - **`all`**: Fetching all locations. (GET `/locations`) - **`by-id`**: Fetching a single location by ID. (GET `/locations/:id`) - **`by-entity`**: Fetching locations associated with an entity ref. (GET `/locations/by-entity`) diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 8c0de3d221..c7ca028182 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -35,7 +35,7 @@ import { LocationAnalyzer } from '@backstage/plugin-catalog-node'; import express from 'express'; import yn from 'yn'; import { z } from 'zod'; -import { Cursor, EntitiesCatalog } from '../catalog/types'; +import { EntitiesCatalog } from '../catalog/types'; import { CatalogProcessingOrchestrator } from '../processing/types'; import { validateEntityEnvelope } from '../processing/util'; import { createOpenApiRouter } from '../schema/openapi'; @@ -50,11 +50,7 @@ import { import { parseEntityFacetParams } from './request/parseEntityFacetParams'; import { parseEntityOrderParams } from './request/parseEntityOrderParams'; import { parseEntityPaginationParams } from './request/parseEntityPaginationParams'; -import { - createEntityArrayJsonStream, - writeEntitiesResponse, - writeSingleEntityResponse, -} from './response'; +import { writeEntitiesResponse, writeSingleEntityResponse } from './response'; import { LocationService, RefreshService } from './types'; import { disallowReadonlyMode, @@ -129,9 +125,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'entity-mutate', - subEventId: 'refresh', severityLevel: 'medium', meta: { + queryType: 'refresh', entityRef: restBody.entityRef, }, request: req, @@ -165,8 +161,11 @@ export async function createRouter( .get('/entities', async (req, res) => { const auditorEvent = await auditor?.createEvent({ eventId: 'entity-fetch', - subEventId: 'all', request: req, + meta: { + queryType: 'all', + query: req.query, + }, }); try { @@ -260,8 +259,10 @@ export async function createRouter( .get('/entities/by-query', async (req, res) => { const auditorEvent = await auditor?.createEvent({ eventId: 'entity-fetch', - subEventId: 'by-query', request: req, + meta: { + queryType: 'by-query', + }, }); try { @@ -311,9 +312,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'entity-fetch', - subEventId: 'by-uid', request: req, meta: { + queryType: 'by-uid', uid: uid, }, }); @@ -343,10 +344,10 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'entity-mutate', - subEventId: 'delete', severityLevel: 'medium', request: req, meta: { + actionType: 'delete', uid: uid, }, }); @@ -372,9 +373,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'entity-fetch', - subEventId: 'by-name', request: req, meta: { + queryType: 'by-name', entityRef: entityRef, }, }); @@ -407,9 +408,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'entity-fetch', - subEventId: 'ancestry', request: req, meta: { + actionType: 'ancestry', entityRef: entityRef, }, }); @@ -443,8 +444,10 @@ export async function createRouter( .post('/entities/by-refs', async (req, res) => { const auditorEvent = await auditor?.createEvent({ eventId: 'entity-fetch', - subEventId: 'by-refs', request: req, + meta: { + queryType: 'by-refs', + }, }); try { @@ -510,10 +513,10 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'location-mutate', - subEventId: 'create', severityLevel: dryRun ? 'low' : 'medium', request: req, meta: { + actionType: 'create', location: location, isDryRun: dryRun, }, @@ -555,8 +558,10 @@ export async function createRouter( .get('/locations', async (req, res) => { const auditorEvent = await auditor?.createEvent({ eventId: 'location-fetch', - subEventId: 'all', request: req, + meta: { + queryType: 'all', + }, }); try { @@ -580,9 +585,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'location-fetch', - subEventId: 'by-id', request: req, meta: { + queryType: 'by-id', id: id, }, }); @@ -611,10 +616,10 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'location-mutate', - subEventId: 'delete', severityLevel: 'medium', request: req, meta: { + actionType: 'delete', id: id, }, }); @@ -642,9 +647,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'location-fetch', - subEventId: 'by-entity', request: req, meta: { + queryType: 'by-entity', locationRef: locationRef, }, }); diff --git a/plugins/scaffolder-backend/README.md b/plugins/scaffolder-backend/README.md index 4a4281ec9c..34a378ba44 100644 --- a/plugins/scaffolder-backend/README.md +++ b/plugins/scaffolder-backend/README.md @@ -79,6 +79,9 @@ The Scaffolder backend emits audit events for various operations. Events are gro **Task Events:** - **`task`**: Operations related to Scaffolder tasks. + + Filter on `actionType`. + - **`create`**: Creates a new task. (POST `/v2/tasks`) - **`list`**: Fetches details of all tasks. (GET `/v2/tasks`) - **`get`**: Fetches details of a specific task. (GET `/v2/tasks/:taskId`) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 56a395106a..863f06c586 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -207,9 +207,9 @@ export class TaskManager implements TaskContext { const auditorEvent = await this.auditor?.createEvent({ eventId: 'task', - subEventId: 'execution', severityLevel: 'medium', meta: { + actionType: 'execution', taskId: this.task.taskId, taskParameters: this.task.spec.parameters, }, @@ -474,9 +474,9 @@ export class StorageTaskBroker implements TaskBroker { tasks.map(async task => { const auditorEvent = await this.auditor?.createEvent({ eventId: 'task', - subEventId: 'stale-cancel', severityLevel: 'medium', meta: { + actionType: 'stale-cancel', taskId: task.taskId, }, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 944fa9b6ac..492d8f10b1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -176,9 +176,9 @@ export class TaskWorker { async runOneTask(task: TaskContext) { await this.auditor?.createEvent({ eventId: 'task', - subEventId: 'execution', severityLevel: 'medium', meta: { + actionType: 'execution', taskId: task.taskId, taskParameters: task.spec.parameters, templateRef: task.spec.templateInfo?.entityRef, diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 66ad2a7350..c947ad77ff 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -542,10 +542,10 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'create', severityLevel: 'medium', request: req, meta: { + actionType: 'create', templateRef: templateRef, }, }); @@ -648,8 +648,10 @@ export async function createRouter( .get('/v2/tasks', async (req, res) => { const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'list', request: req, + meta: { + actionType: 'list', + }, }); try { @@ -711,9 +713,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'get', request: req, meta: { + actionType: 'get', taskId: taskId, }, }); @@ -746,10 +748,12 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'cancel', severityLevel: 'medium', request: req, - meta: { taskId: taskId }, + meta: { + actionType: 'cancel', + taskId: taskId, + }, }); try { @@ -776,10 +780,12 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'retry', severityLevel: 'medium', request: req, - meta: { taskId: taskId }, + meta: { + actionType: 'retry', + taskId: taskId, + }, }); try { @@ -805,9 +811,11 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'stream', request: req, - meta: { taskId: taskId }, + meta: { + actionType: 'stream', + taskId: taskId, + }, }); try { @@ -875,9 +883,9 @@ export async function createRouter( const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'events', request: req, meta: { + actionType: 'events', taskId: taskId, }, }); @@ -927,8 +935,10 @@ export async function createRouter( .post('/v2/dry-run', async (req, res) => { const auditorEvent = await auditor?.createEvent({ eventId: 'task', - subEventId: 'dry-run', request: req, + meta: { + actionType: 'dry-run', + }, }); try {