diff --git a/packages/backend/src/plugins/badges.ts b/packages/backend/src/plugins/badges.ts index 6b40d1e5fb..fa50731809 100644 --- a/packages/backend/src/plugins/badges.ts +++ b/packages/backend/src/plugins/badges.ts @@ -20,15 +20,10 @@ import { } from '@backstage/plugin-badges-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; -import { DatabaseBadgesStore } from '@backstage/plugin-badges-backend'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - const db = await DatabaseBadgesStore.create({ - database: env.database, - }); - return await createRouter({ config: env.config, discovery: env.discovery, @@ -36,6 +31,5 @@ export default async function createPlugin( tokenManager: env.tokenManager, logger: env.logger, identity: env.identity, - db: db, }); } diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md index 4fcef981f1..5e9b8dd3cc 100644 --- a/plugins/badges-backend/README.md +++ b/plugins/badges-backend/README.md @@ -32,10 +32,6 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - const db = await DatabaseBadgesStore.create({ - database: env.database, - }); - return await createRouter({ config: env.config, discovery: env.discovery, @@ -43,7 +39,6 @@ export default async function createPlugin( tokenManager: env.tokenManager, logger: env.logger, identity: env.identity, - db: db, }); } ``` @@ -134,23 +129,6 @@ app: > Note that you cannot use env vars to set the `obfuscate` value. It must be a boolean value and env vars are always strings. -Also you need to provide the [salt]() in the `app-config.yaml`: - -```yaml -custom: - badges-backend: - salt: # required - cacheTimeToLive: 60 # minutes (optional) -``` - -Any string can be used as a salt, but it's recommended to use a long random string to increase entropy. You can generate a random string using the following command: - -```bash -openssl rand -hex 32 -``` - -> Note: The salt is used to obfuscate the entity names, so if you change the salt, the entity names will be obfuscated differently and the already established badges (in Github repositories for examples) will need to be updated. - ## API The badges backend api exposes two main endpoints for entity badges. The @@ -169,16 +147,15 @@ The badges backend api exposes two main endpoints for entity badges. The ### If obfuscation is enabled (apps.badges.obfuscate: true) -- `/badges/entity/:namespace/:kind/:name/obfuscated` Get the obfuscated entity - hash from name, namespace, kind. +- `/badges/entity/:namespace/:kind/:name/obfuscated` Get the obfuscated entity url. -> Note that endpoint have a embedded authMiddleware to authenticate the user requesting this endpoint. It meant to be called from the frontend plugin. +> Note that endpoint have a embedded authMiddleware to authenticate the user requesting this endpoint. _It meant to be called from the frontend plugin._ -- `/badges/entity/:entityHash/:badgeId` Get the entity badge as an SVG image. If +- `/badges/entity/:entityUuid/:badgeId` Get the entity badge as an SVG image. If the `accept` request header prefers `application/json` the badge spec as JSON will be returned instead of the image. -- `/badge/entity/:entityHash/badge-specs` List all defined badges for a +- `/badge/entity/:entityUuid/badge-specs` List all defined badges for a particular entity, in json format. See [BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts) from the frontend plugin for a type declaration. diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 6f252cf27d..013c2c5ac7 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -7,7 +7,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -85,28 +84,15 @@ export type BadgeSpec = { // @public export interface BadgesStore { // (undocumented) - countAllBadges(): Promise; + addBadge( + name: string, + namespace: string, + kind: string, + ): Promise<{ + uuid: string; + }>; // (undocumented) - createAllBadges( - entities: GetEntitiesResponse, - salt: string | undefined, - ): Promise; - // (undocumented) - deleteObsoleteHashes( - entities: GetEntitiesResponse, - salt: string | undefined, - ): Promise; - // (undocumented) - getAllBadges(): Promise< - { - name: string; - namespace: string; - kind: string; - hash: string; - }[] - >; - // (undocumented) - getBadgeFromHash(hash: string): Promise< + getBadgeFromUuid(uuid: string): Promise< | { name: string; namespace: string; @@ -115,13 +101,13 @@ export interface BadgesStore { | undefined >; // (undocumented) - getHashFromEntityMetadata( + getUuidFromEntityMetadata( name: string, namespace: string, kind: string, ): Promise< | { - hash: string; + uuid: string; } | undefined >; @@ -156,12 +142,12 @@ export interface RouterOptions { // (undocumented) badgeFactories?: BadgeFactories; // (undocumented) + badgeStore?: BadgesStore; + // (undocumented) catalog?: CatalogApi; // (undocumented) config: Config; // (undocumented) - db: BadgesStore; - // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) identity: IdentityApi; diff --git a/plugins/badges-backend/migrations/20230404_init.js b/plugins/badges-backend/migrations/20230404_init.js index d9a395d09a..f44b211038 100644 --- a/plugins/badges-backend/migrations/20230404_init.js +++ b/plugins/badges-backend/migrations/20230404_init.js @@ -19,15 +19,9 @@ exports.up = async function up(knex) { table.string('kind').notNullable(); table.string('namespace').notNullable(); table.string('name').notNullable(); - table - .string('hash') - .unique() - .comment( - 'Hash is calculated from the SHA256 of badge kind, namespace, name, and applications salt', - ) - .notNullable(); - table.index(['hash'], 'badges_hash_index'); - table.primary(['hash']); + table.string('uuid').unique().notNullable(); + table.index(['uuid'], 'badges_uuid_index'); + table.primary(['kind', 'namespace', 'name']); }); }; diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 4b05ffb2e4..b7bdd7c44a 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -47,6 +47,7 @@ "knex": "^2.4.2", "lodash": "^4.17.21", "supertest": "^6.3.3", + "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/badges-backend/src/database/badgesStore.ts b/plugins/badges-backend/src/database/badgesStore.ts index fa7bfa3f40..62d546f025 100644 --- a/plugins/badges-backend/src/database/badgesStore.ts +++ b/plugins/badges-backend/src/database/badgesStore.ts @@ -19,39 +19,28 @@ import { resolvePackagePath, } from '@backstage/backend-common'; import { Knex } from 'knex'; -import crypto from 'crypto'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { v4 as uuidv4 } from 'uuid'; /** * internal * @public */ export interface BadgesStore { - createAllBadges( - entities: GetEntitiesResponse, - salt: string | undefined, - ): Promise; - - getBadgeFromHash( - hash: string, - ): Promise<{ name: string; namespace: string; kind: string } | undefined>; - - getHashFromEntityMetadata( + addBadge( name: string, namespace: string, kind: string, - ): Promise<{ hash: string } | undefined>; + ): Promise<{ uuid: string }>; - deleteObsoleteHashes( - entities: GetEntitiesResponse, - salt: string | undefined, - ): Promise; + getBadgeFromUuid( + uuid: string, + ): Promise<{ name: string; namespace: string; kind: string } | undefined>; - countAllBadges(): Promise; - - getAllBadges(): Promise< - { name: string; namespace: string; kind: string; hash: string }[] - >; + getUuidFromEntityMetadata( + name: string, + namespace: string, + kind: string, + ): Promise<{ uuid: string } | undefined>; } const migrationsDir = resolvePackagePath( @@ -84,99 +73,47 @@ export class DatabaseBadgesStore implements BadgesStore { return new DatabaseBadgesStore(client); } - async createAllBadges( - entities: GetEntitiesResponse, - salt: string, - ): Promise { - for (const entity of entities.items) { - const name = entity.metadata.name.toLocaleLowerCase(); - const namespace = - entity.metadata.namespace?.toLocaleLowerCase() ?? 'default'; - const kind = entity.kind.toLocaleLowerCase(); - const entityHash = crypto - .createHash('sha256') - .update(`${kind}:${namespace}:${name}:${salt}`) - .digest('hex'); - - await this.db('badges') - .insert({ - hash: entityHash, - namespace: namespace, - name: name, - kind: kind, - }) - .onConflict() - .ignore(); - } - } - - async getAllBadges(): Promise< - { name: string; namespace: string; kind: string; hash: string }[] - > { - const result = await this.db('badges').select('*').orderBy('name', 'asc'); - return result; - } - - async getBadgeFromHash( - hash: string, + async getBadgeFromUuid( + uuid: string, ): Promise<{ name: string; namespace: string; kind: string } | undefined> { const result = await this.db('badges') .select('namespace', 'name', 'kind') - .where({ hash: hash }) + .where({ uuid: uuid }) .first(); return result; } - async getHashFromEntityMetadata( + async getUuidFromEntityMetadata( name: string, namespace: string, kind: string, - ): Promise<{ hash: string } | undefined> { + ): Promise<{ uuid: string } | undefined> { const result = await this.db('badges') - .select('hash') + .select('uuid') .where({ name: name, namespace: namespace, kind: kind }) .first(); return result; } - async deleteObsoleteHashes( - entities: GetEntitiesResponse, - salt: string, - ): Promise { - const entityInCatalog: string[] = []; - const entityInBadgeDatabase: string[] = []; - let entityToDelete: string[] = []; + async addBadge( + name: string, + namespace: string, + kind: string, + ): Promise<{ uuid: string }> { + const uuid = uuidv4(); - for (const entity of entities.items) { - const name = entity.metadata.name.toLowerCase(); - const namespace = entity.metadata.namespace?.toLowerCase() ?? 'default'; - const kind = entity.kind.toLowerCase(); - const entityHash = crypto - .createHash('sha256') - .update(`${kind}:${namespace}:${name}:${salt}`) - .digest('hex'); + await this.db('badges') + .insert({ + uuid: uuid, + name: name, + namespace: namespace, + kind: kind, + }) + .onConflict(['name', 'namespace', 'kind']) + .ignore(); - entityInCatalog.push(entityHash); - } - - for (const entity of await this.getAllBadges()) { - entityInBadgeDatabase.push(entity.hash); - } - - entityToDelete = entityInBadgeDatabase.filter( - entity => !entityInCatalog.includes(entity), - ); - - for (const entity of entityToDelete) { - await this.db('badges').where({ hash: entity }).del(); - } - } - - async countAllBadges(): Promise { - const result = await this.db('badges').countDistinct('hash as count'); - const count: number = +result[0].count; - return count; + return { uuid }; } } diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index db568eb0d0..d1cb0cff36 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -17,6 +17,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { + DatabaseManager, errorHandler, PluginEndpointDiscovery, TokenManager, @@ -27,11 +28,10 @@ import { NotFoundError } from '@backstage/errors'; import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder'; import { BadgeContext, BadgeFactories } from '../types'; import { isNil } from 'lodash'; -import crypto from 'crypto'; import { Logger } from 'winston'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; -import type { BadgesStore } from '../database/badgesStore'; +import { BadgesStore, DatabaseBadgesStore } from '../database/badgesStore'; /** @public */ export interface RouterOptions { @@ -43,7 +43,7 @@ export interface RouterOptions { tokenManager: TokenManager; logger: Logger; identity: IdentityApi; - db: BadgesStore; + badgeStore?: BadgesStore; } /** @public */ @@ -57,39 +57,29 @@ export async function createRouter( new DefaultBadgeBuilder(options.badgeFactories || {}); const router = Router(); - const { db, config, logger, tokenManager, discovery, identity } = options; + const { config, logger, tokenManager, discovery, identity } = options; const baseUrl = await discovery.getExternalBaseUrl('badges'); - const salt = config.getOptionalString('custom.badges-backend.salt'); - const cacheTimeToLive = - config.getOptionalNumber('badgeDatabaseRefreshCacheTimeToLive') ?? 3600; - let lastDatabaseRefresh = 0; - - // Check if the users have enabled the obfuscation of the entity name if (config.getOptionalBoolean('app.badges.obfuscate')) { logger.info('Badges obfuscation is enabled'); - if (isNil(salt)) { - throw new Error( - 'Badges obfuscation is enabled but no salt has been provided', - ); - } + const store = options.badgeStore + ? options.badgeStore + : await DatabaseBadgesStore.create({ + database: await DatabaseManager.fromConfig(config).forPlugin( + 'badges', + ), + }); - // Use the generated hash instead of the triplet namespace/kind/name - router.get('/entity/:entityHash/badge-specs', async (req, res) => { - const { entityHash } = req.params; - - // Chech if the database needs to be refreshed - if (await isBadgeDatabaseRefreshNeeded(lastDatabaseRefresh)) { - lastDatabaseRefresh = await refreshBadgeDatabase(); - } + router.get('/entity/:entityUuid/badge-specs', async (req, res) => { + const { entityUuid } = req.params; // Retrieve the badge info from the database - const badgeInfos = await db.getBadgeFromHash(entityHash); + const badgeInfos = await store.getBadgeFromUuid(entityUuid); if (isNil(badgeInfos)) { throw new NotFoundError( - `No badge found for entity hash "${entityHash}"`, + `No badge found for entity uuid "${entityUuid}"`, ); } @@ -118,7 +108,7 @@ export async function createRouter( const specs = []; for (const badgeInfo of await badgeBuilder.getBadges()) { const context: BadgeContext = { - badgeUrl: await getBadgeObfuscatedUrl(entityHash, badgeInfo.id), + badgeUrl: await getBadgeObfuscatedUrl(entityUuid, badgeInfo.id), config: config, entity, }; @@ -133,20 +123,15 @@ export async function createRouter( res.status(200).json(specs); }); - // Use the generated hash instead of the triplet namespace/kind/name - router.get('/entity/:entityHash/:badgeId', async (req, res) => { - if (await isBadgeDatabaseRefreshNeeded(lastDatabaseRefresh)) { - lastDatabaseRefresh = await refreshBadgeDatabase(); - } - - const { entityHash, badgeId } = req.params; + router.get('/entity/:entityUuid/:badgeId', async (req, res) => { + const { entityUuid, badgeId } = req.params; // Retrieve the badge info from the database - const badgeInfo = await db.getBadgeFromHash(entityHash); + const badgeInfo = await store.getBadgeFromUuid(entityUuid); if (isNil(badgeInfo)) { throw new NotFoundError( - `No badge found for entity hash "${entityHash}"`, + `No badge found for entity uuid "${entityUuid}"`, ); } @@ -180,7 +165,7 @@ export async function createRouter( const badgeOptions = { badgeInfo: { id: badgeId }, context: { - badgeUrl: await getBadgeObfuscatedUrl(entityHash, badgeId), + badgeUrl: await getBadgeObfuscatedUrl(entityUuid, badgeId), config: config, entity, }, @@ -201,7 +186,6 @@ export async function createRouter( res.status(200).send(data); }); - // Generate the hash for queried the namespace/kind/name triplet router.get( '/entity/:namespace/:kind/:name/obfuscated', function authenticate(req, res, next) { @@ -223,34 +207,21 @@ export async function createRouter( } }, async (req, res) => { - if (await isBadgeDatabaseRefreshNeeded(lastDatabaseRefresh)) { - lastDatabaseRefresh = await refreshBadgeDatabase(); - } - const { namespace, kind, name } = req.params; - const storedEntityHash: { hash: string } | undefined = - await db.getHashFromEntityMetadata(name, namespace, kind); + let storedEntityUuid: { uuid: string } | undefined = + await store.getUuidFromEntityMetadata(name, namespace, kind); - if (isNil(storedEntityHash)) { - throw new NotFoundError( - `No hash found for entity "${namespace}/${kind}/${name}"`, - ); + if (isNil(storedEntityUuid)) { + storedEntityUuid = await store.addBadge(name, namespace, kind); + + if (isNil(storedEntityUuid)) { + throw new NotFoundError( + `No uuid found for entity "${namespace}/${kind}/${name}"`, + ); + } } - // Compare a live calculated hash with the stored one to avoid returning an obsolete entityHash (in case of a salt change) - - const liveHash = crypto - .createHash('sha256') - .update(`${kind}:${namespace}:${name}:${salt}`) - .digest('hex'); - - if (storedEntityHash.hash !== liveHash) { - throw new NotFoundError( - "Stored Hash and live Hash don't match, did you change the salt? Try to refresh the badge database table and try again", - ); - } - - return res.status(200).json(storedEntityHash); + return res.status(200).json(storedEntityUuid); }, ); @@ -348,10 +319,10 @@ export async function createRouter( // This function return the obfuscated badge url based on the namespace/kind/name triplet async function getBadgeObfuscatedUrl( - hash: string, + uuid: string, badgeId: string, ): Promise { - return `${baseUrl}/entity/${hash}/${badgeId}`; + return `${baseUrl}/entity/${uuid}/${badgeId}`; } // This function return the badge url based on the namespace/kind/name triplet @@ -363,45 +334,4 @@ export async function createRouter( ): Promise { return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`; } - - async function isBadgeDatabaseRefreshNeeded( - lastDatabaseRefreshTimestamp: number, - ): Promise { - if ((await db.countAllBadges()) === 0) { - logger.info('Badge database is empty, refreshing it'); - return true; - } - - if ( - lastDatabaseRefreshTimestamp === 0 || - Date.now() - lastDatabaseRefreshTimestamp > cacheTimeToLive * 1000 - ) { - logger.info( - 'Badge database refresh cache to live exceeded, refreshing it', - ); - return true; - } - - return false; - } - - async function refreshBadgeDatabase(): Promise { - const token = await tokenManager.getToken(); - const entities = await catalog.getEntities( - { - filter: { - kind: ['Component'], - }, - }, - token, - ); - logger.info( - `Refreshing badge database with ${entities.items.length} entities`, - ); - await db.createAllBadges(entities, salt); - logger.info('Badge database refreshed, deleting obsolete hashes'); - await db.deleteObsoleteHashes(entities, salt); - - return Date.now(); - } } diff --git a/plugins/badges-backend/src/service/standaloneServer.ts b/plugins/badges-backend/src/service/standaloneServer.ts index 5def5d6499..b07e1fa3b1 100644 --- a/plugins/badges-backend/src/service/standaloneServer.ts +++ b/plugins/badges-backend/src/service/standaloneServer.ts @@ -70,7 +70,7 @@ export async function startStandaloneServer( tokenManager, logger, identity, - db: await DatabaseBadgesStore.create({ + badgeStore: await DatabaseBadgesStore.create({ database: { getClient: async () => database }, }), }); diff --git a/plugins/badges-backend/src/tests/badgeStore.test.ts b/plugins/badges-backend/src/tests/badgeStore.test.ts index 1c6a76cb38..2fd18e4233 100644 --- a/plugins/badges-backend/src/tests/badgeStore.test.ts +++ b/plugins/badges-backend/src/tests/badgeStore.test.ts @@ -18,31 +18,8 @@ import { DatabaseBadgesStore } from '../database/badgesStore'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Knex } from 'knex'; import { Entity } from '@backstage/catalog-model'; -import { ConfigReader } from '@backstage/config'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; -import crypto from 'crypto'; describe('DatabaseBadgesStore', () => { - const config = new ConfigReader({ - backend: { - baseUrl: 'http://127.0.0.1', - listen: { - port: 7007, - }, - }, - app: { - badges: { - obfuscate: true, - }, - }, - custom: { - 'badges-backend': { - salt: 'random-string', - cacheTimeToLive: '60', - }, - }, - }); - const entity: Entity = { apiVersion: 'v1', kind: 'Component', @@ -51,28 +28,6 @@ describe('DatabaseBadgesStore', () => { }, }; - const entities: Entity[] = [ - entity, - { - apiVersion: 'v1', - kind: 'Component', - metadata: { - name: 'test-2', - }, - }, - { - apiVersion: 'v1', - kind: 'Component', - metadata: { - name: 'test-3', - }, - }, - ]; - - const defaultEntityListResponse: GetEntitiesResponse = { - items: entities, - }; - const databases = TestDatabases.create(); async function createDatabaseBadgesStore(databaseId: TestDatabaseId) { @@ -93,53 +48,33 @@ describe('DatabaseBadgesStore', () => { ({ knex, badgeStore } = await createDatabaseBadgesStore(databaseId)); }); - it('createAllBadges', async () => { - await badgeStore.createAllBadges( - defaultEntityListResponse, - config.getString('custom.badges-backend.salt'), + it('createABadge', async () => { + const uuid = await badgeStore.addBadge( + entity.metadata.name, + entity.metadata.namespace || 'default', + entity.kind, ); - const storedBadges = await badgeStore.getAllBadges(); - const testedEntities: { - name: string; - namespace: string; - kind: string; - hash: string; - }[] = []; - - entities.forEach(entityInLoop => { - const kind = entityInLoop.kind.toLowerCase(); - const namespace = entityInLoop.metadata.namespace || 'default'; - const name = entityInLoop.metadata.name.toLocaleLowerCase(); - const salt = config.getString('custom.badges-backend.salt'); - const entityHash = crypto - .createHash('sha256') - .update(`${kind}:${namespace}:${name}:${salt}`) - .digest('hex'); - - testedEntities.push({ - hash: entityHash, - name: name, - namespace: namespace, - kind: kind, - }); - expect(storedBadges).toEqual(expect.arrayContaining(testedEntities)); - expect(storedBadges.length).toEqual(3); - }); + const storedBadge = await badgeStore.getBadgeFromUuid(uuid.uuid); + expect(storedBadge?.kind).toEqual(entity.kind); + expect(storedBadge?.name).toEqual(entity.metadata.name); + expect(storedBadge?.namespace).toEqual( + entity.metadata.namespace || 'default', + ); }); - it('getBadgeFromHash', async () => { + it('getBadgeFromUuid', async () => { await knex('badges').truncate(); await knex('badges').insert([ { - hash: 'hash1', + uuid: 'uuid1', name: 'test', namespace: 'default', kind: 'component', }, ]); - const storedEntity = await badgeStore.getBadgeFromHash('hash1'); + const storedEntity = await badgeStore.getBadgeFromUuid('uuid1'); expect(storedEntity).toEqual({ name: 'test', @@ -148,147 +83,26 @@ describe('DatabaseBadgesStore', () => { }); }); - it('getHashFromEntityMetadata', async () => { + it('getUuidFromEntityMetadata', async () => { await knex('badges').truncate(); await knex('badges').insert([ { - hash: 'hash1', + uuid: 'uuid1', name: 'test', namespace: 'default', kind: 'component', }, ]); - const storedHash = await badgeStore.getHashFromEntityMetadata( + const storedUuid = await badgeStore.getUuidFromEntityMetadata( 'test', 'default', 'component', ); - expect(storedHash).toEqual({ - hash: 'hash1', + expect(storedUuid).toEqual({ + uuid: 'uuid1', }); }); - - it('deleteObsoleteHashes', async () => { - const entityHash = crypto - .createHash('sha256') - .update( - `component:default:test:${config.getString( - 'custom.badges-backend.salt', - )}`, - ) - .digest('hex'); - - await knex('badges').insert([ - { - hash: 'obsoleteHash', - name: 'obsoleteEntity', - namespace: 'default', - kind: 'component', - }, - { - hash: entityHash, - name: 'test', - namespace: 'default', - kind: 'component', - }, - ]); - - await badgeStore.deleteObsoleteHashes( - defaultEntityListResponse, - config.getString('custom.badges-backend.salt'), - ); - - const storedBadges = await badgeStore.getAllBadges(); - - expect(storedBadges).toEqual( - expect.not.arrayContaining([ - { - hash: 'obsoleteHash', - name: 'obsoleteEntity', - namespace: 'default', - kind: 'component', - }, - ]), - ); - expect(storedBadges.length).toEqual(1); - expect(storedBadges).toEqual( - expect.arrayContaining([ - { - hash: entityHash, - name: 'test', - namespace: 'default', - kind: 'component', - }, - ]), - ); - }); - - it('countAllBadges', async () => { - await knex('badges').truncate(); - await knex('badges').insert([ - { - hash: 'hash1', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash2', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash3', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash4', - name: 'test', - namespace: 'default', - kind: 'component', - }, - ]); - - const storedBadgesCount = await badgeStore.countAllBadges(); - expect(storedBadgesCount).toEqual(4); - }); - - it('getAllBadges', async () => { - await knex('badges').truncate(); - await knex('badges').insert([ - { - hash: 'hash1', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash2', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash3', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash4', - name: 'test', - namespace: 'default', - kind: 'component', - }, - ]); - - const storedBadgesCount = await badgeStore.countAllBadges(); - expect(storedBadgesCount).toEqual(4); - }); }); }); diff --git a/plugins/badges-backend/src/tests/router-obfuscated.test.ts b/plugins/badges-backend/src/tests/router-obfuscated.test.ts index edf19d9f52..1253e59157 100644 --- a/plugins/badges-backend/src/tests/router-obfuscated.test.ts +++ b/plugins/badges-backend/src/tests/router-obfuscated.test.ts @@ -32,7 +32,6 @@ import { IdentityApiGetIdentityRequest, } from '@backstage/plugin-auth-node'; import { BadgesStore } from '../database/badgesStore'; -import crypto from 'crypto'; describe('createRouter', () => { let app: express.Express; @@ -80,18 +79,16 @@ describe('createRouter', () => { listen: { port: 7007, }, + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, }, app: { badges: { obfuscate: true, }, }, - custom: { - 'badges-backend': { - salt: 'random-string', - cacheTimeToLive: '60', - }, - }, }); let discovery: PluginEndpointDiscovery; @@ -131,40 +128,18 @@ describe('createRouter', () => { kind: 'component', }; - const badgeEntities = [ - { - hash: 'hash1', - name: 'test', - namespace: 'default', - kind: 'component', - }, - { - hash: 'hash2', - name: 'test2', - namespace: 'default', - kind: 'component', - }, - ]; - const badgeStore: jest.Mocked = { - createAllBadges: jest.fn(), - getBadgeFromHash: jest.fn().mockImplementation(async () => { + addBadge: jest.fn().mockImplementation(async () => { + return { uuid: 'uuid1' }; + }), + getBadgeFromUuid: jest.fn().mockImplementation(async () => { return badgeEntity; }), - getHashFromEntityMetadata: jest + getUuidFromEntityMetadata: jest .fn() - .mockImplementation(async () => 'niceHash'), - deleteObsoleteHashes: jest.fn(), - countAllBadges: jest.fn().mockImplementation(async () => 4), - getAllBadges: jest.fn().mockImplementation(async () => badgeEntities), + .mockImplementation(async () => 'uuid1'), }; - const salt = config.getString('custom.badges-backend.salt'); - const entityHash = crypto - .createHash('sha256') - .update(`component:default:test:${salt}`) - .digest('hex'); - beforeAll(async () => { discovery = SingleHostDiscovery.fromConfig(config); const tokenManager = ServerTokenManager.noop(); @@ -176,7 +151,6 @@ describe('createRouter', () => { tokenManager, logger: getVoidLogger(), identity: { getIdentity }, - db: badgeStore, }); app = express().use(router); }); @@ -185,7 +159,7 @@ describe('createRouter', () => { jest.clearAllMocks(); }); - it('works', async () => { + it('works with provided badgeStore', async () => { const tokenManager = ServerTokenManager.noop(); const router = await createRouter({ badgeBuilder, @@ -195,7 +169,7 @@ describe('createRouter', () => { tokenManager, logger: getVoidLogger(), identity: { getIdentity }, - db: badgeStore, + badgeStore: badgeStore, }); expect(router).toBeDefined(); }); @@ -240,113 +214,9 @@ describe('createRouter', () => { }); }); - describe('GET /entity/:entityHash/badge-specs', () => { - it('returns all badge specs for entity', async () => { - catalog.getEntities.mockResolvedValueOnce({ items: entities }); - catalog.getEntityByRef.mockResolvedValueOnce(entity); - - badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); - badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); - - const response = await request(app).get(`/entity/hash1/badge-specs`); - expect(response.status).toEqual(200); - expect(response.body).toEqual([badge]); - - expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByRef).toHaveBeenCalledWith( - { - namespace: 'default', - kind: 'component', - name: 'test', - }, - { token: '' }, - ); - - expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadgeJson).toHaveBeenCalledWith({ - badgeInfo: { id: badge.id }, - context: { - badgeUrl: expect.stringMatching( - /http:\/\/127.0.0.1\/api\/badges\/entity\/hash1\/test-badge/, - ), - config, - entity, - }, - }); - }); - }); - - describe('GET /entity/:entityHash/test-badge', () => { - it('returns badge for entity', async () => { - catalog.getEntityByRef.mockResolvedValueOnce(entity); - catalog.getEntities.mockResolvedValueOnce({ items: entities }); - - const image = '...'; - badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); - - const response = await request(app).get( - '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7c322c600e7497dc9d517353e5bed/test-badge', - ); - - expect(response.status).toEqual(200); - expect(response.body).toEqual(Buffer.from(image)); - - expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByRef).toHaveBeenCalledWith( - { - namespace: 'default', - kind: 'component', - name: 'test', - }, - { token: '' }, - ); - - expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0); - expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({ - badgeInfo: { id: badge.id }, - context: { - badgeUrl: expect.stringMatching( - /http:\/\/127.0.0.1\/api\/badges\/entity\/3a5f91c1e66519be5394c37a8ba69c3087b7c322c600e7497dc9d517353e5bed\/test-badge/, - ), - config, - entity, - }, - }); - }); - - it('returns badge spec for entity', async () => { - catalog.getEntityByRef.mockResolvedValueOnce(entity); - catalog.getEntities.mockResolvedValueOnce({ items: entities }); - badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); - - const url = - '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7c322c600e7497dc9d517353e5bed/test-badge?format=json'; - const response = await request(app).get(url); - - expect(response.status).toEqual(200); - expect(response.body).toEqual(badge); - }); - }); - describe('GET /entity/:namespace/:kind/:name/obfuscated', () => { catalog.getEntityByRef.mockResolvedValueOnce(entity); catalog.getEntities.mockResolvedValueOnce({ items: entities }); - badgeStore.getHashFromEntityMetadata.mockResolvedValue({ - hash: entityHash, - }); - - it('returns obfuscated entity', async () => { - const obfuscatedEntity = await request(app) - .get('/entity/default/component/test/obfuscated') - .set('Authorization', 'Bearer fakeToken'); - expect(obfuscatedEntity.status).toEqual(200); - // echo -n "component:default:test:random-string" | openssl dgst -sha256 - expect(obfuscatedEntity.body).toEqual({ - hash: entityHash, - }); - }); it('returns obfuscated 401 if no auth', async () => { const obfuscatedEntity = await request(app).get( @@ -354,39 +224,82 @@ describe('createRouter', () => { ); expect(obfuscatedEntity.status).toEqual(401); }); - }); - describe('Errors', () => { - it('returns 404 for unknown entity hash', async () => { - badgeStore.getBadgeFromHash.mockResolvedValue(undefined); + it('returns obfuscated entity and badges', async () => { + const obfuscatedEntity = await request(app) + .get('/entity/default/component/test/obfuscated') + .set('Authorization', 'Bearer fakeToken'); + expect(obfuscatedEntity.status).toEqual(200); + expect(obfuscatedEntity.body.uuid).toMatch( + new RegExp( + '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + ), + ); + + const uuid = obfuscatedEntity.body.uuid; + const url = `/entity/${uuid}/test-badge?format=json`; + let response = await request(app).get(url); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(badge); + catalog.getEntityByRef.mockResolvedValueOnce(entity); catalog.getEntities.mockResolvedValueOnce({ items: entities }); + + const image = '...'; + badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); + + response = await request(app).get(`/entity/${uuid}/test-badge`); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(Buffer.from(image)); + + catalog.getEntities.mockResolvedValueOnce({ items: entities }); + catalog.getEntityByRef.mockResolvedValueOnce(entity); + badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); - async function testUrl(url: string) { - const response = await request(app).get(url); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ - error: { - message: expect.any(String), - name: 'NotFoundError', - }, - request: { - method: 'GET', - url, - }, - response: { - statusCode: 404, - }, - }); - } - await testUrl( - '/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs', - ); - await testUrl( - '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge', - ); + response = await request(app).get(`/entity/${uuid}/badge-specs`); + expect(response.status).toEqual(200); + expect(response.body).toEqual([badge]); + + expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1); + expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(2); + }); + + describe('Errors', () => { + it('returns 404 for unknown entity uuid', async () => { + badgeStore.getBadgeFromUuid.mockResolvedValue(undefined); + catalog.getEntityByRef.mockResolvedValueOnce(entity); + catalog.getEntities.mockResolvedValueOnce({ items: entities }); + badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + async function testUrl(url: string) { + const response = await request(app).get(url); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ + error: { + message: expect.any(String), + name: 'NotFoundError', + }, + request: { + method: 'GET', + url, + }, + response: { + statusCode: 404, + }, + }); + } + await testUrl( + '/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs', + ); + await testUrl( + '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge', + ); + }); }); }); }); diff --git a/plugins/badges-backend/src/tests/router.test.ts b/plugins/badges-backend/src/tests/router.test.ts index 1d5fb3e9a1..a44281e9fa 100644 --- a/plugins/badges-backend/src/tests/router.test.ts +++ b/plugins/badges-backend/src/tests/router.test.ts @@ -75,12 +75,9 @@ describe('createRouter', () => { }; const badgeStore: jest.Mocked = { - createAllBadges: jest.fn(), - getBadgeFromHash: jest.fn(), - getHashFromEntityMetadata: jest.fn(), - deleteObsoleteHashes: jest.fn(), - countAllBadges: jest.fn(), - getAllBadges: jest.fn(), + getBadgeFromUuid: jest.fn(), + getUuidFromEntityMetadata: jest.fn(), + addBadge: jest.fn(), }; beforeAll(async () => { @@ -93,6 +90,10 @@ describe('createRouter', () => { backend: { baseUrl: 'http://127.0.0.1', listen: { port: 7007 }, + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, }, }); @@ -123,7 +124,6 @@ describe('createRouter', () => { tokenManager, logger: getVoidLogger(), identity: { getIdentity }, - db: badgeStore, }); app = express().use(router); }); @@ -132,7 +132,7 @@ describe('createRouter', () => { jest.resetAllMocks(); }); - it('works', async () => { + it('works with badgeStore', async () => { const tokenManager = ServerTokenManager.noop(); const router = await createRouter({ badgeBuilder, @@ -142,7 +142,7 @@ describe('createRouter', () => { tokenManager, logger: getVoidLogger(), identity: { getIdentity }, - db: badgeStore, + badgeStore: badgeStore, }); expect(router).toBeDefined(); }); @@ -261,7 +261,7 @@ describe('createRouter', () => { }); }); - it('returns 404 for hashed entities', async () => { + it('returns 404 for uuid entities', async () => { catalog.getEntityByRef.mockResolvedValue(undefined); async function testUrl(url: string) { const response = await request(app).get(url); diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index db5019d893..9a08db882a 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -47,17 +47,16 @@ export class BadgesClient implements BadgesApi { // Check if obfuscation is enabled in the configuration const obfuscate = this.configApi.getOptionalBoolean('app.badges.obfuscate'); - // If obfuscation is enabled, get the hash of the entity and use that to get the badge specs if (obfuscate) { - const entityHashUrl = await this.getEntityHashUrl(entity); - const entityHash = await this.getEntityHash(entityHashUrl).then(data => { - return data.hash; + const entityUuidUrl = await this.getEntityUuidUrl(entity); + const entityUuid = await this.getEntityUuid(entityUuidUrl).then(data => { + return data.uuid; }); - const entityHashedBadgeSpecsUrl = await this.getEntityHashedBadgeSpecsUrl( - entityHash, + const entityUuidBadgeSpecsUrl = await this.getEntityUuidBadgeSpecsUrl( + entityUuid, ); - const response = await this.fetchApi.fetch(entityHashedBadgeSpecsUrl); + const response = await this.fetchApi.fetch(entityUuidBadgeSpecsUrl); if (!response.ok) { throw await ResponseError.fromResponse(response); } @@ -75,8 +74,7 @@ export class BadgesClient implements BadgesApi { return await response.json(); } - // This function is used to generate the URL to get the badge specs for an entity when obfuscation is enabled. Using the hash - private async getEntityHashUrl(entity: Entity): Promise { + private async getEntityUuidUrl(entity: Entity): Promise { const routeParams = this.getEntityRouteParams(entity); const path = generatePath(`:namespace/:kind/:name`, routeParams); const baseUrl = await this.discoveryApi.getBaseUrl('badges'); @@ -85,25 +83,22 @@ export class BadgesClient implements BadgesApi { return obfuscatedEntityUrl; } - // This function is used to get the hash of an entity when obfuscation is enabled. It's calling the badges backend to get the hash - private async getEntityHash(entityHashUrl: string): Promise { - const responseEntityHash = await this.fetchApi.fetch(entityHashUrl); + private async getEntityUuid(entityUuidUrl: string): Promise { + const responseEntityUuid = await this.fetchApi.fetch(entityUuidUrl); - if (!responseEntityHash.ok) { - throw await ResponseError.fromResponse(responseEntityHash); + if (!responseEntityUuid.ok) { + throw await ResponseError.fromResponse(responseEntityUuid); } - return await responseEntityHash.json(); + return await responseEntityUuid.json(); } - // This function is used to generate the URLs to use in the frontend when obfuscation is enabled. It's using the hash of the entity to get the badge specs - private async getEntityHashedBadgeSpecsUrl(entityHash: { - hash: string; + private async getEntityUuidBadgeSpecsUrl(entityUuid: { + uuid: string; }): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('badges'); - return `${baseUrl}/entity/${entityHash}/badge-specs`; + return `${baseUrl}/entity/${entityUuid}/badge-specs`; } - // This function is used to generate the URLs to use in the frontend when obfuscation is disabled. It's using the entity name to get the badge specs private async getEntityBadgeSpecsUrl(entity: Entity): Promise { const routeParams = this.getEntityRouteParams(entity); const path = generatePath(`:namespace/:kind/:name`, routeParams); diff --git a/yarn.lock b/yarn.lock index 899440e09e..442d981e9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4953,6 +4953,7 @@ __metadata: knex: ^2.4.2 lodash: ^4.17.21 supertest: ^6.3.3 + uuid: ^9.0.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown