apply requested changes

Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
Paul Schultz
2024-12-10 10:07:11 -06:00
parent e15f345b40
commit 3db23e512b
16 changed files with 316 additions and 438 deletions
+4 -31
View File
@@ -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.
@@ -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: {
@@ -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<string>) => 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<string>) => 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<string>) => 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<TMeta extends JsonObject>(
options: AuditorEventOptions<TMeta>,
): Promise<void> {
const auditEvent = await this.reshapeAuditorEvent(options);
this.#winstonLogger.info(...auditEvent);
this.impl.log(auditEvent);
}
async createEvent<TMeta extends JsonObject>(
@@ -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<string>) {
this.#addRedactions?.(redactions);
}
private async getActorId(
request?: Request<any, any, any, any, any>,
): Promise<string | undefined> {
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<AuditorEvent> {
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<void> {
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);
}
}
@@ -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 });
},
});
@@ -15,8 +15,4 @@
*/
export * from './Auditor';
export {
auditorServiceFactory,
auditorServiceFactoryWithOptions,
type AuditorFactoryOptions,
} from './auditorServiceFactory';
export { auditorServiceFactory } from './auditorServiceFactory';
@@ -29,7 +29,6 @@ import type { Response as Response_2 } from 'express';
// @public (undocumented)
export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
eventId: string;
subEventId?: string;
severityLevel?: AuditorEventSeverityLevel;
request?: Request_2<any, any, any, any, any>;
meta?: TRootMeta;
@@ -35,20 +35,17 @@ export type AuditorCreateEvent<TRootMeta extends JsonObject> = (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<any, any, any, any, any>;
/** (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. */
@@ -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',
@@ -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;
}
@@ -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<AuditorOptions, 'format' | 'transports'>;
export type Options = Omit<RootAuditorOptions, 'format' | 'transports'>;
export const factory = (options?: auditor.Options) =>
createServiceFactory({
+6
View File
@@ -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`)
@@ -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,
},
});
+3
View File
@@ -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`)
@@ -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,
},
});
@@ -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,
@@ -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 {