Merge pull request #17024 from Rbillon59/feat/badges/enable-safe-public-access
Enable safe public access for Badges
This commit is contained in:
@@ -18,6 +18,51 @@ This will popup a badges dialog showing all available badges for that entity lik
|
||||
|
||||

|
||||
|
||||
## 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 see <https://backstage.io/docs/conf/defining/#visibility> :
|
||||
|
||||
Example implementation would be in : `packages/app/src/config.d.ts`
|
||||
|
||||
```typescript
|
||||
export interface Config {
|
||||
app: {
|
||||
... some code
|
||||
badges: {
|
||||
/**
|
||||
* badges obfuscate
|
||||
* @visibility frontend
|
||||
*/
|
||||
obfuscate?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
then include in the `packages/app/package.json` :
|
||||
|
||||
```json
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts",
|
||||
```
|
||||
|
||||
## Sample Badges
|
||||
|
||||
Here are some samples of badges for the `artists-lookup` service in the Demo Backstage site:
|
||||
|
||||
@@ -50,14 +50,9 @@
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/dom": "^8.0.0",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^1.0.0"
|
||||
"cross-fetch": "^3.1.5"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -18,31 +18,55 @@ 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, FetchApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export class BadgesClient implements BadgesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly fetchApi: FetchApi;
|
||||
private readonly configApi: ConfigApi;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
fetchApi: FetchApi;
|
||||
configApi: ConfigApi;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.fetchApi = options.fetchApi;
|
||||
this.configApi = options.configApi;
|
||||
}
|
||||
|
||||
static fromConfig(options: {
|
||||
fetchApi: FetchApi;
|
||||
discoveryApi: DiscoveryApi;
|
||||
configApi: ConfigApi;
|
||||
}) {
|
||||
return new BadgesClient(options);
|
||||
}
|
||||
|
||||
public async getEntityBadgeSpecs(entity: Entity): Promise<BadgeSpec[]> {
|
||||
const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
const response = await fetch(entityBadgeSpecsUrl, {
|
||||
headers: token
|
||||
? {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
// Check if obfuscation is enabled in the configuration
|
||||
const obfuscate = this.configApi.getOptionalBoolean('app.badges.obfuscate');
|
||||
|
||||
if (obfuscate) {
|
||||
const entityUuidUrl = await this.getEntityUuidUrl(entity);
|
||||
const entityUuid = await this.getEntityUuid(entityUuidUrl).then(data => {
|
||||
return data.uuid;
|
||||
});
|
||||
const entityUuidBadgeSpecsUrl = await this.getEntityUuidBadgeSpecsUrl(
|
||||
entityUuid,
|
||||
);
|
||||
|
||||
const response = await this.fetchApi.fetch(entityUuidBadgeSpecsUrl);
|
||||
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 this.fetchApi.fetch(entityBadgeSpecsUrl);
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
@@ -50,14 +74,39 @@ export class BadgesClient implements BadgesApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getEntityUuidUrl(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;
|
||||
}
|
||||
|
||||
private async getEntityUuid(entityUuidUrl: string): Promise<any> {
|
||||
const responseEntityUuid = await this.fetchApi.fetch(entityUuidUrl);
|
||||
|
||||
if (!responseEntityUuid.ok) {
|
||||
throw await ResponseError.fromResponse(responseEntityUuid);
|
||||
}
|
||||
return await responseEntityUuid.json();
|
||||
}
|
||||
|
||||
private async getEntityUuidBadgeSpecsUrl(entityUuid: {
|
||||
uuid: string;
|
||||
}): Promise<string> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('badges');
|
||||
return `${baseUrl}/entity/${entityUuid}/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'),
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
import { badgesApiRef, BadgesClient } from './api';
|
||||
import {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createComponentExtension,
|
||||
createPlugin,
|
||||
fetchApiRef,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/** @public */
|
||||
@@ -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: {
|
||||
fetchApi: fetchApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ fetchApi, discoveryApi, configApi }) =>
|
||||
new BadgesClient({
|
||||
fetchApi,
|
||||
discoveryApi,
|
||||
configApi,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user