From 2fbf5300c5b925267d2acf212d2d8330ecebcc40 Mon Sep 17 00:00:00 2001 From: Rbillon59 Date: Thu, 27 Apr 2023 11:45:47 +0200 Subject: [PATCH] chore(badges): improve error handling, improve visibility splitting if else block to functions, move db test file beside the tested file Signed-off-by: Rbillon59 --- .../{tests => database}/badgeStore.test.ts | 2 +- plugins/badges-backend/src/service/router.ts | 450 +++++++++--------- 2 files changed, 231 insertions(+), 221 deletions(-) rename plugins/badges-backend/src/{tests => database}/badgeStore.test.ts (97%) diff --git a/plugins/badges-backend/src/tests/badgeStore.test.ts b/plugins/badges-backend/src/database/badgeStore.test.ts similarity index 97% rename from plugins/badges-backend/src/tests/badgeStore.test.ts rename to plugins/badges-backend/src/database/badgeStore.test.ts index 2fd18e4233..60a885daa4 100644 --- a/plugins/badges-backend/src/tests/badgeStore.test.ts +++ b/plugins/badges-backend/src/database/badgeStore.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DatabaseBadgesStore } from '../database/badgesStore'; +import { DatabaseBadgesStore } from './badgesStore'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Knex } from 'knex'; import { Entity } from '@backstage/catalog-model'; diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index d1cb0cff36..92bbb1aeae 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -24,7 +24,7 @@ import { } from '@backstage/backend-common'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { NotFoundError } from '@backstage/errors'; +import { AuthenticationError, NotFoundError } from '@backstage/errors'; import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder'; import { BadgeContext, BadgeFactories } from '../types'; import { isNil } from 'lodash'; @@ -61,97 +61,252 @@ export async function createRouter( const baseUrl = await discovery.getExternalBaseUrl('badges'); if (config.getOptionalBoolean('app.badges.obfuscate')) { - logger.info('Badges obfuscation is enabled'); + return obfuscatedRoute( + router, + catalog, + badgeBuilder, + tokenManager, + logger, + options, + config, + identity, + baseUrl, + ); + } + return nonObfuscatedRoute( + router, + catalog, + badgeBuilder, + tokenManager, + config, + baseUrl, + ); +} - const store = options.badgeStore - ? options.badgeStore - : await DatabaseBadgesStore.create({ - database: await DatabaseManager.fromConfig(config).forPlugin( - 'badges', - ), - }); +async function obfuscatedRoute( + router: express.Router, + catalog: CatalogApi, + badgeBuilder: BadgeBuilder, + tokenManager: TokenManager, + logger: Logger, + options: RouterOptions, + config: Config, + identity: IdentityApi, + baseUrl: string, +) { + logger.info('Badges obfuscation is enabled'); - router.get('/entity/:entityUuid/badge-specs', async (req, res) => { - const { entityUuid } = req.params; + const store = options.badgeStore + ? options.badgeStore + : await DatabaseBadgesStore.create({ + database: await DatabaseManager.fromConfig(config).forPlugin('badges'), + }); - // Retrieve the badge info from the database - const badgeInfos = await store.getBadgeFromUuid(entityUuid); + router.get('/entity/:entityUuid/badge-specs', async (req, res) => { + const { entityUuid } = req.params; - if (isNil(badgeInfos)) { - throw new NotFoundError( - `No badge found for entity uuid "${entityUuid}"`, - ); - } + // Retrieve the badge info from the database + const badgeInfos = await store.getBadgeFromUuid(entityUuid); - // If a mapping is found, map name, namespace and kind - const name = badgeInfos.name; - const namespace = badgeInfos.namespace; - const kind = badgeInfos.kind; - const token = await tokenManager.getToken(); + if (isNil(badgeInfos)) { + throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`); + } - // Query the catalog with the name, namespace, kind to get the entity informations - const entity = await catalog.getEntityByRef( - { - namespace, - kind, - name, - }, - token, + // If a mapping is found, map name, namespace and kind + const name = badgeInfos.name; + const namespace = badgeInfos.namespace; + const kind = badgeInfos.kind; + const token = await tokenManager.getToken(); + + // Query the catalog with the name, namespace, kind to get the entity informations + const entity = await catalog.getEntityByRef( + { + namespace, + kind, + name, + }, + token, + ); + if (isNil(entity)) { + throw new NotFoundError( + `No ${kind} entity in ${namespace} named "${name}"`, ); - if (isNil(entity)) { - throw new NotFoundError( - `No ${kind} entity in ${namespace} named "${name}"`, - ); + } + + // Create the badge specs + const specs = []; + for (const badgeInfo of await badgeBuilder.getBadges()) { + const context: BadgeContext = { + badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeInfo.id}`, + config: config, + entity, + }; + + const badge = await badgeBuilder.createBadgeJson({ + badgeInfo, + context, + }); + specs.push(badge); + } + + res.status(200).json(specs); + }); + + router.get('/entity/:entityUuid/:badgeId', async (req, res) => { + const { entityUuid, badgeId } = req.params; + + // Retrieve the badge info from the database + const badgeInfo = await store.getBadgeFromUuid(entityUuid); + + if (isNil(badgeInfo)) { + throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`); + } + + // If a mapping is found, map name, namespace and kind + const name = badgeInfo.name; + const namespace = badgeInfo.namespace; + const kind = badgeInfo.kind; + const token = await tokenManager.getToken(); + const entity = await catalog.getEntityByRef( + { + namespace, + kind, + name, + }, + token, + ); + if (isNil(entity)) { + throw new NotFoundError( + `No ${kind} entity in ${namespace} named "${name}"`, + res.sendStatus(404), + ); + } + + let format = + req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml'; + if (req.query.format === 'json') { + format = 'application/json'; + } + + const badgeOptions = { + badgeInfo: { id: badgeId }, + context: { + badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeId}`, + config: config, + entity, + }, + }; + + let data: string; + if (format === 'application/json') { + data = JSON.stringify( + await badgeBuilder.createBadgeJson(badgeOptions), + null, + 2, + ); + } else { + data = await badgeBuilder.createBadgeSvg(badgeOptions); + } + + res.setHeader('Content-Type', format); + res.status(200).send(data); + }); + + router.get( + '/entity/:namespace/:kind/:name/obfuscated', + function authenticate(req, _res, next) { + const token = + getBearerTokenFromAuthorizationHeader(req.headers.authorization) || + (req.cookies?.token as string | undefined); + + if (!token) { + throw new AuthenticationError('Unauthorized'); } - // Create the badge specs - const specs = []; - for (const badgeInfo of await badgeBuilder.getBadges()) { - const context: BadgeContext = { - badgeUrl: await getBadgeObfuscatedUrl(entityUuid, badgeInfo.id), - config: config, - entity, - }; + try { + req.user = identity.getIdentity({ request: req }); + next(); + } catch (error) { + tokenManager.authenticate(token.toString()); + next(error); + } + }, + async (req, res) => { + const { namespace, kind, name } = req.params; + let storedEntityUuid: { uuid: string } | undefined = + await store.getUuidFromEntityMetadata(name, namespace, kind); - const badge = await badgeBuilder.createBadgeJson({ - badgeInfo, - context, - }); - specs.push(badge); + if (isNil(storedEntityUuid)) { + storedEntityUuid = await store.addBadge(name, namespace, kind); + + if (isNil(storedEntityUuid)) { + throw new NotFoundError( + `No uuid found for entity "${namespace}/${kind}/${name}"`, + ); + } } - res.status(200).json(specs); - }); + return res.status(200).json(storedEntityUuid); + }, + ); - router.get('/entity/:entityUuid/:badgeId', async (req, res) => { - const { entityUuid, badgeId } = req.params; + router.use(errorHandler()); - // Retrieve the badge info from the database - const badgeInfo = await store.getBadgeFromUuid(entityUuid); + return router; +} - if (isNil(badgeInfo)) { - throw new NotFoundError( - `No badge found for entity uuid "${entityUuid}"`, - ); - } +async function nonObfuscatedRoute( + router: express.Router, + catalog: CatalogApi, + badgeBuilder: BadgeBuilder, + tokenManager: TokenManager, + config: Config, + baseUrl: string, +) { + router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => { + const token = await tokenManager.getToken(); + const { namespace, kind, name } = req.params; + const entity = await catalog.getEntityByRef( + { namespace, kind, name }, + token, + ); + if (!entity) { + throw new NotFoundError( + `No ${kind} entity in ${namespace} named "${name}"`, + ); + } - // If a mapping is found, map name, namespace and kind - const name = badgeInfo.name; - const namespace = badgeInfo.namespace; - const kind = badgeInfo.kind; + const specs = []; + for (const badgeInfo of await badgeBuilder.getBadges()) { + const badgeId = badgeInfo.id; + const context: BadgeContext = { + badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`, + config: config, + entity, + }; + + const badge = await badgeBuilder.createBadgeJson({ + badgeInfo, + context, + }); + specs.push(badge); + } + + res.status(200).json(specs); + }); + + router.get( + '/entity/:namespace/:kind/:name/badge/:badgeId', + async (req, res) => { + const { namespace, kind, name, badgeId } = req.params; const token = await tokenManager.getToken(); const entity = await catalog.getEntityByRef( - { - namespace, - kind, - name, - }, + { namespace, kind, name }, token, ); - if (isNil(entity)) { + if (!entity) { throw new NotFoundError( `No ${kind} entity in ${namespace} named "${name}"`, - res.sendStatus(404), ); } @@ -161,11 +316,10 @@ export async function createRouter( format = 'application/json'; } - // Generate the badge URL for the different types of badgeId const badgeOptions = { badgeInfo: { id: badgeId }, context: { - badgeUrl: await getBadgeObfuscatedUrl(entityUuid, badgeId), + badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`, config: config, entity, }, @@ -184,154 +338,10 @@ export async function createRouter( res.setHeader('Content-Type', format); res.status(200).send(data); - }); + }, + ); - router.get( - '/entity/:namespace/:kind/:name/obfuscated', - function authenticate(req, res, next) { - const token = - getBearerTokenFromAuthorizationHeader(req.headers.authorization) || - (req.cookies?.token as string | undefined); + router.use(errorHandler()); - if (!token) { - res.status(401).send('Unauthorized'); - return; - } - - try { - req.user = identity.getIdentity({ request: req }); - next(); - } catch (error) { - tokenManager.authenticate(token.toString()); - next(error); - } - }, - async (req, res) => { - const { namespace, kind, name } = req.params; - let storedEntityUuid: { uuid: string } | undefined = - await store.getUuidFromEntityMetadata(name, namespace, kind); - - if (isNil(storedEntityUuid)) { - storedEntityUuid = await store.addBadge(name, namespace, kind); - - if (isNil(storedEntityUuid)) { - throw new NotFoundError( - `No uuid found for entity "${namespace}/${kind}/${name}"`, - ); - } - } - - return res.status(200).json(storedEntityUuid); - }, - ); - - router.use(errorHandler()); - - return router; - - // If the obfuscation is disabled, use the previously implemented routes - // eslint-disable-next-line no-else-return - } else { - router.get( - '/entity/:namespace/:kind/:name/badge-specs', - async (req, res) => { - const token = await tokenManager.getToken(); - const { namespace, kind, name } = req.params; - const entity = await catalog.getEntityByRef( - { namespace, kind, name }, - token, - ); - if (!entity) { - throw new NotFoundError( - `No ${kind} entity in ${namespace} named "${name}"`, - ); - } - - const specs = []; - for (const badgeInfo of await badgeBuilder.getBadges()) { - const context: BadgeContext = { - badgeUrl: await getBadgeUrl(namespace, kind, name, badgeInfo.id), - config: config, - entity, - }; - - const badge = await badgeBuilder.createBadgeJson({ - badgeInfo, - context, - }); - specs.push(badge); - } - - res.status(200).json(specs); - }, - ); - - router.get( - '/entity/:namespace/:kind/:name/badge/:badgeId', - async (req, res) => { - const { namespace, kind, name, badgeId } = req.params; - const token = await tokenManager.getToken(); - const entity = await catalog.getEntityByRef( - { namespace, kind, name }, - token, - ); - if (!entity) { - throw new NotFoundError( - `No ${kind} entity in ${namespace} named "${name}"`, - ); - } - - let format = - req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml'; - if (req.query.format === 'json') { - format = 'application/json'; - } - - const badgeOptions = { - badgeInfo: { id: badgeId }, - context: { - badgeUrl: await getBadgeUrl(namespace, kind, name, badgeId), - config: config, - entity, - }, - }; - - let data: string; - if (format === 'application/json') { - data = JSON.stringify( - await badgeBuilder.createBadgeJson(badgeOptions), - null, - 2, - ); - } else { - data = await badgeBuilder.createBadgeSvg(badgeOptions); - } - - res.setHeader('Content-Type', format); - res.status(200).send(data); - }, - ); - - router.use(errorHandler()); - - return router; - } - - // This function return the obfuscated badge url based on the namespace/kind/name triplet - async function getBadgeObfuscatedUrl( - uuid: string, - badgeId: string, - ): Promise { - return `${baseUrl}/entity/${uuid}/${badgeId}`; - } - - // This function return the badge url based on the namespace/kind/name triplet - async function getBadgeUrl( - namespace: string, - kind: string, - name: string, - badgeId: string, - ): Promise { - return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`; - } + return router; }