notifications-backend: migrated to use 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-14 12:18:01 +01:00
parent 72572b2fe1
commit 84af361987
9 changed files with 94 additions and 69 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-notifications-node': minor
'@backstage/plugin-notifications-backend': patch
---
Migrated to using the new auth services.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createBackendPlugin,
@@ -58,6 +59,8 @@ export const notificationsPlugin = createBackendPlugin({
env.registerInit({
deps: {
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
identity: coreServices.identity,
@@ -67,6 +70,8 @@ export const notificationsPlugin = createBackendPlugin({
signals: signalService,
},
async init({
auth,
httpAuth,
httpRouter,
logger,
identity,
@@ -77,6 +82,8 @@ export const notificationsPlugin = createBackendPlugin({
}) {
httpRouter.use(
await createRouter({
auth,
httpAuth,
logger,
identity,
database,
@@ -14,16 +14,14 @@
* limitations under the License.
*/
import {
createLegacyAuthAdapters,
errorHandler,
PluginDatabaseManager,
TokenManager,
} from '@backstage/backend-common';
import express, { Request } from 'express';
import Router from 'express-promise-router';
import {
getBearerTokenFromAuthorizationHeader,
IdentityApi,
} from '@backstage/plugin-auth-node';
import { IdentityApi } from '@backstage/plugin-auth-node';
import {
DatabaseNotificationsStore,
NotificationGetOptions,
@@ -39,7 +37,12 @@ import {
} from '@backstage/catalog-model';
import { NotificationProcessor } from '@backstage/plugin-notifications-node';
import { AuthenticationError, InputError } from '@backstage/errors';
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import {
AuthService,
DiscoveryService,
HttpAuthService,
LoggerService,
} from '@backstage/backend-plugin-api';
import { SignalService } from '@backstage/plugin-signals-node';
import {
NewNotificationSignal,
@@ -58,6 +61,8 @@ export interface RouterOptions {
signalService?: SignalService;
catalog?: CatalogApi;
processors?: NotificationProcessor[];
auth?: AuthService;
httpAuth?: HttpAuthService;
}
/** @internal */
@@ -70,7 +75,6 @@ export async function createRouter(
identity,
discovery,
catalog,
tokenManager,
processors,
signalService,
} = options;
@@ -79,6 +83,8 @@ export async function createRouter(
catalog ?? new CatalogClient({ discoveryApi: discovery });
const store = await DatabaseNotificationsStore.create({ database });
const { auth, httpAuth } = createLegacyAuthAdapters(options);
const getUser = async (req: Request<unknown>) => {
const user = await identity.getIdentity({ request: req });
if (!user) {
@@ -87,20 +93,13 @@ export async function createRouter(
return user.identity.userEntityRef;
};
const authenticateService = async (req: Request<unknown>) => {
const token = getBearerTokenFromAuthorizationHeader(
req.header('authorization'),
);
if (!token) {
throw new AuthenticationError();
}
await tokenManager.authenticate(token);
};
const getUsersForEntityRef = async (
entityRef: string | string[] | null,
): Promise<string[]> => {
const { token } = await tokenManager.getToken();
const { token } = await auth.getPluginRequestToken({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'catalog',
});
// TODO: Support for broadcast
if (entityRef === null) {
@@ -276,11 +275,7 @@ export async function createRouter(
const notifications = [];
let users = [];
try {
await authenticateService(req);
} catch (e) {
throw new AuthenticationError();
}
await httpAuth.credentials(req, { allow: ['service'] });
const { title, link, scope } = payload;
+5 -7
View File
@@ -3,21 +3,19 @@
> 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 { DiscoveryService } from '@backstage/backend-plugin-api';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { NotificationPayload } from '@backstage/plugin-notifications-common';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { TokenManager } from '@backstage/backend-common';
// @public (undocumented)
export class DefaultNotificationService implements NotificationService {
// (undocumented)
static create({
tokenManager,
discovery,
pluginId,
}: NotificationServiceOptions): DefaultNotificationService;
static create(
options: NotificationServiceOptions,
): DefaultNotificationService;
// (undocumented)
send(notification: NotificationSendOptions): Promise<void>;
}
@@ -51,8 +49,8 @@ export const notificationService: ServiceRef<NotificationService, 'plugin'>;
// @public (undocumented)
export type NotificationServiceOptions = {
auth: AuthService;
discovery: DiscoveryService;
tokenManager: TokenManager;
pluginId: string;
};
+1
View File
@@ -27,6 +27,7 @@
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"msw": "^1.0.0"
+3 -3
View File
@@ -29,14 +29,14 @@ export const notificationService = createServiceRef<NotificationService>({
createServiceFactory({
service,
deps: {
auth: coreServices.auth,
discovery: coreServices.discovery,
tokenManager: coreServices.tokenManager,
pluginMetadata: coreServices.pluginMetadata,
},
factory({ discovery, tokenManager, pluginMetadata }) {
factory({ auth, discovery, pluginMetadata }) {
return DefaultNotificationService.create({
auth,
discovery,
tokenManager,
pluginId: pluginMetadata.getId(),
});
},
@@ -22,6 +22,7 @@ import {
DefaultNotificationService,
NotificationSendOptions,
} from './DefaultNotificationService';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
const server = setupServer();
@@ -33,21 +34,14 @@ const testNotification: NotificationPayload = {
describe('DefaultNotificationService', () => {
setupRequestMockHandlers(server);
const mockBaseUrl = 'http://backstage/api/notifications';
const discoveryApi = {
getBaseUrl: async () => mockBaseUrl,
getExternalBaseUrl: async () => mockBaseUrl,
};
const tokenManager = {
getToken: async () => ({ token: '1234' }),
authenticate: jest.fn(),
};
const discovery = mockServices.discovery();
const auth = mockServices.auth();
let service: DefaultNotificationService;
beforeEach(() => {
beforeEach(async () => {
service = DefaultNotificationService.create({
discovery: discoveryApi,
tokenManager,
auth,
discovery,
pluginId: 'test',
});
});
@@ -60,14 +54,22 @@ describe('DefaultNotificationService', () => {
};
server.use(
rest.post(`${mockBaseUrl}/`, async (req, res, ctx) => {
const json = await req.json();
expect(json).toEqual({ ...body, origin: 'plugin-test' });
expect(req.headers.get('Authorization')).toEqual('Bearer 1234');
return res(ctx.status(200));
}),
rest.post(
`${await discovery.getBaseUrl('notifications')}/`,
async (req, res, ctx) => {
const json = await req.json();
expect(json).toEqual({ ...body, origin: 'plugin-test' });
expect(req.headers.get('Authorization')).toBe(
mockCredentials.service.header({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'notifications',
}),
);
return res(ctx.status(200));
},
),
);
await expect(service.send(body)).resolves.not.toThrow();
await expect(service.send(body)).resolves.toBeUndefined();
});
it('should throw error if failing', async () => {
@@ -77,14 +79,24 @@ describe('DefaultNotificationService', () => {
};
server.use(
rest.post(`${mockBaseUrl}/`, async (req, res, ctx) => {
const json = await req.json();
expect(json).toEqual({ ...body, origin: 'plugin-test' });
expect(req.headers.get('Authorization')).toEqual('Bearer 1234');
return res(ctx.status(400));
}),
rest.post(
`${await discovery.getBaseUrl('notifications')}/`,
async (req, res, ctx) => {
const json = await req.json();
expect(json).toEqual({ ...body, origin: 'plugin-test' });
expect(req.headers.get('Authorization')).toBe(
mockCredentials.service.header({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'notifications',
}),
);
return res(ctx.status(400));
},
),
);
await expect(service.send(body)).rejects.toThrow(
'Request failed with status 400',
);
await expect(service.send(body)).rejects.toThrow();
});
});
});
@@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TokenManager } from '@backstage/backend-common';
import { NotificationService } from './NotificationService';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';
import { NotificationPayload } from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationServiceOptions = {
auth: AuthService;
discovery: DiscoveryService;
tokenManager: TokenManager;
pluginId: string;
};
@@ -44,22 +44,27 @@ export type NotificationSendOptions = {
export class DefaultNotificationService implements NotificationService {
private constructor(
private readonly discovery: DiscoveryService,
private readonly tokenManager: TokenManager,
private readonly auth: AuthService,
private readonly pluginId: string,
) {}
static create({
tokenManager,
discovery,
pluginId,
}: NotificationServiceOptions): DefaultNotificationService {
return new DefaultNotificationService(discovery, tokenManager, pluginId);
static create(
options: NotificationServiceOptions,
): DefaultNotificationService {
return new DefaultNotificationService(
options.discovery,
options.auth,
options.pluginId,
);
}
async send(notification: NotificationSendOptions): Promise<void> {
try {
const baseUrl = await this.discovery.getBaseUrl('notifications');
const { token } = await this.tokenManager.getToken();
const { token } = await this.auth.getPluginRequestToken({
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'notifications',
});
const response = await fetch(`${baseUrl}/`, {
method: 'POST',
body: JSON.stringify({
+1
View File
@@ -7664,6 +7664,7 @@ __metadata:
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"