feat(badges): Remove hash implementation and replace it with uuid one.
Signed-off-by: Rbillon59 <r.billon@celonis.com>
This commit is contained in:
@@ -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<void>;
|
||||
|
||||
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<void>;
|
||||
getBadgeFromUuid(
|
||||
uuid: string,
|
||||
): Promise<{ name: string; namespace: string; kind: string } | undefined>;
|
||||
|
||||
countAllBadges(): Promise<number>;
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<number> {
|
||||
const result = await this.db('badges').countDistinct('hash as count');
|
||||
const count: number = +result[0].count;
|
||||
return count;
|
||||
return { uuid };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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<string> {
|
||||
return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`;
|
||||
}
|
||||
|
||||
async function isBadgeDatabaseRefreshNeeded(
|
||||
lastDatabaseRefreshTimestamp: number,
|
||||
): Promise<boolean> {
|
||||
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<number> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function startStandaloneServer(
|
||||
tokenManager,
|
||||
logger,
|
||||
identity,
|
||||
db: await DatabaseBadgesStore.create({
|
||||
badgeStore: await DatabaseBadgesStore.create({
|
||||
database: { getClient: async () => database },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<BadgesStore> = {
|
||||
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 = '<svg>...</svg>';
|
||||
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 = '<svg>...</svg>';
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,12 +75,9 @@ describe('createRouter', () => {
|
||||
};
|
||||
|
||||
const badgeStore: jest.Mocked<BadgesStore> = {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user