feat(plugins): Update frontend and backend badges plugin to implement an obfuscation feature to limit security risks when opening badges to public

Signed-off-by: Rbillon59 <r.billon@celonis.com>
This commit is contained in:
Rbillon59
2023-03-22 15:49:20 +01:00
parent 8444b0834f
commit a0108c4977
10 changed files with 1045 additions and 131 deletions
+33
View File
@@ -18,6 +18,39 @@ This will popup a badges dialog showing all available badges for that entity lik
![Badges Dialog](./doc/badges-dialog.png)
##  Badge obfuscation
The badges plugin supports obfuscating the badge URL to prevent it from being enumerated if the badges are used in a public context (like in Github repositories).
To enable obfuscation, set the `obfuscate` option to `true` in the `app.badges` section of your `app-config.yaml`:
```yaml
app:
badges:
obfuscate: true
```
Please note that if you have already set badges in your repositories and you activate the obfuscation you will need to update the badges in your repositories to use the new obfuscated URLs.
Please note that the backend part needs to be configured to support obfuscation. See the [backend plugin documentation](../badges-backend/README.md) for more details.
Also, you need to allow your frontend to access the configuration :
```typescript
export interface Config {
app: {
... some code
badges: {
/**
* badges obfuscate
* @visibility frontend
*/
obfuscate?: string;
};
};
}
```
## Sample Badges
Here are some samples of badges for the `artists-lookup` service in the Demo Backstage site:
+86 -6
View File
@@ -18,23 +18,67 @@ import { generatePath } from 'react-router-dom';
import { ResponseError } from '@backstage/errors';
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { BadgesApi, BadgeSpec } from './types';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import {
ConfigApi,
DiscoveryApi,
IdentityApi,
} from '@backstage/core-plugin-api';
export class BadgesClient implements BadgesApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly configApi: ConfigApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.configApi = options.configApi;
}
static fromConfig(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
configApi: ConfigApi;
}) {
return new BadgesClient(options);
}
public async getEntityBadgeSpecs(entity: Entity): Promise<BadgeSpec[]> {
const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
// Check if obfuscation is enabled in the configuration
const obfuscate =
this.configApi.getOptionalBoolean('app.badges.obfuscate') ?? false;
const { token } = await this.identityApi.getCredentials();
// 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 entityHashedBadgeSpecsUrl = await this.getEntityHashedBadgeSpecsUrl(
entityHash,
);
const response = await fetch(entityHashedBadgeSpecsUrl, {
headers: token
? {
Authorization: `Bearer ${token}`,
}
: undefined,
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
// If obfuscation is disabled, get the badge specs directly as the previous implementation
const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
const response = await fetch(entityBadgeSpecsUrl, {
headers: token
? {
@@ -42,7 +86,6 @@ export class BadgesClient implements BadgesApi {
}
: undefined,
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
@@ -50,14 +93,51 @@ 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<string> {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(`:namespace/:kind/:name`, routeParams);
const baseUrl = await this.discoveryApi.getBaseUrl('badges');
const obfuscatedEntityUrl = `${baseUrl}/entity/${path}/obfuscated`;
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<any> {
const { token: idToken } = await this.identityApi.getCredentials();
const responseEntityHash = await fetch(entityHashUrl, {
headers: idToken
? {
Authorization: `Bearer ${idToken}`,
}
: undefined,
});
if (!responseEntityHash.ok) {
throw await ResponseError.fromResponse(responseEntityHash);
}
return await responseEntityHash.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;
}): Promise<string> {
const baseUrl = await this.discoveryApi.getBaseUrl('badges');
return `${baseUrl}/entity/${entityHash}/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<string> {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(`:namespace/:kind/:name`, routeParams);
return `${await this.discoveryApi.getBaseUrl(
'badges',
)}/entity/${path}/badge-specs`;
const baseUrl = await this.discoveryApi.getBaseUrl('badges');
return `${baseUrl}/entity/${path}/badge-specs`;
}
// This function is used to generate the route parameters using the entity kind, namespace and name
private getEntityRouteParams(entity: Entity) {
return {
kind: entity.kind.toLocaleLowerCase('en-US'),
+12 -3
View File
@@ -15,6 +15,7 @@
*/
import { badgesApiRef, BadgesClient } from './api';
import {
configApiRef,
createApiFactory,
createComponentExtension,
createPlugin,
@@ -28,9 +29,17 @@ export const badgesPlugin = createPlugin({
apis: [
createApiFactory({
api: badgesApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new BadgesClient({ discoveryApi, identityApi }),
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, identityApi, configApi }) =>
new BadgesClient({
discoveryApi,
identityApi,
configApi,
}),
}),
],
});