diff --git a/.changeset/olive-boxes-hide-backend-defaults.md b/.changeset/olive-boxes-hide-backend-defaults.md index 64c828feca..9d4ff05f17 100644 --- a/.changeset/olive-boxes-hide-backend-defaults.md +++ b/.changeset/olive-boxes-hide-backend-defaults.md @@ -2,6 +2,4 @@ '@backstage/backend-defaults': minor --- -feat: add auditor to `coreServices` - This change introduces the `auditor` service implementation details. diff --git a/.changeset/olive-boxes-hide-backend-plugin-api.md b/.changeset/olive-boxes-hide-backend-plugin-api.md index 5595cfb338..32e4d41813 100644 --- a/.changeset/olive-boxes-hide-backend-plugin-api.md +++ b/.changeset/olive-boxes-hide-backend-plugin-api.md @@ -2,6 +2,4 @@ '@backstage/backend-plugin-api': minor --- -feat: add auditor to `coreServices` - This change introduces the `auditor` service definition. diff --git a/.changeset/olive-boxes-hide-backend-test-utils.md b/.changeset/olive-boxes-hide-backend-test-utils.md index 04ae80b24d..41b2016f5d 100644 --- a/.changeset/olive-boxes-hide-backend-test-utils.md +++ b/.changeset/olive-boxes-hide-backend-test-utils.md @@ -2,6 +2,4 @@ '@backstage/backend-test-utils': minor --- -feat: add auditor to `coreServices` - This change introduces mocks for the `auditor` service. diff --git a/.changeset/olive-boxes-hide-catalog-backend.md b/.changeset/olive-boxes-hide-catalog-backend.md index bbfbb315dd..09d88bf6e2 100644 --- a/.changeset/olive-boxes-hide-catalog-backend.md +++ b/.changeset/olive-boxes-hide-catalog-backend.md @@ -2,6 +2,4 @@ '@backstage/plugin-catalog-backend': minor --- -feat: add auditor to `coreServices` - This change integrates the `auditor` service into the Catalog plugin. diff --git a/.changeset/olive-boxes-hide-scaffolder-backend.md b/.changeset/olive-boxes-hide-scaffolder-backend.md index b19ef1993b..be67064091 100644 --- a/.changeset/olive-boxes-hide-scaffolder-backend.md +++ b/.changeset/olive-boxes-hide-scaffolder-backend.md @@ -2,6 +2,4 @@ '@backstage/plugin-scaffolder-backend': minor --- -feat: add auditor to `coreServices` - This change integrates the `auditor` service into the Scaffolder plugin. diff --git a/.changeset/olive-boxes-hide-scaffolder-node.md b/.changeset/olive-boxes-hide-scaffolder-node.md index 7c3d3cdedc..f3c18f0991 100644 --- a/.changeset/olive-boxes-hide-scaffolder-node.md +++ b/.changeset/olive-boxes-hide-scaffolder-node.md @@ -2,6 +2,4 @@ '@backstage/plugin-scaffolder-node': minor --- -feat: add auditor to `coreServices` - This change introduces an optional `taskId` property to `TaskContext`. diff --git a/packages/backend-defaults/report-auditor.api.md b/packages/backend-defaults/report-auditor.api.md index 57bb8949fd..c6d12295b3 100644 --- a/packages/backend-defaults/report-auditor.api.md +++ b/packages/backend-defaults/report-auditor.api.md @@ -21,6 +21,7 @@ import * as winston from 'winston'; export type AuditorEvent = [ eventId: string, meta: { + plugin: string; severityLevel: AuditorServiceEventSeverityLevel; actor: AuditorEventActorDetails; meta?: JsonObject; @@ -60,7 +61,7 @@ export type AuditorEventStatus = } | { status: 'failed'; - error: Error; + error: string; }; // @public diff --git a/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts b/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts index 854fe94c8b..671e4ef102 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/Auditor.test.ts @@ -80,6 +80,7 @@ describe('Auditor', () => { expect(auditorSpy).toHaveBeenCalledTimes(2); expect(auditorSpy).toHaveBeenLastCalledWith({ eventId: 'test-event', + meta: {}, status: 'succeeded', }); }); @@ -107,8 +108,9 @@ describe('Auditor', () => { expect(auditorSpy).toHaveBeenCalledTimes(2); expect(auditorSpy).toHaveBeenLastCalledWith({ eventId: 'test-event', + meta: {}, status: 'failed', - error, + error: error.toString(), }); }); @@ -160,7 +162,7 @@ describe('Auditor', () => { initiated: 'test', failed: 'test', }, - error, + error: error.toString(), }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts b/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts index 4a62b02a03..c8e9e25fec 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/Auditor.ts @@ -52,7 +52,7 @@ export type AuditorEventStatus = | { status: 'succeeded' } | { status: 'failed'; - error: Error; + error: string; }; /** @@ -85,6 +85,7 @@ export type AuditorEventOptions = { export type AuditorEvent = [ eventId: string, meta: { + plugin: string; severityLevel: AuditorServiceEventSeverityLevel; actor: AuditorEventActorDetails; meta?: JsonObject; @@ -164,31 +165,18 @@ export class DefaultAuditorService 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, + meta: { ...options.meta, ...params?.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, + error: params.error.toString(), + meta: { ...options.meta, ...params?.meta }, status: 'failed', }); }, @@ -223,11 +211,12 @@ export class DefaultAuditorService implements AuditorService { private async reshapeAuditorEvent( options: AuditorEventOptions, ): Promise { - const { eventId, severityLevel = 'low', request, ...rest } = options; + const { eventId, severityLevel = 'low', request, meta, ...rest } = options; const auditEvent: AuditorEvent = [ `${this.plugin.getId()}.${eventId}`, { + plugin: this.plugin.getId(), severityLevel, actor: { actorId: await this.getActorId(request), @@ -241,6 +230,7 @@ export class DefaultAuditorService implements AuditorService { method: request?.method, } : undefined, + meta: Object.keys(meta ?? {}).length === 0 ? undefined : meta, ...rest, }, ]; @@ -304,18 +294,7 @@ export class DefaultRootAuditorService { } async log(auditorEvent: AuditorEvent): Promise { - const [eventId, meta] = auditorEvent; - - // change `error` type to a string for logging purposes - let fields: Omit & { error?: string }; - - if ('error' in meta) { - fields = { ...meta, error: meta.error.toString() }; - } else { - fields = meta; - } - - this.impl.info(eventId, fields); + this.impl.info(...auditorEvent); } forPlugin(deps: { diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 8c5601cd09..b358b268dc 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -154,7 +154,6 @@ export function mockErrorHandler(): ErrorRequestHandler< // @public export namespace mockServices { - export function auditor(options?: { pluginId?: string }): AuditorService; // (undocumented) export namespace auditor { const // (undocumented) diff --git a/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts b/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts deleted file mode 100644 index 594301579b..0000000000 --- a/packages/backend-test-utils/src/next/services/MockAuditorService.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ErrorLike } from '@backstage/errors'; -import { MockRootAuditorService } from './MockAuditorService'; -import { MockAuthService } from './MockAuthService'; -import { MockHttpAuthService } from './MockHttpAuthService'; -import { mockCredentials } from './mockCredentials'; - -describe('MockAuditorService', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should send initiated log with createEvent', async () => { - const spy = jest.spyOn(MockRootAuditorService.prototype, 'log'); - - const pluginId = 'test-plugin'; - - const auditor = MockRootAuditorService.create().forPlugin({ - plugin: { - getId: () => pluginId, - }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); - - await auditor.createEvent({ - eventId: 'test-event', - }); - - expect(spy).toHaveBeenCalled(); - }); - - it('should send succeeded log with createEvent', async () => { - const spy = jest.spyOn(MockRootAuditorService.prototype, 'log'); - - const pluginId = 'test-plugin'; - - const auditor = MockRootAuditorService.create().forPlugin({ - plugin: { - getId: () => pluginId, - }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); - - const auditorEvent = await auditor.createEvent({ - eventId: 'test-event', - }); - - await auditorEvent.success(); - - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('should send failed log with createEvent', async () => { - const spy = jest.spyOn(MockRootAuditorService.prototype, 'log'); - - const pluginId = 'test-plugin'; - - const auditor = MockRootAuditorService.create().forPlugin({ - plugin: { - getId: () => pluginId, - }, - auth: new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }), - httpAuth: new MockHttpAuthService(pluginId, mockCredentials.user()), - }); - - const auditorEvent = await auditor.createEvent({ - eventId: 'test-event', - }); - - await auditorEvent.fail({ error: new Error('error') as ErrorLike }); - - expect(spy).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/backend-test-utils/src/next/services/MockAuditorService.ts b/packages/backend-test-utils/src/next/services/MockAuditorService.ts deleted file mode 100644 index 0a734275ec..0000000000 --- a/packages/backend-test-utils/src/next/services/MockAuditorService.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { - AuditorEvent, - AuditorEventOptions, -} from '@backstage/backend-defaults/auditor'; -import type { - AuditorService, - AuditorServiceCreateEventOptions, - AuditorServiceEvent, - AuthService, - BackstageCredentials, - HttpAuthService, - PluginMetadataService, - RootLoggerService, -} from '@backstage/backend-plugin-api'; -import { ForwardedError } from '@backstage/errors'; -import type { JsonObject } from '@backstage/types'; -import type { Request } from 'express'; -import { mockServices } from './mockServices'; - -export class MockAuditorService implements AuditorService { - private readonly impl: MockRootAuditorService; - private readonly auth: AuthService; - private readonly httpAuth: HttpAuthService; - private readonly plugin: PluginMetadataService; - - private constructor( - impl: MockRootAuditorService, - deps: { - auth: AuthService; - httpAuth: HttpAuthService; - plugin: PluginMetadataService; - }, - ) { - this.impl = impl; - this.auth = deps.auth; - this.httpAuth = deps.httpAuth; - this.plugin = deps.plugin; - } - - static create( - impl: MockRootAuditorService, - deps: { - auth: AuthService; - httpAuth: HttpAuthService; - plugin: PluginMetadataService; - }, - ): AuditorService { - return new MockAuditorService(impl, deps); - } - - private async log( - options: AuditorEventOptions, - ): Promise { - const auditEvent = await this.reshapeAuditorEvent(options); - this.impl.log(auditEvent); - } - - async createEvent( - options: AuditorServiceCreateEventOptions, - ): Promise { - await this.log({ ...options, status: 'initiated' }); - - 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, - 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, - status: 'failed', - }); - }, - }; - } - - private async getActorId( - request?: Request, - ): Promise { - let credentials: BackstageCredentials = - await this.auth.getOwnServiceCredentials(); - - if (request) { - try { - credentials = await this.httpAuth.credentials(request); - } catch (error) { - throw new ForwardedError('Could not resolve credentials', error); - } - } - - if (this.auth.isPrincipal(credentials, 'user')) { - return credentials.principal.userEntityRef; - } - - if (this.auth.isPrincipal(credentials, 'service')) { - return credentials.principal.subject; - } - - return undefined; - } - - private async reshapeAuditorEvent( - options: AuditorEventOptions, - ): Promise { - const { eventId, severityLevel = 'low', request, ...rest } = options; - - const auditEvent: AuditorEvent = [ - `${this.plugin.getId()}.${eventId}`, - { - severityLevel, - actor: { - actorId: await this.getActorId(request), - ip: request?.ip, - hostname: request?.hostname, - userAgent: request?.get('user-agent'), - }, - request: request - ? { - url: request?.originalUrl, - method: request?.method, - } - : undefined, - ...rest, - }, - ]; - - return auditEvent; - } -} - -export class MockRootAuditorService { - private readonly impl: RootLoggerService; - - private constructor() { - this.impl = mockServices.rootLogger(); - } - - static create(): MockRootAuditorService { - return new MockRootAuditorService(); - } - - async log(auditorEvent: AuditorEvent): Promise { - const [eventId, meta] = auditorEvent; - - // change `error` type to a string for logging purposes - let fields: Omit & { error?: string }; - - if ('error' in meta) { - fields = { ...meta, error: meta.error.toString() }; - } else { - fields = meta; - } - - this.impl.info(eventId, fields); - } - - forPlugin(deps: { - auth: AuthService; - httpAuth: HttpAuthService; - plugin: PluginMetadataService; - }): AuditorService { - const impl = new MockRootAuditorService(); - return MockAuditorService.create(impl, deps); - } -} diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 5aff29032c..15026d5dda 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -28,7 +28,6 @@ import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLif import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler'; import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader'; import { - AuditorService, AuthService, BackstageCredentials, BackstageUserInfo, @@ -50,12 +49,12 @@ import { } from '@backstage/plugin-events-node'; import { JsonObject } from '@backstage/types'; import { Knex } from 'knex'; -import { MockRootAuditorService } from './MockAuditorService'; import { MockAuthService } from './MockAuthService'; import { MockHttpAuthService } from './MockHttpAuthService'; import { MockRootLoggerService } from './MockRootLoggerService'; import { MockUserInfoService } from './MockUserInfoService'; import { mockCredentials } from './mockCredentials'; +import { auditorServiceFactory } from '@backstage/backend-defaults/auditor'; /** @internal */ function createLoggerMock() { @@ -223,56 +222,8 @@ export namespace mockServices { })); } - /** - * Creates a mock implementation of the `AuditorService`. - */ - export function auditor(options?: { pluginId?: string }): AuditorService { - const pluginId = options?.pluginId ?? 'test'; - const mockAuth = new MockAuthService({ - pluginId, - disableDefaultAuthPolicy: false, - }); - const mockHttpAuth = new MockHttpAuthService( - pluginId, - mockCredentials.user(), - ); - - const mockPlugin = { - getId: () => pluginId, - }; - - const auditorMock = MockRootAuditorService.create(); - - return auditorMock.forPlugin({ - auth: mockAuth, - httpAuth: mockHttpAuth, - plugin: mockPlugin, - }); - } - export namespace auditor { - export const factory = () => - createServiceFactory({ - service: coreServices.auditor, - deps: { - auth: coreServices.auth, - httpAuth: coreServices.httpAuth, - plugin: coreServices.pluginMetadata, - }, - createRootContext() { - return MockRootAuditorService.create(); - }, - factory( - { auth: mockAuth, httpAuth: mockHttpAuth, plugin: mockPlugin }, - rootAuditor, - ) { - return rootAuditor.forPlugin({ - auth: mockAuth, - httpAuth: mockHttpAuth, - plugin: mockPlugin, - }); - }, - }); + export const factory = () => auditorServiceFactory; export const mock = simpleMock(coreServices.auditor, () => ({ createEvent: jest.fn(async _ => {