From 81746eb44bda866869ce721daea38c5a168d45de Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Tue, 28 Jun 2022 12:52:59 +0100 Subject: [PATCH 1/6] feat: log User's metadata.name on POST to /v2/tasks Signed-off-by: Marco Crivellaro --- plugins/scaffolder-backend/src/service/router.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 2abb730250..809f64c4c9 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -17,17 +17,17 @@ import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { + Entity, parseEntityRef, stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { Entity } from '@backstage/catalog-model'; import { Config, JsonObject } from '@backstage/config'; import { InputError, NotFoundError, stringifyError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { - TemplateEntityV1beta3, TaskSpec, + TemplateEntityV1beta3, templateEntityV1beta3Validator, } from '@backstage/plugin-scaffolder-common'; import { JsonValue } from '@backstage/types'; @@ -35,8 +35,8 @@ import express from 'express'; import Router from 'express-promise-router'; import { validate } from 'jsonschema'; import { Logger } from 'winston'; -import { TemplateFilter } from '../lib'; import { z } from 'zod'; +import { TemplateFilter } from '../lib'; import { createBuiltinActions, DatabaseTaskStore, @@ -47,7 +47,7 @@ import { } from '../scaffolder'; import { createDryRunner } from '../scaffolder/dryrun'; import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; -import { getEntityBaseUrl, getWorkingDirectory, findTemplate } from './helpers'; +import { findTemplate, getEntityBaseUrl, getWorkingDirectory } from './helpers'; /** * RouterOptions @@ -193,6 +193,8 @@ export async function createRouter( ? await catalogClient.getEntityByRef(userEntityRef, { token }) : undefined; + logger.info(`POST /v2/tasks from ${userEntity?.metadata.name}`); + const values = req.body.values; const template = await findTemplate({ From 8a1987a7b2c06bf8cbca36a8cc07ba5554655559 Mon Sep 17 00:00:00 2001 From: David Rubio Vidal Date: Thu, 30 Jun 2022 14:18:47 +0200 Subject: [PATCH 2/6] feat: add audit logs to scaffolder-backend plugin Signed-off-by: David Rubio Vidal --- app-config.yaml | 9 ++++++ .../scaffolder-backend/src/service/router.ts | 30 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app-config.yaml b/app-config.yaml index a56a3df9d1..5806ad31b9 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -286,6 +286,15 @@ scaffolder: # email: scaffolder@backstage.io # Use to customize the default commit message when new components are created # defaultCommitMessage: 'Initial commit' + # Use to customize the audit log emitted when a template task is created + auditlog: + enabled: true + # Properties of the user entity metadata (i.e. userEntity.metadata.name) + identityMetadata: + - name + # Annotations under the user entity metadata (i.e. userEntity.metadata.annotations['name/of-annotation']) + # identityAnnotations: + # # - name/of-annotation auth: ### Add auth.keyStore.provider to more granularly control how to store JWK data when running diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 809f64c4c9..c254994a26 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -193,7 +193,35 @@ export async function createRouter( ? await catalogClient.getEntityByRef(userEntityRef, { token }) : undefined; - logger.info(`POST /v2/tasks from ${userEntity?.metadata.name}`); + const auditlogEnabled = config.getOptionalBoolean( + 'scaffolder.auditlog.enabled', + ); + + if (auditlogEnabled) { + const identityMetadata = config.getOptionalStringArray( + 'scaffolder.auditlog.identityMetadata', + ); + const identityAnnotations = config.getOptionalStringArray( + 'scaffolder.auditlog.identityAnnotations', + ); + + const logMe: { [key: string]: string }[] = []; + + identityMetadata?.forEach((prop: string) => + logMe.push({ [prop]: userEntity?.metadata[prop] as string }), + ); + identityAnnotations?.forEach((prop: string) => + logMe.push({ + [prop]: userEntity?.metadata.annotations?.[prop] as string, + }), + ); + + logger.info( + `Scaffolding template '${templateRef}'. ${ + logMe.length > 0 && JSON.stringify(logMe) + }`, + ); + } const values = req.body.values; From d10d03c941b7c498b4eafbe63081de4a5c495e43 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Wed, 13 Jul 2022 22:11:34 +0100 Subject: [PATCH 3/6] feat: unit tests and log refactor Signed-off-by: Marco Crivellaro --- app-config.yaml | 9 +- .../src/service/router.test.ts | 301 +++++++++++++++++- .../scaffolder-backend/src/service/router.ts | 20 +- 3 files changed, 309 insertions(+), 21 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 5806ad31b9..706137f991 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -288,13 +288,10 @@ scaffolder: # defaultCommitMessage: 'Initial commit' # Use to customize the audit log emitted when a template task is created auditlog: - enabled: true - # Properties of the user entity metadata (i.e. userEntity.metadata.name) - identityMetadata: - - name - # Annotations under the user entity metadata (i.e. userEntity.metadata.annotations['name/of-annotation']) + enabled: true # emit an audit log when a new scaffolder task is created, by default it logs the name of the authenticated user (if available) + # Optional authenticated user annotations to log (i.e. userEntity.metadata.annotations['name/of-annotation']) # identityAnnotations: - # # - name/of-annotation + # - name/of-annotation auth: ### Add auth.keyStore.provider to more granularly control how to store JWK data when running diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index aea33f1158..b3bef8bb9a 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -35,24 +35,25 @@ import { UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ConfigReader } from '@backstage/config'; -import ObservableImpl from 'zen-observable'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import express from 'express'; import request from 'supertest'; +import ObservableImpl from 'zen-observable'; + /** * TODO: The following should import directly from the router file. * Due to a circular dependency between this plugin and the * plugin-scaffolder-backend-module-cookiecutter plugin, it results in an error: * TypeError: _pluginscaffolderbackend.createTemplateAction is not a function */ -import { createRouter, DatabaseTaskStore, TaskBroker } from '../index'; -import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; import { parseEntityRef, stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; +import { createRouter, DatabaseTaskStore, TaskBroker } from '../index'; +import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker'; function createDatabase(): PluginDatabaseManager { return DatabaseManager.fromConfig( @@ -595,3 +596,295 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ }); }); }); + +describe('createRouter logging', () => { + let app: express.Express; + let taskBroker: TaskBroker; + const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi; + + const mockTemplate: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', + kind: 'Template', + metadata: { + description: 'Create a new CRA website project', + name: 'create-react-app-template', + tags: ['experimental', 'react', 'cra'], + title: 'Create React App Template', + annotations: { + 'backstage.io/managed-by-location': 'url:https://dev.azure.com', + }, + }, + spec: { + owner: 'web@example.com', + type: 'website', + steps: [], + parameters: { + type: 'object', + required: ['required'], + properties: { + required: { + type: 'string', + description: 'Required parameter', + }, + }, + }, + }, + }; + + const mockUser: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'guest', + annotations: { + 'google.com/email': 'bobby@tables.com', + 'example.com/uuid': 'bc48e920-f1c9-4094-bb5c-99d120e87ab8', + }, + }, + spec: { + profile: { + displayName: 'Robert Tables of the North', + }, + }, + }; + + beforeEach(async () => { + const logger = getVoidLogger(); + + const databaseTaskStore = await DatabaseTaskStore.create({ + database: await createDatabase().getClient(), + }); + taskBroker = new StorageTaskBroker(databaseTaskStore, logger); + + jest.spyOn(taskBroker, 'dispatch'); + jest.spyOn(taskBroker, 'get'); + jest.spyOn(taskBroker, 'list'); + jest.spyOn(taskBroker, 'event$'); + + jest + .spyOn(catalogClient, 'getEntityByRef') + .mockImplementation(async ref => { + const { kind } = parseEntityRef(ref); + + if (kind === 'template') { + return mockTemplate; + } + + if (kind === 'user') { + return mockUser; + } + throw new Error(`no mock found for kind: ${kind}`); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('POST /v2/tasks', () => { + it('does not log auditlog by default', async () => { + const config = new ConfigReader({}); + + const logger = getVoidLogger(); + const loggerSpy = jest.spyOn(logger, 'info'); + + const router = await createRouter({ + logger: logger, + config: config, + database: createDatabase(), + catalogClient, + reader: mockUrlReader, + taskBroker, + }); + app = express().use(router); + + const response = await request(app) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(0); + expect(response.status).toEqual(201); + }); + + it('does not log auditlog when not enabled', async () => { + const config = new ConfigReader({ + scaffolder: { + auditlog: { + enabled: false, + }, + }, + }); + + const logger = getVoidLogger(); + const loggerSpy = jest.spyOn(logger, 'info'); + + const router = await createRouter({ + logger: logger, + config: config, + database: createDatabase(), + catalogClient, + reader: mockUrlReader, + taskBroker, + }); + app = express().use(router); + + const response = await request(app) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(0); + expect(response.status).toEqual(201); + }); + + it('emits auditlog when enabled without identity', async () => { + const config = new ConfigReader({ + scaffolder: { + auditlog: { + enabled: true, + }, + }, + }); + + const logger = getVoidLogger(); + const loggerSpy = jest.spyOn(logger, 'info'); + + const router = await createRouter({ + logger: logger, + config: config, + database: createDatabase(), + catalogClient, + reader: mockUrlReader, + taskBroker, + }); + app = express().use(router); + + const response = await request(app) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + "Scaffolding task for 'template:default/create-react-app-template'", + ); + + expect(response.status).toEqual(201); + }); + + it('emits auditlog when enabled with identity', async () => { + const config = new ConfigReader({ + scaffolder: { + auditlog: { + enabled: true, + }, + }, + }); + + const logger = getVoidLogger(); + const loggerSpy = jest.spyOn(logger, 'info'); + + const router = await createRouter({ + logger: logger, + config: config, + database: createDatabase(), + catalogClient, + reader: mockUrlReader, + taskBroker, + }); + app = express().use(router); + + const mockToken = + 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; + + const response = await request(app) + .post('/v2/tasks') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + "Scaffolding task for 'template:default/create-react-app-template' created by guest", + ); + + expect(response.status).toEqual(201); + }); + + it('emits auditlog when enabled with identity and annotations', async () => { + const config = new ConfigReader({ + scaffolder: { + auditlog: { + enabled: true, + identityAnnotations: ['google.com/email', 'example.com/uuid'], + }, + }, + }); + + const logger = getVoidLogger(); + const loggerSpy = jest.spyOn(logger, 'info'); + + const router = await createRouter({ + logger: logger, + config: config, + database: createDatabase(), + catalogClient, + reader: mockUrlReader, + taskBroker, + }); + app = express().use(router); + + const mockToken = + 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; + + const response = await request(app) + .post('/v2/tasks') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + 'Scaffolding task for \'template:default/create-react-app-template\' created by guest [{"google.com/email":"bobby@tables.com"},{"example.com/uuid":"bc48e920-f1c9-4094-bb5c-99d120e87ab8"}]', + ); + + expect(response.status).toEqual(201); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index c254994a26..7d2c1848ab 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -198,29 +198,27 @@ export async function createRouter( ); if (auditlogEnabled) { - const identityMetadata = config.getOptionalStringArray( - 'scaffolder.auditlog.identityMetadata', - ); const identityAnnotations = config.getOptionalStringArray( 'scaffolder.auditlog.identityAnnotations', ); const logMe: { [key: string]: string }[] = []; - identityMetadata?.forEach((prop: string) => - logMe.push({ [prop]: userEntity?.metadata[prop] as string }), - ); identityAnnotations?.forEach((prop: string) => logMe.push({ [prop]: userEntity?.metadata.annotations?.[prop] as string, }), ); - logger.info( - `Scaffolding template '${templateRef}'. ${ - logMe.length > 0 && JSON.stringify(logMe) - }`, - ); + let userLog = ''; + if (userEntity) { + userLog = ` created by ${userEntity.metadata.name}`; + if (logMe.length > 0) { + userLog += ` ${JSON.stringify(logMe)}`; + } + } + + logger.info(`Scaffolding task for '${templateRef}'${userLog}`); } const values = req.body.values; From d10ccc2ed1a593bed73b62a79c765be099c33449 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Wed, 13 Jul 2022 22:15:50 +0100 Subject: [PATCH 4/6] chore: changeset Signed-off-by: Marco Crivellaro --- .changeset/tame-gorillas-look.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tame-gorillas-look.md diff --git a/.changeset/tame-gorillas-look.md b/.changeset/tame-gorillas-look.md new file mode 100644 index 0000000000..a976af2968 --- /dev/null +++ b/.changeset/tame-gorillas-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Introduced new option to emit an audit log message when a new scaffolder task is created From b5583f8ee087a0fb06fbdcc8518770cadde9a45b Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Fri, 15 Jul 2022 14:38:26 +0100 Subject: [PATCH 5/6] feat: simplified the scaffolding audit log Signed-off-by: Marco Crivellaro --- app-config.yaml | 6 - .../src/service/router.test.ts | 338 +++--------------- .../scaffolder-backend/src/service/router.ts | 30 +- 3 files changed, 49 insertions(+), 325 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 706137f991..a56a3df9d1 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -286,12 +286,6 @@ scaffolder: # email: scaffolder@backstage.io # Use to customize the default commit message when new components are created # defaultCommitMessage: 'Initial commit' - # Use to customize the audit log emitted when a template task is created - auditlog: - enabled: true # emit an audit log when a new scaffolder task is created, by default it logs the name of the authenticated user (if available) - # Optional authenticated user annotations to log (i.e. userEntity.metadata.annotations['name/of-annotation']) - # identityAnnotations: - # - name/of-annotation auth: ### Add auth.keyStore.provider to more granularly control how to store JWK data when running diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index b3bef8bb9a..f545e57f85 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -75,6 +75,7 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; + let loggerSpy: jest.SpyInstance; let taskBroker: TaskBroker; const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi; @@ -134,9 +135,10 @@ describe('createRouter', () => { jest.spyOn(taskBroker, 'get'); jest.spyOn(taskBroker, 'list'); jest.spyOn(taskBroker, 'event$'); + loggerSpy = jest.spyOn(logger, 'info'); const router = await createRouter({ - logger: getVoidLogger(), + logger: logger, config: new ConfigReader({}), database: createDatabase(), catalogClient, @@ -289,6 +291,48 @@ describe('createRouter', () => { }), ); }); + + it('should emit auditlog containing without user identifier when no backstage auth is passed', async () => { + await request(app) + .post('/v2/tasks') + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + 'Scaffolding task for template:default/create-react-app-template', + ); + }); + + it('should emit auditlog containing user identifier when backstage auth is passed', async () => { + const mockToken = + 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; + + await request(app) + .post('/v2/tasks') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + templateRef: stringifyEntityRef({ + kind: 'template', + name: 'create-react-app-template', + }), + values: { + required: 'required-value', + }, + }); + + expect(loggerSpy).toHaveBeenCalledTimes(1); + expect(loggerSpy).toHaveBeenCalledWith( + 'Scaffolding task for template:default/create-react-app-template created by user:default/guest', + ); + }); }); describe('GET /v2/tasks', () => { @@ -596,295 +640,3 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ }); }); }); - -describe('createRouter logging', () => { - let app: express.Express; - let taskBroker: TaskBroker; - const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi; - - const mockTemplate: TemplateEntityV1beta3 = { - apiVersion: 'scaffolder.backstage.io/v1beta3', - kind: 'Template', - metadata: { - description: 'Create a new CRA website project', - name: 'create-react-app-template', - tags: ['experimental', 'react', 'cra'], - title: 'Create React App Template', - annotations: { - 'backstage.io/managed-by-location': 'url:https://dev.azure.com', - }, - }, - spec: { - owner: 'web@example.com', - type: 'website', - steps: [], - parameters: { - type: 'object', - required: ['required'], - properties: { - required: { - type: 'string', - description: 'Required parameter', - }, - }, - }, - }, - }; - - const mockUser: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'guest', - annotations: { - 'google.com/email': 'bobby@tables.com', - 'example.com/uuid': 'bc48e920-f1c9-4094-bb5c-99d120e87ab8', - }, - }, - spec: { - profile: { - displayName: 'Robert Tables of the North', - }, - }, - }; - - beforeEach(async () => { - const logger = getVoidLogger(); - - const databaseTaskStore = await DatabaseTaskStore.create({ - database: await createDatabase().getClient(), - }); - taskBroker = new StorageTaskBroker(databaseTaskStore, logger); - - jest.spyOn(taskBroker, 'dispatch'); - jest.spyOn(taskBroker, 'get'); - jest.spyOn(taskBroker, 'list'); - jest.spyOn(taskBroker, 'event$'); - - jest - .spyOn(catalogClient, 'getEntityByRef') - .mockImplementation(async ref => { - const { kind } = parseEntityRef(ref); - - if (kind === 'template') { - return mockTemplate; - } - - if (kind === 'user') { - return mockUser; - } - throw new Error(`no mock found for kind: ${kind}`); - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('POST /v2/tasks', () => { - it('does not log auditlog by default', async () => { - const config = new ConfigReader({}); - - const logger = getVoidLogger(); - const loggerSpy = jest.spyOn(logger, 'info'); - - const router = await createRouter({ - logger: logger, - config: config, - database: createDatabase(), - catalogClient, - reader: mockUrlReader, - taskBroker, - }); - app = express().use(router); - - const response = await request(app) - .post('/v2/tasks') - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }); - - expect(loggerSpy).toHaveBeenCalledTimes(0); - expect(response.status).toEqual(201); - }); - - it('does not log auditlog when not enabled', async () => { - const config = new ConfigReader({ - scaffolder: { - auditlog: { - enabled: false, - }, - }, - }); - - const logger = getVoidLogger(); - const loggerSpy = jest.spyOn(logger, 'info'); - - const router = await createRouter({ - logger: logger, - config: config, - database: createDatabase(), - catalogClient, - reader: mockUrlReader, - taskBroker, - }); - app = express().use(router); - - const response = await request(app) - .post('/v2/tasks') - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }); - - expect(loggerSpy).toHaveBeenCalledTimes(0); - expect(response.status).toEqual(201); - }); - - it('emits auditlog when enabled without identity', async () => { - const config = new ConfigReader({ - scaffolder: { - auditlog: { - enabled: true, - }, - }, - }); - - const logger = getVoidLogger(); - const loggerSpy = jest.spyOn(logger, 'info'); - - const router = await createRouter({ - logger: logger, - config: config, - database: createDatabase(), - catalogClient, - reader: mockUrlReader, - taskBroker, - }); - app = express().use(router); - - const response = await request(app) - .post('/v2/tasks') - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }); - - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - "Scaffolding task for 'template:default/create-react-app-template'", - ); - - expect(response.status).toEqual(201); - }); - - it('emits auditlog when enabled with identity', async () => { - const config = new ConfigReader({ - scaffolder: { - auditlog: { - enabled: true, - }, - }, - }); - - const logger = getVoidLogger(); - const loggerSpy = jest.spyOn(logger, 'info'); - - const router = await createRouter({ - logger: logger, - config: config, - database: createDatabase(), - catalogClient, - reader: mockUrlReader, - taskBroker, - }); - app = express().use(router); - - const mockToken = - 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; - - const response = await request(app) - .post('/v2/tasks') - .set('Authorization', `Bearer ${mockToken}`) - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }); - - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - "Scaffolding task for 'template:default/create-react-app-template' created by guest", - ); - - expect(response.status).toEqual(201); - }); - - it('emits auditlog when enabled with identity and annotations', async () => { - const config = new ConfigReader({ - scaffolder: { - auditlog: { - enabled: true, - identityAnnotations: ['google.com/email', 'example.com/uuid'], - }, - }, - }); - - const logger = getVoidLogger(); - const loggerSpy = jest.spyOn(logger, 'info'); - - const router = await createRouter({ - logger: logger, - config: config, - database: createDatabase(), - catalogClient, - reader: mockUrlReader, - taskBroker, - }); - app = express().use(router); - - const mockToken = - 'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob'; - - const response = await request(app) - .post('/v2/tasks') - .set('Authorization', `Bearer ${mockToken}`) - .send({ - templateRef: stringifyEntityRef({ - kind: 'template', - name: 'create-react-app-template', - }), - values: { - required: 'required-value', - }, - }); - - expect(loggerSpy).toHaveBeenCalledTimes(1); - expect(loggerSpy).toHaveBeenCalledWith( - 'Scaffolding task for \'template:default/create-react-app-template\' created by guest [{"google.com/email":"bobby@tables.com"},{"example.com/uuid":"bc48e920-f1c9-4094-bb5c-99d120e87ab8"}]', - ); - - expect(response.status).toEqual(201); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 7d2c1848ab..b682ebdf25 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -193,33 +193,11 @@ export async function createRouter( ? await catalogClient.getEntityByRef(userEntityRef, { token }) : undefined; - const auditlogEnabled = config.getOptionalBoolean( - 'scaffolder.auditlog.enabled', - ); - - if (auditlogEnabled) { - const identityAnnotations = config.getOptionalStringArray( - 'scaffolder.auditlog.identityAnnotations', - ); - - const logMe: { [key: string]: string }[] = []; - - identityAnnotations?.forEach((prop: string) => - logMe.push({ - [prop]: userEntity?.metadata.annotations?.[prop] as string, - }), - ); - - let userLog = ''; - if (userEntity) { - userLog = ` created by ${userEntity.metadata.name}`; - if (logMe.length > 0) { - userLog += ` ${JSON.stringify(logMe)}`; - } - } - - logger.info(`Scaffolding task for '${templateRef}'${userLog}`); + let auditLog = `Scaffolding task for ${templateRef}`; + if (userEntityRef) { + auditLog += ` created by ${userEntityRef}`; } + logger.info(auditLog); const values = req.body.values; From 8b7f86dcc1f55f3955c9fbb92d22396eca27db90 Mon Sep 17 00:00:00 2001 From: Marco Crivellaro Date: Fri, 15 Jul 2022 16:01:44 +0100 Subject: [PATCH 6/6] chore: update changeset Signed-off-by: Marco Crivellaro --- .changeset/tame-gorillas-look.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tame-gorillas-look.md b/.changeset/tame-gorillas-look.md index a976af2968..8a7e230ce6 100644 --- a/.changeset/tame-gorillas-look.md +++ b/.changeset/tame-gorillas-look.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': minor --- -Introduced new option to emit an audit log message when a new scaffolder task is created +Introduced audit log message when a new scaffolder task is created