entity-feedback-backend: migrated to support new auth services

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Carl-Erik Bergström <cbergstrom@spotify.com>
Co-authored-by: blam <ben@blam.sh>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-16 13:39:07 +01:00
parent 72572b2fe1
commit 4f8ecd65f9
4 changed files with 84 additions and 69 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-entity-feedback-backend': patch
---
Migrated to support new auth services.
@@ -3,8 +3,10 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AuthService } from '@backstage/backend-plugin-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import express from 'express';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
@@ -19,11 +21,15 @@ export default entityFeedbackPlugin;
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
auth?: AuthService;
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
discovery: PluginEndpointDiscovery;
// (undocumented)
httpAuth?: HttpAuthService;
// (undocumented)
identity: IdentityApi;
// (undocumented)
logger: Logger;
@@ -21,11 +21,11 @@ import {
} from '@backstage/backend-common';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import { IdentityApi } from '@backstage/plugin-auth-node';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
const sampleOwnedEntities = [
{
@@ -65,7 +65,7 @@ const sampleEntities = [
},
];
const mockGetEntties = jest
const mockGetEntities = jest
.fn()
.mockImplementation(async () => ({ items: sampleOwnedEntities }));
@@ -75,15 +75,11 @@ const mockGetEnttiesByRefs = jest
jest.mock('@backstage/catalog-client', () => ({
CatalogClient: jest.fn().mockImplementation(() => ({
getEntities: mockGetEntties,
getEntities: mockGetEntities,
getEntitiesByRefs: mockGetEnttiesByRefs,
})),
}));
jest.mock('@backstage/plugin-auth-node', () => ({
getBearerTokenFromAuthorizationHeader: () => 'token',
}));
const mockRatings = [
{ userRef: 'user:default/foo', rating: 'LIKE' },
{ userRef: 'user:default/bar', rating: 'LIKE' },
@@ -149,12 +145,6 @@ describe('createRouter', () => {
}),
).forPlugin('entity-feedback');
const mockIdentityClient = {
getIdentity: jest.fn().mockImplementation(async () => ({
identity: { userEntityRef: 'user:default/me' },
})),
} as unknown as IdentityApi;
const discovery: jest.Mocked<PluginEndpointDiscovery> = {
getBaseUrl: jest.fn(),
getExternalBaseUrl: jest.fn(),
@@ -164,8 +154,10 @@ describe('createRouter', () => {
const router = await createRouter({
database: createDatabase(),
discovery,
identity: mockIdentityClient,
identity: mockServices.identity(),
logger: getVoidLogger(),
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
});
app = express().use(router);
@@ -174,44 +166,38 @@ describe('createRouter', () => {
describe('GET /ratings', () => {
it('should get ratings for all entities correctly', async () => {
const response = await request(app).get('/ratings').send();
const response = await request(app)
.get('/ratings')
.set('authorization', mockCredentials.user.header())
.send();
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
entityRef: 'component:default/foo',
entityTitle: 'Foo Component',
ratings: { LIKE: 3, DISLIKE: 1 },
},
{
entityRef: 'component:default/bar',
entityTitle: 'Bar Component',
ratings: { LIKE: 5 },
},
]);
expect(mockDbHandler.getAllRatedEntities).toHaveBeenCalled();
expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith(
sampleEntities
.filter(Boolean)
.map((ent: any) => stringifyEntityRef(ent)),
);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
entityRef: 'component:default/foo',
entityTitle: 'Foo Component',
ratings: { LIKE: 3, DISLIKE: 1 },
},
{
entityRef: 'component:default/bar',
entityTitle: 'Bar Component',
ratings: { LIKE: 5 },
},
]);
});
it('should get ratings for all owned entities correctly', async () => {
const response = await request(app)
.get('/ratings?ownerRef=group:default/test-team')
.set('authorization', mockCredentials.user.header())
.send();
expect(mockGetEntties).toHaveBeenCalledWith(
expect.objectContaining({
filter: { 'relations.ownedBy': 'group:default/test-team' },
}),
{ token: 'token' },
);
expect(mockDbHandler.getAllRatedEntities).not.toHaveBeenCalled();
expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith(
sampleOwnedEntities.map((ent: any) => stringifyEntityRef(ent)),
);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
@@ -225,6 +211,21 @@ describe('createRouter', () => {
ratings: { LIKE: 5 },
},
]);
expect(mockGetEntities).toHaveBeenCalledWith(
expect.objectContaining({
filter: { 'relations.ownedBy': 'group:default/test-team' },
}),
{
token: mockCredentials.service.token({
onBehalfOf: mockCredentials.user(),
targetPluginId: 'catalog',
}),
},
);
expect(mockDbHandler.getAllRatedEntities).not.toHaveBeenCalled();
expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith(
sampleOwnedEntities.map((ent: any) => stringifyEntityRef(ent)),
);
});
});
@@ -233,10 +234,11 @@ describe('createRouter', () => {
const body = { rating: 'LIKE' };
const response = await request(app)
.post('/ratings/component%3Adefault%2Fservice')
.set('authorization', mockCredentials.user.header())
.send(body);
expect(mockDbHandler.recordRating).toHaveBeenCalledWith({
entityRef: 'component:default/service',
userRef: 'user:default/me',
userRef: 'user:default/mock',
...body,
});
expect(response.status).toEqual(201);
@@ -247,6 +249,7 @@ describe('createRouter', () => {
it('should get ratings for an entity correctly', async () => {
const response = await request(app)
.get('/ratings/component%3Adefault%2Fservice')
.set('authorization', mockCredentials.user.header())
.send();
expect(mockDbHandler.getRatings).toHaveBeenCalledWith(
'component:default/service',
@@ -262,6 +265,7 @@ describe('createRouter', () => {
it('should get aggregated ratings for an entity correctly', async () => {
const response = await request(app)
.get('/ratings/component%3Adefault%2Fservice/aggregate')
.set('authorization', mockCredentials.user.header())
.send();
expect(mockDbHandler.getRatings).toHaveBeenCalledWith(
'component:default/service',
@@ -279,10 +283,11 @@ describe('createRouter', () => {
const body = { response: 'blah', comments: 'feedback', consent: true };
const response = await request(app)
.post('/responses/component%3Adefault%2Fservice')
.set('authorization', mockCredentials.user.header())
.send(body);
expect(mockDbHandler.recordResponse).toHaveBeenCalledWith({
entityRef: 'component:default/service',
userRef: 'user:default/me',
userRef: 'user:default/mock',
...body,
});
expect(response.status).toEqual(201);
@@ -293,6 +298,7 @@ describe('createRouter', () => {
it('should get responses for an entity correctly', async () => {
const response = await request(app)
.get('/responses/component%3Adefault%2Fservice')
.set('authorization', mockCredentials.user.header())
.send();
expect(mockDbHandler.getResponses).toHaveBeenCalledWith(
'component:default/service',
@@ -15,16 +15,15 @@
*/
import {
createLegacyAuthAdapters,
errorHandler,
PluginDatabaseManager,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api';
import { CatalogClient } from '@backstage/catalog-client';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import {
getBearerTokenFromAuthorizationHeader,
IdentityApi,
} from '@backstage/plugin-auth-node';
import { IdentityApi } from '@backstage/plugin-auth-node';
import {
EntityRatingsData,
Ratings,
@@ -43,6 +42,8 @@ export interface RouterOptions {
discovery: PluginEndpointDiscovery;
identity: IdentityApi;
logger: Logger;
auth?: AuthService;
httpAuth?: HttpAuthService;
}
/**
@@ -51,9 +52,10 @@ export interface RouterOptions {
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { database, discovery, identity, logger } = options;
const { database, discovery, logger } = options;
logger.info('Initializing Entity Feedback backend');
const { auth, httpAuth } = createLegacyAuthAdapters(options);
const catalogClient = new CatalogClient({ discoveryApi: discovery });
const db = await database.getClient();
@@ -63,9 +65,10 @@ export async function createRouter(
router.use(express.json());
router.get('/ratings', async (req, res) => {
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await httpAuth.credentials(req),
targetPluginId: 'catalog',
});
const requestedEntities: { [ref: string]: Entity } = {};
if (req.query.ownerRef) {
@@ -131,11 +134,12 @@ export async function createRouter(
});
router.post('/ratings/:entityRef', async (req, res) => {
const user = await identity.getIdentity({ request: req });
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
const rating = req.body.rating;
if (!user || !rating) {
if (!rating) {
logger.warn(
`Can't save rating because there is not enough info: user=${user}, rating=${rating}`,
`Can't save rating because there is not enough info: user=${credentials.principal.userEntityRef}, rating=${rating}`,
);
res.status(400).end();
return;
@@ -144,7 +148,7 @@ export async function createRouter(
await dbHandler.recordRating({
entityRef: req.params.entityRef,
rating,
userRef: user.identity.userEntityRef,
userRef: credentials.principal.userEntityRef,
});
res.status(201).end();
@@ -153,9 +157,10 @@ export async function createRouter(
router.get('/ratings/:entityRef', async (req, res) => {
const ratings = await dbHandler.getRatings(req.params.entityRef);
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await httpAuth.credentials(req),
targetPluginId: 'catalog',
});
// Filter ratings via user refs to only expose entity refs accessible by current user
const accessibleEntityRefs = (
@@ -177,9 +182,7 @@ export async function createRouter(
const entityRatings = (
await dbHandler.getRatings(req.params.entityRef)
).reduce((ratings: Ratings, { rating }) => {
ratings[rating] = ratings[rating] ?? 0;
ratings[rating]++;
ratings[rating] = (ratings[rating] ?? 0) + 1;
return ratings;
}, {});
@@ -187,21 +190,15 @@ export async function createRouter(
});
router.post('/responses/:entityRef', async (req, res) => {
const user = await identity.getIdentity({ request: req });
const { response, comments, consent } = req.body;
if (!user) {
logger.warn(`Could not identify user to save responses, user=${user}`);
res.status(400).end();
return;
}
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
await dbHandler.recordResponse({
entityRef: req.params.entityRef,
response,
comments,
consent,
userRef: user.identity.userEntityRef,
userRef: credentials.principal.userEntityRef,
});
res.status(201).end();
@@ -210,9 +207,10 @@ export async function createRouter(
router.get('/responses/:entityRef', async (req, res) => {
const responses = await dbHandler.getResponses(req.params.entityRef);
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await httpAuth.credentials(req),
targetPluginId: 'catalog',
});
// Filter responses via user refs to only expose entity refs accessible by current user
const accessibleEntityRefs = (