apply requested changes
Signed-off-by: Paul Schultz <pschultz@pobox.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/backend-defaults': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change introduces the `auditor` service implementation details.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change introduces the `auditor` service definition.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change introduces mocks for the `auditor` service.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change integrates the `auditor` service into the Catalog plugin.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change integrates the `auditor` service into the Scaffolder plugin.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-node': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change introduces an optional `taskId` property to `TaskContext`.
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': minor
|
||||
'@backstage/backend-test-utils': minor
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
'@backstage/backend-defaults': minor
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
'@backstage/plugin-scaffolder-node': minor
|
||||
---
|
||||
|
||||
feat: add auditor to `coreServices`
|
||||
|
||||
This change introduces a new `auditor` service to the `coreServices` in Backstage.
|
||||
The auditor service enables plugins to emit audit events for security-relevant actions.
|
||||
@@ -85,20 +85,3 @@ When defining `eventId` and `subEventId` for your audit events, follow these gui
|
||||
- Use `subEventId` to further categorize 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.
|
||||
- Avoid redundant prefixes related to the plugin ID, as that context is already provided.
|
||||
- Choose names that clearly and concisely describe the event being audited.
|
||||
|
||||
## Configuring the service
|
||||
|
||||
The Auditor Service can be configured using the `backend.auditor` section in your `app-config.yaml` file.
|
||||
|
||||
### Console Logging
|
||||
|
||||
Console logging allows you to see audit events directly in your terminal output. This is useful for development and debugging purposes. To enable console logging, set the `enabled` flag to `true` within the `console` section:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
auditor:
|
||||
console:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
By default, console logging is enabled. You can disable it by setting the `enabled` flag to `false`.
|
||||
|
||||
-12
@@ -632,18 +632,6 @@ export interface Config {
|
||||
paths?: string[];
|
||||
}>;
|
||||
};
|
||||
auditor?: {
|
||||
/**
|
||||
* Configuration for the auditing to the console
|
||||
*/
|
||||
console: {
|
||||
/**
|
||||
* Enables auditing to console
|
||||
* @default true
|
||||
*/
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import type { AuditorCreateEvent } from '@backstage/backend-plugin-api';
|
||||
import type { AuditorEventSeverityLevel } from '@backstage/backend-plugin-api';
|
||||
import { AuditorService } from '@backstage/backend-plugin-api';
|
||||
import type { AuditorServiceCreateEventOptions } from '@backstage/backend-plugin-api';
|
||||
import type { AuditorServiceEvent } from '@backstage/backend-plugin-api';
|
||||
import type { AuditorServiceEventSeverityLevel } from '@backstage/backend-plugin-api';
|
||||
import type { AuthService } from '@backstage/backend-plugin-api';
|
||||
import type { Format } from 'logform';
|
||||
import type { HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import type { JsonObject } from '@backstage/types';
|
||||
import type { PluginMetadataService } from '@backstage/backend-plugin-api';
|
||||
import type { Request as Request_2 } from 'express';
|
||||
import type { RootLoggerService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import * as winston from 'winston';
|
||||
|
||||
@@ -19,7 +21,7 @@ import * as winston from 'winston';
|
||||
export type AuditorEvent = [
|
||||
eventId: string,
|
||||
meta: {
|
||||
severityLevel: AuditorEventSeverityLevel;
|
||||
severityLevel: AuditorServiceEventSeverityLevel;
|
||||
actor: AuditorEventActorDetails;
|
||||
meta?: JsonObject;
|
||||
request?: AuditorEventRequest;
|
||||
@@ -37,7 +39,7 @@ export type AuditorEventActorDetails = {
|
||||
// @public
|
||||
export type AuditorEventOptions<TMeta extends JsonObject> = {
|
||||
eventId: string;
|
||||
severityLevel?: AuditorEventSeverityLevel;
|
||||
severityLevel?: AuditorServiceEventSeverityLevel;
|
||||
request?: Request_2<any, any, any, any, any>;
|
||||
meta?: TMeta;
|
||||
} & AuditorEventStatus;
|
||||
@@ -49,23 +51,17 @@ export type AuditorEventRequest = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type AuditorEventStatus<TError extends Error = Error> =
|
||||
export type AuditorEventStatus =
|
||||
| {
|
||||
status: 'initiated';
|
||||
}
|
||||
| {
|
||||
status: 'succeeded';
|
||||
}
|
||||
| ({
|
||||
| {
|
||||
status: 'failed';
|
||||
} & (
|
||||
| {
|
||||
error: TError;
|
||||
}
|
||||
| {
|
||||
errors: TError[];
|
||||
}
|
||||
));
|
||||
error: Error;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const auditorFieldFormat: Format;
|
||||
@@ -88,17 +84,16 @@ export class DefaultAuditorService implements AuditorService {
|
||||
},
|
||||
): DefaultAuditorService;
|
||||
// (undocumented)
|
||||
createEvent<TMeta extends JsonObject>(
|
||||
options: Parameters<AuditorCreateEvent<TMeta>>[0],
|
||||
): ReturnType<AuditorCreateEvent<TMeta>>;
|
||||
createEvent(
|
||||
options: AuditorServiceCreateEventOptions,
|
||||
): Promise<AuditorServiceEvent>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const defaultProdFormat: Format;
|
||||
export const defaultFormatter: Format;
|
||||
|
||||
// @public (undocumented)
|
||||
export class DefaultRootAuditorService {
|
||||
static colorFormat(): Format;
|
||||
static create(options?: RootAuditorOptions): DefaultRootAuditorService;
|
||||
// (undocumented)
|
||||
forPlugin(deps: {
|
||||
@@ -107,18 +102,19 @@ export class DefaultRootAuditorService {
|
||||
plugin: PluginMetadataService;
|
||||
}): AuditorService;
|
||||
// (undocumented)
|
||||
log(auditEvent: AuditorEvent): Promise<void>;
|
||||
log(auditorEvent: AuditorEvent): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootAuditorOptions {
|
||||
// (undocumented)
|
||||
format?: Format;
|
||||
// (undocumented)
|
||||
meta?: JsonObject;
|
||||
// (undocumented)
|
||||
transports?: winston.transport[];
|
||||
}
|
||||
// @public
|
||||
export type RootAuditorOptions =
|
||||
| {
|
||||
meta?: JsonObject;
|
||||
format?: Format;
|
||||
transports?: winston.transport[];
|
||||
}
|
||||
| {
|
||||
rootLogger: RootLoggerService;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
*/
|
||||
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { format } from 'logform';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import Transport from 'winston-transport';
|
||||
import { DefaultAuditorService, DefaultRootAuditorService } from './Auditor';
|
||||
|
||||
describe('Auditor', () => {
|
||||
@@ -38,44 +35,6 @@ describe('Auditor', () => {
|
||||
expect(childLogger).toBeInstanceOf(DefaultAuditorService);
|
||||
});
|
||||
|
||||
it('should log', async () => {
|
||||
const mockTransport = new Transport({
|
||||
log: jest.fn(),
|
||||
logv: jest.fn(),
|
||||
});
|
||||
|
||||
const pluginId = 'test-plugin';
|
||||
|
||||
const auditor = DefaultRootAuditorService.create({
|
||||
format: format.json(),
|
||||
transports: [mockTransport],
|
||||
}).forPlugin({
|
||||
auth: mockServices.auth.mock(),
|
||||
httpAuth: mockServices.httpAuth.mock(),
|
||||
plugin: {
|
||||
getId: () => pluginId,
|
||||
},
|
||||
});
|
||||
|
||||
await auditor.createEvent({
|
||||
eventId: 'test-event',
|
||||
});
|
||||
|
||||
expect(mockTransport.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[MESSAGE]: JSON.stringify({
|
||||
actor: {},
|
||||
isAuditorEvent: true,
|
||||
level: 'info',
|
||||
message: 'test-plugin.test-event',
|
||||
severityLevel: 'low',
|
||||
status: 'initiated',
|
||||
}),
|
||||
}),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a status "initiated" using createEvent', async () => {
|
||||
const pluginId = 'test-plugin';
|
||||
|
||||
|
||||
@@ -15,21 +15,22 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
AuditorCreateEvent,
|
||||
AuditorEventSeverityLevel,
|
||||
AuditorService,
|
||||
AuditorServiceCreateEventOptions,
|
||||
AuditorServiceEvent,
|
||||
AuditorServiceEventSeverityLevel,
|
||||
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 type { Format } from 'logform';
|
||||
import * as winston from 'winston';
|
||||
import { colorFormat } from '../../lib/colorFormat';
|
||||
import { defaultConsoleTransport } from '../../lib/defaultConsoleTransport';
|
||||
import { WinstonLogger } from '../rootLogger';
|
||||
|
||||
/** @public */
|
||||
export type AuditorEventActorDetails = {
|
||||
@@ -46,12 +47,13 @@ export type AuditorEventRequest = {
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type AuditorEventStatus<TError extends Error = Error> =
|
||||
export type AuditorEventStatus =
|
||||
| { status: 'initiated' }
|
||||
| { status: 'succeeded' }
|
||||
| ({
|
||||
| {
|
||||
status: 'failed';
|
||||
} & ({ error: TError } | { errors: TError[] }));
|
||||
error: Error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for creating an auditor event.
|
||||
@@ -66,7 +68,7 @@ export type AuditorEventOptions<TMeta extends JsonObject> = {
|
||||
*/
|
||||
eventId: string;
|
||||
|
||||
severityLevel?: AuditorEventSeverityLevel;
|
||||
severityLevel?: AuditorServiceEventSeverityLevel;
|
||||
|
||||
/** (Optional) The associated HTTP request, if applicable. */
|
||||
request?: Request<any, any, any, any, any>;
|
||||
@@ -83,7 +85,7 @@ export type AuditorEventOptions<TMeta extends JsonObject> = {
|
||||
export type AuditorEvent = [
|
||||
eventId: string,
|
||||
meta: {
|
||||
severityLevel: AuditorEventSeverityLevel;
|
||||
severityLevel: AuditorServiceEventSeverityLevel;
|
||||
actor: AuditorEventActorDetails;
|
||||
meta?: JsonObject;
|
||||
request?: AuditorEventRequest;
|
||||
@@ -91,7 +93,7 @@ export type AuditorEvent = [
|
||||
];
|
||||
|
||||
/** @public */
|
||||
export const defaultProdFormat = winston.format.combine(
|
||||
export const defaultFormatter = winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
}),
|
||||
@@ -109,13 +111,6 @@ export const auditorFieldFormat = winston.format(info => {
|
||||
return { ...info, isAuditorEvent: true };
|
||||
})();
|
||||
|
||||
/** @public */
|
||||
export interface RootAuditorOptions {
|
||||
meta?: JsonObject;
|
||||
format?: Format;
|
||||
transports?: winston.transport[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link @backstage/backend-plugin-api#AuditorService} implementation based on winston.
|
||||
*
|
||||
@@ -162,12 +157,10 @@ export class DefaultAuditorService implements AuditorService {
|
||||
this.impl.log(auditEvent);
|
||||
}
|
||||
|
||||
async createEvent<TMeta extends JsonObject>(
|
||||
options: Parameters<AuditorCreateEvent<TMeta>>[0],
|
||||
): ReturnType<AuditorCreateEvent<TMeta>> {
|
||||
if (!options.suppressInitialEvent) {
|
||||
await this.log({ ...options, status: 'initiated' });
|
||||
}
|
||||
async createEvent(
|
||||
options: AuditorServiceCreateEventOptions,
|
||||
): Promise<AuditorServiceEvent> {
|
||||
await this.log({ ...options, status: 'initiated' });
|
||||
|
||||
return {
|
||||
success: async params => {
|
||||
@@ -256,11 +249,28 @@ export class DefaultAuditorService implements AuditorService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a root auditor.
|
||||
* If `rootLogger` is provided, the root auditor will default to using it.
|
||||
* Otherwise, a new logger will be created using the provided `meta`, `format`, and `transports`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RootAuditorOptions =
|
||||
| {
|
||||
meta?: JsonObject;
|
||||
format?: Format;
|
||||
transports?: winston.transport[];
|
||||
}
|
||||
| {
|
||||
rootLogger: RootLoggerService;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export class DefaultRootAuditorService {
|
||||
private readonly impl: winston.Logger;
|
||||
private readonly impl: WinstonLogger;
|
||||
|
||||
private constructor(impl: winston.Logger) {
|
||||
private constructor(impl: WinstonLogger) {
|
||||
this.impl = impl;
|
||||
}
|
||||
|
||||
@@ -268,35 +278,44 @@ export class DefaultRootAuditorService {
|
||||
* Creates a {@link DefaultRootAuditorService} instance.
|
||||
*/
|
||||
static create(options?: RootAuditorOptions): DefaultRootAuditorService {
|
||||
const defaultFormatter =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? defaultProdFormat
|
||||
: DefaultRootAuditorService.colorFormat();
|
||||
if (options && 'rootLogger' in options) {
|
||||
return new DefaultRootAuditorService(
|
||||
options.rootLogger.child({ isAuditorEvent: true }) as WinstonLogger,
|
||||
);
|
||||
}
|
||||
|
||||
let auditor = winston.createLogger({
|
||||
let auditor = WinstonLogger.create({
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
},
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
auditorFieldFormat,
|
||||
options?.format ?? defaultFormatter,
|
||||
),
|
||||
transports: options?.transports ?? defaultConsoleTransport,
|
||||
transports: options?.transports,
|
||||
});
|
||||
|
||||
if (options?.meta) {
|
||||
auditor = auditor.child(options.meta);
|
||||
auditor = auditor.child(options.meta) as WinstonLogger;
|
||||
}
|
||||
|
||||
return new DefaultRootAuditorService(auditor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pretty printed winston log formatter.
|
||||
*/
|
||||
static colorFormat(): Format {
|
||||
return colorFormat();
|
||||
}
|
||||
async log(auditorEvent: AuditorEvent): Promise<void> {
|
||||
const [eventId, meta] = auditorEvent;
|
||||
|
||||
async log(auditEvent: AuditorEvent): Promise<void> {
|
||||
this.impl.info(...auditEvent);
|
||||
// change `error` type to a string for logging purposes
|
||||
let fields: Omit<AuditorEvent[1], 'error'> & { error?: string };
|
||||
|
||||
if ('error' in meta) {
|
||||
fields = { ...meta, error: meta.error.toString() };
|
||||
} else {
|
||||
fields = meta;
|
||||
}
|
||||
|
||||
this.impl.info(eventId, fields);
|
||||
}
|
||||
|
||||
forPlugin(deps: {
|
||||
@@ -304,7 +323,9 @@ export class DefaultRootAuditorService {
|
||||
httpAuth: HttpAuthService;
|
||||
plugin: PluginMetadataService;
|
||||
}): AuditorService {
|
||||
const impl = new DefaultRootAuditorService(this.impl.child({}));
|
||||
const impl = new DefaultRootAuditorService(
|
||||
this.impl.child({}) as WinstonLogger,
|
||||
);
|
||||
return DefaultAuditorService.create(impl, deps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,23 +18,7 @@ import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import type { Config } from '@backstage/config';
|
||||
import * as winston from 'winston';
|
||||
import { defaultConsoleTransport } from '../../lib/defaultConsoleTransport';
|
||||
import {
|
||||
DefaultRootAuditorService,
|
||||
auditorFieldFormat,
|
||||
defaultProdFormat,
|
||||
} from './Auditor';
|
||||
|
||||
const transports = {
|
||||
auditorConsole: (config?: Config) => {
|
||||
if (!config?.getOptionalBoolean('console.enabled')) {
|
||||
return [];
|
||||
}
|
||||
return [defaultConsoleTransport];
|
||||
},
|
||||
};
|
||||
import { DefaultRootAuditorService } from './Auditor';
|
||||
|
||||
/**
|
||||
* Plugin-level auditing.
|
||||
@@ -48,26 +32,13 @@ const transports = {
|
||||
export const auditorServiceFactory = createServiceFactory({
|
||||
service: coreServices.auditor,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
rootLogger: coreServices.rootLogger,
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
plugin: coreServices.pluginMetadata,
|
||||
},
|
||||
async createRootContext({ config }) {
|
||||
const auditorConfig = config.getOptionalConfig('backend.auditor');
|
||||
|
||||
const auditor = DefaultRootAuditorService.create({
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
},
|
||||
format: winston.format.combine(
|
||||
auditorFieldFormat,
|
||||
process.env.NODE_ENV === 'production'
|
||||
? defaultProdFormat
|
||||
: DefaultRootAuditorService.colorFormat(),
|
||||
),
|
||||
transports: [...transports.auditorConsole(auditorConfig)],
|
||||
});
|
||||
async createRootContext({ rootLogger }) {
|
||||
const auditor = DefaultRootAuditorService.create({ rootLogger });
|
||||
|
||||
return auditor;
|
||||
},
|
||||
|
||||
@@ -19,11 +19,16 @@ import {
|
||||
RootLoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Format } from 'logform';
|
||||
import { Logger, transport as Transport, createLogger, format } from 'winston';
|
||||
import { colorFormat } from '../../lib/colorFormat';
|
||||
import { defaultConsoleTransport } from '../../lib/defaultConsoleTransport';
|
||||
import { redacterFormat } from '../../lib/redacterFormat';
|
||||
import { Format, TransformableInfo } from 'logform';
|
||||
import {
|
||||
Logger,
|
||||
format,
|
||||
createLogger,
|
||||
transports,
|
||||
transport as Transport,
|
||||
} from 'winston';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { escapeRegExp } from '../../lib/escapeRegExp';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -60,7 +65,7 @@ export class WinstonLogger implements RootLoggerService {
|
||||
options.format ?? defaultFormatter,
|
||||
redacter.format,
|
||||
),
|
||||
transports: options.transports ?? defaultConsoleTransport,
|
||||
transports: options.transports ?? new transports.Console(),
|
||||
});
|
||||
|
||||
if (options.meta) {
|
||||
@@ -77,14 +82,87 @@ export class WinstonLogger implements RootLoggerService {
|
||||
format: Format;
|
||||
add: (redactions: Iterable<string>) => void;
|
||||
} {
|
||||
return redacterFormat();
|
||||
const redactionSet = new Set<string>();
|
||||
|
||||
let redactionPattern: RegExp | undefined = undefined;
|
||||
|
||||
return {
|
||||
format: format((obj: TransformableInfo) => {
|
||||
if (!redactionPattern || !obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '***');
|
||||
|
||||
return obj;
|
||||
})(),
|
||||
add(newRedactions) {
|
||||
let added = 0;
|
||||
for (const redactionToTrim of newRedactions) {
|
||||
// Trimming the string ensures that we don't accdentally get extra
|
||||
// newlines or other whitespace interfering with the redaction; this
|
||||
// can happen for example when using string literals in yaml
|
||||
const redaction = redactionToTrim.trim();
|
||||
// Exclude secrets that are empty or just one character in length. These
|
||||
// typically mean that you are running local dev or tests, or using the
|
||||
// --lax flag which sets things to just 'x'.
|
||||
if (redaction.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
if (!redactionSet.has(redaction)) {
|
||||
redactionSet.add(redaction);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
const redactions = Array.from(redactionSet)
|
||||
.map(r => escapeRegExp(r))
|
||||
.join('|');
|
||||
redactionPattern = new RegExp(`(${redactions})`, 'g');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pretty printed winston log formatter.
|
||||
*/
|
||||
static colorFormat(): Format {
|
||||
return colorFormat();
|
||||
const colorizer = format.colorize();
|
||||
|
||||
return format.combine(
|
||||
format.timestamp(),
|
||||
format.colorize({
|
||||
colors: {
|
||||
timestamp: 'dim',
|
||||
prefix: 'blue',
|
||||
field: 'cyan',
|
||||
debug: 'grey',
|
||||
},
|
||||
}),
|
||||
format.printf((info: TransformableInfo) => {
|
||||
const { timestamp, level, message, plugin, service, ...fields } = info;
|
||||
const prefix = plugin || service;
|
||||
const timestampColor = colorizer.colorize('timestamp', timestamp);
|
||||
const prefixColor = colorizer.colorize('prefix', prefix);
|
||||
|
||||
const extraFields = Object.entries(fields)
|
||||
.map(([key, value]) => {
|
||||
let stringValue = '';
|
||||
|
||||
try {
|
||||
stringValue = `${value}`;
|
||||
} catch (e) {
|
||||
stringValue = '[field value not castable to string]';
|
||||
}
|
||||
|
||||
return `${colorizer.colorize('field', `${key}`)}=${stringValue}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { format } from 'winston';
|
||||
import { defaultConsoleTransport } from '../../lib/defaultConsoleTransport';
|
||||
import { createConfigSecretEnumerator } from '../rootConfig/createConfigSecretEnumerator';
|
||||
import { transports, format } from 'winston';
|
||||
import { WinstonLogger } from '../rootLogger/WinstonLogger';
|
||||
import { createConfigSecretEnumerator } from '../rootConfig/createConfigSecretEnumerator';
|
||||
|
||||
/**
|
||||
* Root-level logging.
|
||||
@@ -47,7 +46,7 @@ export const rootLoggerServiceFactory = createServiceFactory({
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
: WinstonLogger.colorFormat(),
|
||||
transports: [defaultConsoleTransport],
|
||||
transports: [new transports.Console()],
|
||||
});
|
||||
|
||||
const secretEnumerator = await createConfigSecretEnumerator({ logger });
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Format, TransformableInfo } from 'logform';
|
||||
import { format } from 'winston';
|
||||
|
||||
/**
|
||||
* Creates a pretty printed winston log format.
|
||||
*/
|
||||
export function colorFormat(): Format {
|
||||
const colorizer = format.colorize();
|
||||
|
||||
return format.combine(
|
||||
format.timestamp(),
|
||||
format.colorize({
|
||||
colors: {
|
||||
timestamp: 'dim',
|
||||
prefix: 'blue',
|
||||
field: 'cyan',
|
||||
debug: 'grey',
|
||||
},
|
||||
}),
|
||||
format.printf((info: TransformableInfo) => {
|
||||
const { timestamp, level, message, plugin, service, ...fields } = info;
|
||||
const prefix = plugin || service;
|
||||
const timestampColor = colorizer.colorize('timestamp', timestamp);
|
||||
const prefixColor = colorizer.colorize('prefix', prefix);
|
||||
|
||||
const extraFields = Object.entries(fields)
|
||||
.map(([key, value]) => {
|
||||
let stringValue = '';
|
||||
try {
|
||||
stringValue = `${value}`;
|
||||
} catch (e) {
|
||||
stringValue = '[field value not castable to string]';
|
||||
}
|
||||
return `${colorizer.colorize('field', `${key}`)}=${stringValue}`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 { transports } from 'winston';
|
||||
|
||||
export const defaultConsoleTransport = new transports.Console();
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Format, TransformableInfo } from 'logform';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { format } from 'winston';
|
||||
import { escapeRegExp } from './escapeRegExp';
|
||||
|
||||
/**
|
||||
* Creates a winston log format for redacting secrets.
|
||||
*/
|
||||
export function redacterFormat(): {
|
||||
format: Format;
|
||||
add: (redactions: Iterable<string>) => void;
|
||||
} {
|
||||
const redactionSet = new Set<string>();
|
||||
|
||||
let redactionPattern: RegExp | undefined = undefined;
|
||||
|
||||
return {
|
||||
format: format((obj: TransformableInfo) => {
|
||||
if (!redactionPattern || !obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '***');
|
||||
|
||||
return obj;
|
||||
})(),
|
||||
add(newRedactions) {
|
||||
let added = 0;
|
||||
for (const redactionToTrim of newRedactions) {
|
||||
// Trimming the string ensures that we don't accdentally get extra
|
||||
// newlines or other whitespace interfering with the redaction; this
|
||||
// can happen for example when using string literals in yaml
|
||||
const redaction = redactionToTrim.trim();
|
||||
// Exclude secrets that are empty or just one character in length. These
|
||||
// typically mean that you are running local dev or tests, or using the
|
||||
// --lax flag which sets things to just 'x'.
|
||||
if (redaction.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
if (!redactionSet.has(redaction)) {
|
||||
redactionSet.add(redaction);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
const redactions = Array.from(redactionSet)
|
||||
.map(r => escapeRegExp(r))
|
||||
.join('|');
|
||||
redactionPattern = new RegExp(`(${redactions})`, 'g');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -26,40 +26,35 @@ import { Readable } from 'stream';
|
||||
import type { Request as Request_2 } from 'express';
|
||||
import type { Response as Response_2 } from 'express';
|
||||
|
||||
// @public (undocumented)
|
||||
export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
|
||||
eventId: string;
|
||||
severityLevel?: AuditorEventSeverityLevel;
|
||||
request?: Request_2<any, any, any, any, any>;
|
||||
meta?: TRootMeta;
|
||||
suppressInitialEvent?: boolean;
|
||||
}) => Promise<{
|
||||
success<TMeta extends JsonObject>(options?: { meta?: TMeta }): Promise<void>;
|
||||
fail<TMeta extends JsonObject, TError extends Error>(
|
||||
options: {
|
||||
meta?: TMeta;
|
||||
} & (
|
||||
| {
|
||||
error: TError;
|
||||
}
|
||||
| {
|
||||
errors: TError[];
|
||||
}
|
||||
),
|
||||
): Promise<void>;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export type AuditorEventSeverityLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
// @public
|
||||
export interface AuditorService {
|
||||
// (undocumented)
|
||||
createEvent<TMeta extends JsonObject>(
|
||||
options: Parameters<AuditorCreateEvent<TMeta>>[0],
|
||||
): ReturnType<AuditorCreateEvent<TMeta>>;
|
||||
createEvent(
|
||||
options: AuditorServiceCreateEventOptions,
|
||||
): Promise<AuditorServiceEvent>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type AuditorServiceCreateEventOptions = {
|
||||
eventId: string;
|
||||
severityLevel?: AuditorServiceEventSeverityLevel;
|
||||
request?: Request_2<any, any, any, any, any>;
|
||||
meta?: JsonObject;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type AuditorServiceEvent = {
|
||||
success(options?: { meta?: JsonObject }): Promise<void>;
|
||||
fail(options: { meta?: JsonObject; error: Error }): Promise<void>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type AuditorServiceEventSeverityLevel =
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high'
|
||||
| 'critical';
|
||||
|
||||
// @public
|
||||
export interface AuthService {
|
||||
authenticate(
|
||||
|
||||
@@ -24,10 +24,14 @@ import type { Request } from 'express';
|
||||
* critical: root permission changes
|
||||
* @public
|
||||
*/
|
||||
export type AuditorEventSeverityLevel = 'low' | 'medium' | 'high' | 'critical';
|
||||
export type AuditorServiceEventSeverityLevel =
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high'
|
||||
| 'critical';
|
||||
|
||||
/** @public */
|
||||
export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
|
||||
export type AuditorServiceCreateEventOptions = {
|
||||
/**
|
||||
* Use kebab-case to name audit events (e.g., "user-login", "file-download", "fetch"). Represents a logical group of similar events or operations. For example, "fetch" could be used as an eventId encompassing various fetch methods like "by-id" or "by-location".
|
||||
*
|
||||
@@ -36,7 +40,7 @@ export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
|
||||
eventId: string;
|
||||
|
||||
/** (Optional) The severity level for the audit event. */
|
||||
severityLevel?: AuditorEventSeverityLevel;
|
||||
severityLevel?: AuditorServiceEventSeverityLevel;
|
||||
|
||||
/** (Optional) The associated HTTP request, if applicable. */
|
||||
request?: Request<any, any, any, any, any>;
|
||||
@@ -46,18 +50,14 @@ export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
|
||||
* 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;
|
||||
meta?: JsonObject;
|
||||
};
|
||||
|
||||
/** (Optional) Suppresses the automatic initial event. */
|
||||
suppressInitialEvent?: boolean;
|
||||
}) => Promise<{
|
||||
success<TMeta extends JsonObject>(options?: { meta?: TMeta }): Promise<void>;
|
||||
fail<TMeta extends JsonObject, TError extends Error>(
|
||||
options: {
|
||||
meta?: TMeta;
|
||||
} & ({ error: TError } | { errors: TError[] }),
|
||||
): Promise<void>;
|
||||
}>;
|
||||
/** @public */
|
||||
export type AuditorServiceEvent = {
|
||||
success(options?: { meta?: JsonObject }): Promise<void>;
|
||||
fail(options: { meta?: JsonObject; error: Error }): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A service that provides an auditor facility.
|
||||
@@ -67,7 +67,7 @@ export type AuditorCreateEvent<TRootMeta extends JsonObject> = (options: {
|
||||
* @public
|
||||
*/
|
||||
export interface AuditorService {
|
||||
createEvent<TMeta extends JsonObject>(
|
||||
options: Parameters<AuditorCreateEvent<TMeta>>[0],
|
||||
): ReturnType<AuditorCreateEvent<TMeta>>;
|
||||
createEvent(
|
||||
options: AuditorServiceCreateEventOptions,
|
||||
): Promise<AuditorServiceEvent>;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
*/
|
||||
|
||||
export type {
|
||||
AuditorCreateEvent,
|
||||
AuditorEventSeverityLevel,
|
||||
AuditorService,
|
||||
AuditorServiceCreateEventOptions,
|
||||
AuditorServiceEvent,
|
||||
AuditorServiceEventSeverityLevel,
|
||||
} from './AuditorService';
|
||||
export type {
|
||||
AuthService,
|
||||
@@ -33,7 +34,6 @@ export type {
|
||||
CacheServiceOptions,
|
||||
CacheServiceSetOptions,
|
||||
} from './CacheService';
|
||||
export { coreServices } from './coreServices';
|
||||
export type { DatabaseService } from './DatabaseService';
|
||||
export type { DiscoveryService } from './DiscoveryService';
|
||||
export type { HttpAuthService } from './HttpAuthService';
|
||||
@@ -86,3 +86,4 @@ export type {
|
||||
UrlReaderServiceSearchResponseFile,
|
||||
} from './UrlReaderService';
|
||||
export type { BackstageUserInfo, UserInfoService } from './UserInfoService';
|
||||
export { coreServices } from './coreServices';
|
||||
|
||||
@@ -36,7 +36,6 @@ import { ParamsDictionary } from 'express-serve-static-core';
|
||||
import { ParsedQs } from 'qs';
|
||||
import { PermissionsRegistryService } from '@backstage/backend-plugin-api';
|
||||
import { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import type { RootAuditorOptions } from '@backstage/backend-defaults/auditor';
|
||||
import { RootConfigService } from '@backstage/backend-plugin-api';
|
||||
import { RootHealthService } from '@backstage/backend-plugin-api';
|
||||
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
|
||||
@@ -155,19 +154,11 @@ export function mockErrorHandler(): ErrorRequestHandler<
|
||||
|
||||
// @public
|
||||
export namespace mockServices {
|
||||
export function auditor(
|
||||
options?: auditor.Options & {
|
||||
pluginId?: string;
|
||||
},
|
||||
): AuditorService;
|
||||
export function auditor(options?: { pluginId?: string }): AuditorService;
|
||||
// (undocumented)
|
||||
export namespace auditor {
|
||||
// (undocumented)
|
||||
export type Options = RootAuditorOptions;
|
||||
const // (undocumented)
|
||||
factory: (
|
||||
options?: auditor.Options,
|
||||
) => ServiceFactory<AuditorService, 'plugin', 'singleton'>;
|
||||
factory: () => ServiceFactory<AuditorService, 'plugin', 'singleton'>;
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?: Partial<AuditorService> | undefined,
|
||||
|
||||
@@ -25,31 +25,8 @@ describe('MockAuditorService', () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should log', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
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(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send initiated log with createEvent', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const spy = jest.spyOn(MockRootAuditorService.prototype, 'log');
|
||||
|
||||
const pluginId = 'test-plugin';
|
||||
|
||||
@@ -68,11 +45,11 @@ describe('MockAuditorService', () => {
|
||||
eventId: 'test-event',
|
||||
});
|
||||
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send succeeded log with createEvent', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const spy = jest.spyOn(MockRootAuditorService.prototype, 'log');
|
||||
|
||||
const pluginId = 'test-plugin';
|
||||
|
||||
@@ -93,11 +70,11 @@ describe('MockAuditorService', () => {
|
||||
|
||||
await auditorEvent.success();
|
||||
|
||||
expect(console.log).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should send failed log with createEvent', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const spy = jest.spyOn(MockRootAuditorService.prototype, 'log');
|
||||
|
||||
const pluginId = 'test-plugin';
|
||||
|
||||
@@ -118,6 +95,6 @@ describe('MockAuditorService', () => {
|
||||
|
||||
await auditorEvent.fail({ error: new Error('error') as ErrorLike });
|
||||
|
||||
expect(console.log).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,17 +19,19 @@ import type {
|
||||
AuditorEventOptions,
|
||||
} from '@backstage/backend-defaults/auditor';
|
||||
import type {
|
||||
AuditorCreateEvent,
|
||||
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 type { mockServices } from './mockServices';
|
||||
import { mockServices } from './mockServices';
|
||||
|
||||
export class MockAuditorService implements AuditorService {
|
||||
private readonly impl: MockRootAuditorService;
|
||||
@@ -69,12 +71,10 @@ export class MockAuditorService implements AuditorService {
|
||||
this.impl.log(auditEvent);
|
||||
}
|
||||
|
||||
async createEvent<TMeta extends JsonObject>(
|
||||
options: Parameters<AuditorCreateEvent<TMeta>>[0],
|
||||
): ReturnType<AuditorCreateEvent<TMeta>> {
|
||||
if (!options.suppressInitialEvent) {
|
||||
await this.log({ ...options, status: 'initiated' });
|
||||
}
|
||||
async createEvent(
|
||||
options: AuditorServiceCreateEventOptions,
|
||||
): Promise<AuditorServiceEvent> {
|
||||
await this.log({ ...options, status: 'initiated' });
|
||||
|
||||
return {
|
||||
success: async params => {
|
||||
@@ -164,20 +164,29 @@ export class MockAuditorService implements AuditorService {
|
||||
}
|
||||
|
||||
export class MockRootAuditorService {
|
||||
private readonly options: mockServices.auditor.Options;
|
||||
private readonly impl: RootLoggerService;
|
||||
|
||||
private constructor(options?: mockServices.auditor.Options) {
|
||||
this.options = options ?? {};
|
||||
private constructor() {
|
||||
this.impl = mockServices.rootLogger();
|
||||
}
|
||||
|
||||
static create(
|
||||
options?: mockServices.auditor.Options,
|
||||
): MockRootAuditorService {
|
||||
return new MockRootAuditorService(options);
|
||||
static create(): MockRootAuditorService {
|
||||
return new MockRootAuditorService();
|
||||
}
|
||||
|
||||
async log(auditEvent: AuditorEvent): Promise<void> {
|
||||
this.#log(...auditEvent);
|
||||
async log(auditorEvent: AuditorEvent): Promise<void> {
|
||||
const [eventId, meta] = auditorEvent;
|
||||
|
||||
// change `error` type to a string for logging purposes
|
||||
let fields: Omit<AuditorEvent[1], 'error'> & { error?: string };
|
||||
|
||||
if ('error' in meta) {
|
||||
fields = { ...meta, error: meta.error.toString() };
|
||||
} else {
|
||||
fields = meta;
|
||||
}
|
||||
|
||||
this.impl.info(eventId, fields);
|
||||
}
|
||||
|
||||
forPlugin(deps: {
|
||||
@@ -185,11 +194,7 @@ export class MockRootAuditorService {
|
||||
httpAuth: HttpAuthService;
|
||||
plugin: PluginMetadataService;
|
||||
}): AuditorService {
|
||||
const impl = new MockRootAuditorService(this.options);
|
||||
const impl = new MockRootAuditorService();
|
||||
return MockAuditorService.create(impl, deps);
|
||||
}
|
||||
|
||||
#log(message: string, meta?: AuditorEvent[1]) {
|
||||
console.log(message, JSON.stringify(meta));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -227,12 +226,7 @@ export namespace mockServices {
|
||||
/**
|
||||
* Creates a mock implementation of the `AuditorService`.
|
||||
*/
|
||||
export function auditor(
|
||||
options?: auditor.Options & {
|
||||
pluginId?: string;
|
||||
},
|
||||
): AuditorService {
|
||||
const service = 'backstage';
|
||||
export function auditor(options?: { pluginId?: string }): AuditorService {
|
||||
const pluginId = options?.pluginId ?? 'test';
|
||||
const mockAuth = new MockAuthService({
|
||||
pluginId,
|
||||
@@ -247,9 +241,7 @@ export namespace mockServices {
|
||||
getId: () => pluginId,
|
||||
};
|
||||
|
||||
const auditorMock = MockRootAuditorService.create({
|
||||
meta: options?.meta ? { ...options.meta, service } : { service },
|
||||
});
|
||||
const auditorMock = MockRootAuditorService.create();
|
||||
|
||||
return auditorMock.forPlugin({
|
||||
auth: mockAuth,
|
||||
@@ -259,9 +251,7 @@ export namespace mockServices {
|
||||
}
|
||||
|
||||
export namespace auditor {
|
||||
export type Options = RootAuditorOptions;
|
||||
|
||||
export const factory = (options?: auditor.Options) =>
|
||||
export const factory = () =>
|
||||
createServiceFactory({
|
||||
service: coreServices.auditor,
|
||||
deps: {
|
||||
@@ -270,11 +260,7 @@ export namespace mockServices {
|
||||
plugin: coreServices.pluginMetadata,
|
||||
},
|
||||
createRootContext() {
|
||||
const service = 'backstage';
|
||||
|
||||
return MockRootAuditorService.create({
|
||||
meta: options?.meta ? { ...options.meta, service } : { service },
|
||||
});
|
||||
return MockRootAuditorService.create();
|
||||
},
|
||||
factory(
|
||||
{ auth: mockAuth, httpAuth: mockHttpAuth, plugin: mockPlugin },
|
||||
|
||||
@@ -16,24 +16,22 @@
|
||||
|
||||
import { Backend, createSpecializedBackend } from '@backstage/backend-app-api';
|
||||
import {
|
||||
createServiceFactory,
|
||||
BackendFeature,
|
||||
ExtensionPoint,
|
||||
coreServices,
|
||||
createBackendModule,
|
||||
createBackendPlugin,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { mockServices } from '../services';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import { mockServices } from '../services';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
InternalBackendFeature,
|
||||
InternalBackendRegistrations,
|
||||
} from '../../../../backend-plugin-api/src/wiring/types';
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { HostDiscovery } from '@backstage/backend-defaults/discovery';
|
||||
import {
|
||||
DefaultRootHttpRouter,
|
||||
ExtendedHttpServer,
|
||||
@@ -41,8 +39,7 @@ import {
|
||||
createHealthRouter,
|
||||
createHttpServer,
|
||||
} from '@backstage/backend-defaults/rootHttpRouter';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { HostDiscovery } from '@backstage/backend-defaults/discovery';
|
||||
|
||||
/** @public */
|
||||
export interface TestBackendOptions<TExtensionPoints extends any[]> {
|
||||
|
||||
@@ -137,7 +137,6 @@ export type CatalogEnvironment = {
|
||||
database: DatabaseService;
|
||||
config: RootConfigService;
|
||||
reader: UrlReaderService;
|
||||
// TODO: Require all services once `backend-legacy` is removed
|
||||
permissions: PermissionsService | PermissionAuthorizer;
|
||||
permissionsRegistry?: PermissionsRegistryService;
|
||||
scheduler?: SchedulerService;
|
||||
|
||||
@@ -330,10 +330,23 @@ export async function createRouter(
|
||||
});
|
||||
|
||||
writeSingleEntityResponse(res, entities, `No entity with uid ${uid}`);
|
||||
|
||||
|
||||
await auditorEvent?.success({
|
||||
meta: {
|
||||
entities: entities,
|
||||
// stringify to entity refs
|
||||
entities: entities.entities.reduce((arr, element) => {
|
||||
if (!element) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
if (typeof element === 'string') {
|
||||
arr.push(element);
|
||||
return arr;
|
||||
}
|
||||
|
||||
arr.push(stringifyEntityRef(element));
|
||||
return arr;
|
||||
}, [] as string[]),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -793,7 +806,8 @@ export async function createRouter(
|
||||
const errors = processingResult.errors.map(e => serializeError(e));
|
||||
|
||||
await auditorEvent?.fail({
|
||||
errors: errors,
|
||||
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
||||
error: (AggregateError as any)(errors, 'Could not validate entity'),
|
||||
});
|
||||
|
||||
res.status(400).json({
|
||||
|
||||
@@ -634,7 +634,6 @@ export class TaskManager implements TaskContext_2 {
|
||||
auth?: AuthService,
|
||||
config?: Config,
|
||||
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
|
||||
auditor?: AuditorService,
|
||||
): TaskManager;
|
||||
// (undocumented)
|
||||
get createdBy(): string | undefined;
|
||||
|
||||
@@ -75,7 +75,6 @@ export class TaskManager implements TaskContext {
|
||||
auth?: AuthService,
|
||||
config?: Config,
|
||||
additionalWorkspaceProviders?: Record<string, WorkspaceProvider>,
|
||||
auditor?: AuditorService,
|
||||
) {
|
||||
const workspaceService = DefaultWorkspaceService.create(
|
||||
task,
|
||||
@@ -91,7 +90,6 @@ export class TaskManager implements TaskContext {
|
||||
logger,
|
||||
workspaceService,
|
||||
auth,
|
||||
auditor,
|
||||
);
|
||||
agent.startTimeout();
|
||||
return agent;
|
||||
@@ -105,7 +103,6 @@ export class TaskManager implements TaskContext {
|
||||
private readonly logger: Logger,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly auth?: AuthService,
|
||||
private readonly auditor?: AuditorService,
|
||||
) {}
|
||||
|
||||
get spec() {
|
||||
@@ -204,26 +201,6 @@ export class TaskManager implements TaskContext {
|
||||
if (this.heartbeatTimeoutId) {
|
||||
clearTimeout(this.heartbeatTimeoutId);
|
||||
}
|
||||
|
||||
const auditorEvent = await this.auditor?.createEvent({
|
||||
eventId: 'task',
|
||||
severityLevel: 'medium',
|
||||
meta: {
|
||||
actionType: 'execution',
|
||||
taskId: this.task.taskId,
|
||||
taskParameters: this.task.spec.parameters,
|
||||
},
|
||||
// The initial event is created in TaskWorker
|
||||
suppressInitialEvent: true,
|
||||
});
|
||||
|
||||
if (result === 'failed') {
|
||||
await auditorEvent?.fail({
|
||||
error: metadata?.error as any,
|
||||
});
|
||||
} else {
|
||||
await auditorEvent?.success();
|
||||
}
|
||||
}
|
||||
|
||||
private startTimeout() {
|
||||
@@ -396,7 +373,6 @@ export class StorageTaskBroker implements TaskBroker {
|
||||
this.auth,
|
||||
this.config,
|
||||
this.additionalWorkspaceProviders,
|
||||
this.auditor,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ export class TaskWorker {
|
||||
}
|
||||
|
||||
async runOneTask(task: TaskContext) {
|
||||
await this.auditor?.createEvent({
|
||||
const auditorEvent = await this.auditor?.createEvent({
|
||||
eventId: 'task',
|
||||
severityLevel: 'medium',
|
||||
meta: {
|
||||
@@ -197,8 +197,12 @@ export class TaskWorker {
|
||||
);
|
||||
|
||||
await task.complete('completed', { output });
|
||||
await auditorEvent?.success();
|
||||
} catch (error) {
|
||||
assertError(error);
|
||||
await auditorEvent?.fail({
|
||||
error,
|
||||
});
|
||||
await task.complete('failed', {
|
||||
error: { name: error.name, message: error.message },
|
||||
});
|
||||
|
||||
@@ -589,7 +589,13 @@ export async function createRouter(
|
||||
const result = validate(values, parameters);
|
||||
|
||||
if (!result.valid) {
|
||||
await auditorEvent?.fail({ errors: result.errors });
|
||||
await auditorEvent?.fail({
|
||||
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
||||
error: (AggregateError as any)(
|
||||
result.errors,
|
||||
'Could not create entity',
|
||||
),
|
||||
});
|
||||
|
||||
res.status(400).json({ errors: result.errors });
|
||||
return;
|
||||
@@ -987,7 +993,11 @@ export async function createRouter(
|
||||
const result = validate(body.values, parameters);
|
||||
if (!result.valid) {
|
||||
await auditorEvent?.fail({
|
||||
errors: result.errors,
|
||||
// TODO(Rugvip): Seems like there aren't proper types for AggregateError yet
|
||||
error: (AggregateError as any)(
|
||||
result.errors,
|
||||
'Could not execute dry run',
|
||||
),
|
||||
meta: {
|
||||
templateRef: templateRef,
|
||||
parameters: template.spec.parameters,
|
||||
|
||||
Reference in New Issue
Block a user