From 8db48b968c3d1404776ee2d3c5a2a69599c26af7 Mon Sep 17 00:00:00 2001 From: Shyam Saraswati Date: Thu, 5 Aug 2021 16:46:37 +1000 Subject: [PATCH 1/2] Add auth token to SonarQubeClient via IdentityApi Signed-off-by: Shyam Saraswati --- .changeset/little-tools-melt.md | 5 ++ .../sonarqube/src/api/SonarQubeClient.test.ts | 89 ++++++++++++++++++- plugins/sonarqube/src/api/SonarQubeClient.ts | 14 ++- plugins/sonarqube/src/plugin.ts | 10 ++- 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 .changeset/little-tools-melt.md diff --git a/.changeset/little-tools-melt.md b/.changeset/little-tools-melt.md new file mode 100644 index 0000000000..5e55d27b7a --- /dev/null +++ b/.changeset/little-tools-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sonarqube': minor +--- + +Use IdentityApi to provide Auth Token for SonarQubeClient Api calls diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index 816cf19b5e..e308723289 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -20,9 +20,39 @@ import { setupServer } from 'msw/node'; import { FindingSummary, SonarQubeClient } from './index'; import { ComponentWrapper, MeasuresWrapper } from './types'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; const server = setupServer(); +const identityApiAuthenticated: IdentityApi = { + getUserId() { + return 'jane-fonda'; + }, + getProfile() { + return { email: 'jane-fonda@spotify.com' }; + }, + async getIdToken() { + return Promise.resolve('fake-id-token'); + }, + async signOut() { + return Promise.resolve(); + }, +}; +const identityApiGuest: IdentityApi = { + getUserId() { + return 'guest'; + }, + getProfile() { + return {}; + }, + async getIdToken() { + return Promise.resolve(undefined); + }, + async signOut() { + return Promise.resolve(); + }, +}; + describe('SonarQubeClient', () => { msw.setupDefaultHandlers(server); @@ -161,7 +191,10 @@ describe('SonarQubeClient', () => { it('should report finding summary', async () => { setupHandlers(); - const client = new SonarQubeClient({ discoveryApi }); + const client = new SonarQubeClient({ + discoveryApi, + identityApi: identityApiAuthenticated, + }); const summary = await client.getFindingSummary('our:service'); expect(summary).toEqual( @@ -197,6 +230,7 @@ describe('SonarQubeClient', () => { const client = new SonarQubeClient({ discoveryApi, baseUrl: 'http://a.instance.local', + identityApi: identityApiAuthenticated, }); const summary = await client.getFindingSummary('our:service'); @@ -234,6 +268,7 @@ describe('SonarQubeClient', () => { const client = new SonarQubeClient({ discoveryApi, baseUrl: 'http://a.instance.local', + identityApi: identityApiAuthenticated, }); const summary = await client.getFindingSummary('our:service'); @@ -255,4 +290,56 @@ describe('SonarQubeClient', () => { 'http://a.instance.local/component_measures?id=our%3Aservice&metric=coverage&resolved=false&view=list', ); }); + + it('should add identity token for logged in users', async () => { + setupHandlers(); + server.use( + rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe('component=our%3Aservice'); + expect(req.headers.get('Authorization')).toBe('Bearer fake-id-token'); + return res( + ctx.json({ + component: { + analysisDate: '2020-01-01T00:00:00Z', + }, + } as ComponentWrapper), + ); + }), + ); + + const client = new SonarQubeClient({ + discoveryApi, + baseUrl: 'http://a.instance.local', + identityApi: identityApiAuthenticated, + }); + const summary = await client.getFindingSummary('our:service'); + + expect(summary?.lastAnalysis).toBe('2020-01-01T00:00:00Z'); + }); + + it('should omit identity token for guest users', async () => { + setupHandlers(); + server.use( + rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe('component=our%3Aservice'); + expect(req.headers.get('Authorization')).toBeUndefined(); + return res( + ctx.json({ + component: { + analysisDate: '2020-01-01T00:00:00Z', + }, + } as ComponentWrapper), + ); + }), + ); + + const client = new SonarQubeClient({ + discoveryApi, + baseUrl: 'http://a.instance.local', + identityApi: identityApiGuest, + }); + const summary = await client.getFindingSummary('our:service'); + + expect(summary?.lastAnalysis).toBe('2020-01-01T00:00:00Z'); + }); }); diff --git a/plugins/sonarqube/src/api/SonarQubeClient.ts b/plugins/sonarqube/src/api/SonarQubeClient.ts index f7f1cac92d..d2283693ea 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.ts @@ -17,20 +17,24 @@ import fetch from 'cross-fetch'; import { FindingSummary, Metrics, SonarQubeApi } from './SonarQubeApi'; import { ComponentWrapper, MeasuresWrapper } from './types'; -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; export class SonarQubeClient implements SonarQubeApi { discoveryApi: DiscoveryApi; baseUrl: string; + identityApi: IdentityApi; constructor({ discoveryApi, + identityApi, baseUrl = 'https://sonarcloud.io/', }: { discoveryApi: DiscoveryApi; + identityApi: IdentityApi; baseUrl?: string; }) { this.discoveryApi = discoveryApi; + this.identityApi = identityApi; this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; } @@ -38,9 +42,17 @@ export class SonarQubeClient implements SonarQubeApi { path: string, query: { [key in string]: any }, ): Promise { + const idToken = await this.identityApi.getIdToken(); + const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`; const response = await fetch( `${apiUrl}/${path}?${new URLSearchParams(query).toString()}`, + { + headers: { + 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }, ); if (response.status === 200) { return (await response.json()) as T; diff --git a/plugins/sonarqube/src/plugin.ts b/plugins/sonarqube/src/plugin.ts index d542d40b17..d8e1b7f89a 100644 --- a/plugins/sonarqube/src/plugin.ts +++ b/plugins/sonarqube/src/plugin.ts @@ -21,6 +21,7 @@ import { createComponentExtension, createPlugin, discoveryApiRef, + identityApiRef, } from '@backstage/core-plugin-api'; export const sonarQubePlugin = createPlugin({ @@ -28,11 +29,16 @@ export const sonarQubePlugin = createPlugin({ apis: [ createApiFactory({ api: sonarQubeApiRef, - deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, - factory: ({ configApi, discoveryApi }) => + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, discoveryApi, identityApi }) => new SonarQubeClient({ discoveryApi, baseUrl: configApi.getOptionalString('sonarQube.baseUrl'), + identityApi, }), }), ], From 1099a77ea390bf4dc36b90e1536fc40fb72ebd7d Mon Sep 17 00:00:00 2001 From: Shyam Saraswati Date: Fri, 6 Aug 2021 07:52:56 +1000 Subject: [PATCH 2/2] Fix SonarQubeClient test checking for Authentication header Signed-off-by: Shyam Saraswati --- plugins/sonarqube/src/api/SonarQubeClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index e308723289..dbc65ee59e 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -322,7 +322,7 @@ describe('SonarQubeClient', () => { server.use( rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { expect(req.url.searchParams.toString()).toBe('component=our%3Aservice'); - expect(req.headers.get('Authorization')).toBeUndefined(); + expect(req.headers.has('Authorization')).toBeFalsy(); return res( ctx.json({ component: {