tduniec / backstage-timesaver-plugin

This plugin provides an implementation of charts and statistics related to your time savings that are coming from usage of your templates. Plugins is built from frontend and backend part. This part of plugin `frontend` is responsible of providing views with charts describing data collected from `backend` part of plugin.
Apache License 2.0
15 stars 5 forks source link

Time saver unable to communicate with scaffolder api using service to service auth #21

Closed alesgurd closed 2 months ago

alesgurd commented 4 months ago

We've adopted the timesaver plugin before updating our backstage instance and the backstage update brought a few issues with it. Where the backend is unable to communicate with the scaffolder api when service to service auth is enabled. Also, the frontend plugin is unable to communicate with the backend because the user credentials are missing on the request.

I think a way to approach this would be to read the configuration from the backend side to retrieve a static token to use against the scaffolder api, and moving away from the fetchWithCredentials implementation to the @backstage/core-plugin-api fetchApi which already injects the user credentials to the request.

alesgurd commented 4 months ago

I've added a pull request #22, however the implementation would deprecate the previous service to service auth implementation where you'd get the token from backend.auth.keys on the app-config.yaml. Any feedback is appreciated, we had to run this fork on our instances because the time-saver plugin was appreciated by the team managers almost immediately.

Also, I wasn't able to make a decision on the version bumps to be performed, this could be a major update because even though the change is quite small, it would implicate deprecating the previous implementation. Please, provide feedback if possible.

tduniec commented 4 months ago

Hey @alesgurd Thank you for your contribution. I will take a look on it soon. I need to think on backward compatibility.

rmartine-ias commented 2 months ago

If I'm reading the backstage docs correctly, service-to-service auth doesn't need a token configured:

Backstage plugins that use the new backend system and handle credentials using the auth and httpAuth service APIs are secure by default, without requiring any configuration. They generate self-signed tokens automatically for making requests to other Backstage backend plugins, and the receivers use the caller's public key set endpoint to be able to perform verification.

The existing implementation was giving us 401 errors locally, so I hacked in support for the recommended way:

Patch file for compiled time saver plugin ```diff diff --git a/node_modules/@tduniec/backstage-plugin-time-saver-backend/dist/index.cjs.js b/node_modules/@tduniec/backstage-plugin-time-saver-backend/dist/index.cjs.js index a4d26f1..20801e6 100644 --- a/node_modules/@tduniec/backstage-plugin-time-saver-backend/dist/index.cjs.js +++ b/node_modules/@tduniec/backstage-plugin-time-saver-backend/dist/index.cjs.js @@ -338,9 +338,10 @@ class DatabaseOperations { } class ScaffolderClient { - constructor(logger, config) { + constructor(logger, config, auth) { this.logger = logger; this.config = config; + this.auth = auth; } async fetchTemplatesFromScaffolder() { let backendUrl = this.config.getOptionalString("ts.backendUrl") ?? "http://127.0.0.1:7007"; @@ -374,6 +375,11 @@ class ScaffolderClient { return templateTaskList; } async generateBackendToken(config, name) { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'scaffolder', // e.g. 'catalog' + }); + return token; let key = ""; let decodedBytes = ""; const keyConfig = config.getOptional("backend.auth.externalAccess"); @@ -406,15 +412,16 @@ class ScaffolderClient { } class TimeSaverHandler { - constructor(logger, config, knex) { + constructor(logger, config, knex, auth) { this.logger = logger; this.config = config; this.db = new DatabaseOperations(knex, logger); + this.auth = auth; } db; tsTableName = "ts_template_time_savings"; async fetchTemplates() { - const scaffolderClient = new ScaffolderClient(this.logger, this.config); + const scaffolderClient = new ScaffolderClient(this.logger, this.config, this.auth); this.logger.info(`START - Collecting Time Savings data from templates...}`); let templateTaskList = []; try { @@ -423,6 +430,7 @@ class TimeSaverHandler { return "FAIL"; } await this.db.truncate(this.tsTableName); + this.logger.info(`Template task list: ${JSON.stringify(templateTaskList)}`); templateTaskList = templateTaskList.filter( (single) => single.status === "completed" ); @@ -494,11 +502,12 @@ class ScaffolderDatabaseOperations { } class TsApi { - constructor(logger, config, knex, scaffoldKx) { + constructor(logger, config, knex, scaffoldKx, auth) { this.logger = logger; this.config = config; this.db = new DatabaseOperations(knex, logger); this.scaffolderDb = new ScaffolderDatabaseOperations(scaffoldKx, logger); + this.auth = auth; } db; scaffolderDb; @@ -594,7 +603,8 @@ class TsApi { const tsConfig = JSON.parse(String(tsConfigObj)); const taskTemplateList = await new ScaffolderClient( this.logger, - this.config + this.config, + this.auth ).fetchTemplatesFromScaffolder(); for (let i = 0; i < taskTemplateList.length; i++) { const singleTemplate = taskTemplateList[i]; @@ -762,13 +772,14 @@ class ScaffolderDb { } class TsScheduler { - constructor(logger, config, knex) { + constructor(logger, config, knex, auth) { this.logger = logger; this.config = config; this.knex = knex; + this.auth = auth; } async schedule(taskRunner) { - const tsHandler = new TimeSaverHandler(this.logger, this.config, this.knex); + const tsHandler = new TimeSaverHandler(this.logger, this.config, this.knex, this.auth); await taskRunner.run({ id: uuid__namespace.v4(), fn: async () => { @@ -869,7 +880,7 @@ const TS_PLUGIN_DEFAULT_SCHEDULE = { minutes: 30 }, initialDelay: { - seconds: 60 + seconds: 0 } }; class PluginInitializer { @@ -881,20 +892,23 @@ class PluginInitializer { apiHandler; tsScheduler; router; - constructor(router, logger, config, database, scheduler) { + auth; + constructor(router, logger, config, database, scheduler, auth) { this.router = router; this.logger = logger; this.config = config; this.database = database; this.scheduler = scheduler; + this.auth = auth; } - static async builder(router, logger, config, database, scheduler) { + static async builder(router, logger, config, database, scheduler, auth) { const instance = new PluginInitializer( router, logger, config, database, - scheduler + scheduler, + auth ); await instance.initialize(); return instance; @@ -912,9 +926,9 @@ class PluginInitializer { this.logger.error("Could not get scaffolder database info"); throw new Error("Could not get scaffolder database info"); } - this.tsHandler = new TimeSaverHandler(this.logger, this.config, kx); - this.apiHandler = new TsApi(this.logger, this.config, kx, scaffolderDbKx); - this.tsScheduler = new TsScheduler(this.logger, this.config, kx); + this.tsHandler = new TimeSaverHandler(this.logger, this.config, kx, this.auth); + this.apiHandler = new TsApi(this.logger, this.config, kx, scaffolderDbKx, this.auth); + this.tsScheduler = new TsScheduler(this.logger, this.config, kx, this.auth); const taskRunner = this.scheduler.createScheduledTaskRunner( TS_PLUGIN_DEFAULT_SCHEDULE ); @@ -980,9 +994,10 @@ const timeSaverPlugin = backendPluginApi.createBackendPlugin({ config: backendPluginApi.coreServices.rootConfig, scheduler: backendPluginApi.coreServices.scheduler, database: backendPluginApi.coreServices.database, - http: backendPluginApi.coreServices.httpRouter + http: backendPluginApi.coreServices.httpRouter, + auth: backendPluginApi.coreServices.auth }, - async init({ config, logger, scheduler, database, http }) { + async init({ config, logger, scheduler, database, http, auth }) { const baseRouter = registerRouter(); const winstonLogger = backendCommon.loggerToWinstonLogger(logger); const plugin = await PluginInitializer.builder( @@ -990,7 +1005,8 @@ const timeSaverPlugin = backendPluginApi.createBackendPlugin({ winstonLogger, config, database, - scheduler + scheduler, + auth ); const router = plugin.timeSaverRouter; http.use(router); ```

This fixes the problem for us. I don't have time to make a PR right now, sorry!

ionSurf commented 2 months ago

Hello everyone,

Indeed, this solves the S2S communication using the new Backstage auth service. I was working around fixing other things ATM but I started bumping into this too, implementing the same fix momentarily before a PR.

I appreciate your input Riley and will be making these changes and a correspond PR asap.

My best,

Javier

On Jul 2, 2024, at 4:20 PM, Riley Martine @.***> wrote:

If I'm reading the backstage docshttps://backstage.io/docs/auth/service-to-service-auth#standard-plugin-to-plugin-auth correctly, service-to-service auth doesn't need a token configured:

Backstage plugins that use the new backend system and handle credentials using the auth and httpAuth service APIs are secure by default, without requiring any configuration. They generate self-signed tokens automatically for making requests to other Backstage backend plugins, and the receivers use the caller's public key set endpoint to be able to perform verification.

The existing implementation was giving us 401 errors locally, so I hacked in support for the recommended way:

Patch file for compiled time saver plugin

diff --git @./backstage-plugin-time-saver-backend/dist/index.cjs.js @./backstage-plugin-time-saver-backend/dist/index.cjs.js index a4d26f1..20801e6 100644 --- @./backstage-plugin-time-saver-backend/dist/index.cjs.js +++ @./backstage-plugin-time-saver-backend/dist/index.cjs.js @@ -338,9 +338,10 @@ class DatabaseOperations { }

class ScaffolderClient {

This fixes the problem for us. I don't have time to make a PR right now, sorry!

— Reply to this email directly, view it on GitHubhttps://github.com/tduniec/backstage-timesaver-plugin/issues/21#issuecomment-2204325777, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAOSYQIGKG6CWSLZKHTJ3D3ZKMDSTAVCNFSM6AAAAABHUMWBYOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEMBUGMZDKNZXG4. You are receiving this because you are subscribed to this thread.Message ID: @.***>

tduniec commented 2 months ago

Provided new version 2.0.0 with the new auth service.

Thanks @ionSurf fro driving this change! Thanks all for your input!