diff --git a/.changeset/tame-gorillas-look.md b/.changeset/tame-gorillas-look.md new file mode 100644 index 0000000000..8a7e230ce6 --- /dev/null +++ b/.changeset/tame-gorillas-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Introduced audit log message when a new scaffolder task is created diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index aea33f1158..f545e57f85 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( @@ -74,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; @@ -133,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, @@ -288,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', () => { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 2abb730250..b682ebdf25 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,12 @@ export async function createRouter( ? await catalogClient.getEntityByRef(userEntityRef, { token }) : undefined; + let auditLog = `Scaffolding task for ${templateRef}`; + if (userEntityRef) { + auditLog += ` created by ${userEntityRef}`; + } + logger.info(auditLog); + const values = req.body.values; const template = await findTemplate({