feat: move notifications origin resolving to backend

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-02-29 17:49:48 +02:00
parent 6ea413c7e8
commit a790a3dfa0
11 changed files with 62 additions and 82 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-notifications-backend': patch
'@backstage/plugin-notifications-common': patch
'@backstage/plugin-notifications-node': patch
---
Move notification origin resolving to backend with new auth
+3 -6
View File
@@ -61,22 +61,20 @@ export const notificationsPlugin = createBackendPlugin({
deps: {
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
userInfo: coreServices.userInfo,
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
identity: coreServices.identity,
database: coreServices.database,
tokenManager: coreServices.tokenManager,
discovery: coreServices.discovery,
signals: signalService,
},
async init({
auth,
httpAuth,
userInfo,
httpRouter,
logger,
identity,
database,
tokenManager,
discovery,
signals,
}) {
@@ -84,10 +82,9 @@ export const notificationsPlugin = createBackendPlugin({
await createRouter({
auth,
httpAuth,
userInfo,
logger,
identity,
database,
tokenManager,
discovery,
signalService: signals,
processors: processingExtensions.processors,
@@ -17,16 +17,14 @@ import {
DatabaseManager,
getVoidLogger,
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
} from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { ConfigReader } from '@backstage/config';
import { SignalService } from '@backstage/plugin-signals-node';
import { mockServices } from '@backstage/backend-test-utils';
function createDatabase(): PluginDatabaseManager {
return DatabaseManager.fromConfig(
@@ -44,40 +42,24 @@ function createDatabase(): PluginDatabaseManager {
describe('createRouter', () => {
let app: express.Express;
const identityMock: IdentityApi = {
async getIdentity() {
return {
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'user:default/guest',
},
token: 'no-token',
};
},
};
const mockedTokenManager: jest.Mocked<TokenManager> = {
getToken: jest.fn(),
authenticate: jest.fn(),
};
const discovery: jest.Mocked<PluginEndpointDiscovery> = {
getBaseUrl: jest.fn(),
getExternalBaseUrl: jest.fn(),
};
const signalService: jest.Mocked<SignalService> = {
publish: jest.fn(),
};
const discovery = mockServices.discovery();
const userInfo = mockServices.userInfo();
const httpAuth = mockServices.httpAuth();
const auth = mockServices.auth();
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
identity: identityMock,
database: createDatabase(),
tokenManager: mockedTokenManager,
discovery,
signalService,
userInfo,
httpAuth,
auth,
});
app = express().use(router);
});
@@ -13,15 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createLegacyAuthAdapters,
errorHandler,
PluginDatabaseManager,
TokenManager,
} from '@backstage/backend-common';
import { errorHandler, PluginDatabaseManager } from '@backstage/backend-common';
import express, { Request } from 'express';
import Router from 'express-promise-router';
import { IdentityApi } from '@backstage/plugin-auth-node';
import {
DatabaseNotificationsStore,
NotificationGetOptions,
@@ -36,12 +30,13 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { NotificationProcessor } from '@backstage/plugin-notifications-node';
import { AuthenticationError, InputError } from '@backstage/errors';
import { InputError } from '@backstage/errors';
import {
AuthService,
DiscoveryService,
HttpAuthService,
LoggerService,
UserInfoService,
} from '@backstage/backend-plugin-api';
import { SignalService } from '@backstage/plugin-signals-node';
import {
@@ -53,15 +48,14 @@ import {
/** @internal */
export interface RouterOptions {
logger: LoggerService;
identity: IdentityApi;
database: PluginDatabaseManager;
tokenManager: TokenManager;
discovery: DiscoveryService;
auth: AuthService;
httpAuth: HttpAuthService;
userInfo: UserInfoService;
signalService?: SignalService;
catalog?: CatalogApi;
processors?: NotificationProcessor[];
auth?: AuthService;
httpAuth?: HttpAuthService;
}
/** @internal */
@@ -71,7 +65,9 @@ export async function createRouter(
const {
logger,
database,
identity,
auth,
httpAuth,
userInfo,
discovery,
catalog,
processors,
@@ -82,14 +78,10 @@ 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) {
throw new AuthenticationError();
}
return user.identity.userEntityRef;
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
const info = await userInfo.getUserInfo(credentials);
return info.userEntityRef;
};
const getUsersForEntityRef = async (
@@ -277,18 +269,16 @@ export async function createRouter(
});
// Add new notification
// Allowed only for service-to-service authentication, uses `getUsersForEntityRef` to retrieve recipients for
// specific entity reference
router.post('/', async (req, res) => {
const { recipients, origin, payload } = req.body;
const { recipients, payload } = req.body;
const notifications = [];
let users = [];
await httpAuth.credentials(req, { allow: ['service'] });
const credentials = await httpAuth.credentials(req, { allow: ['service'] });
const { title, link, scope } = payload;
const { title, scope } = payload;
if (!recipients || !title || !origin || !link) {
if (!recipients || !title) {
logger.error(`Invalid notification request received`);
throw new InputError();
}
@@ -305,6 +295,7 @@ export async function createRouter(
throw new InputError();
}
const origin = credentials.principal.subject;
const baseNotification: Omit<Notification, 'id' | 'user'> = {
payload: {
...payload,
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import {
createLegacyAuthAdapters,
createServiceBuilder,
HostDiscovery,
loadBackendConfig,
@@ -37,6 +38,11 @@ import {
EventsService,
EventsServiceSubscribeOptions,
} from '@backstage/plugin-events-node';
import {
AuthService,
HttpAuthService,
UserInfoService,
} from '@backstage/backend-plugin-api';
export interface ServerOptions {
port: number;
@@ -107,15 +113,25 @@ export async function startStandaloneServer(
};
const signalService = DefaultSignalService.create({ events });
// TODO: Move to use services instead this hack
const { auth, httpAuth, userInfo } = createLegacyAuthAdapters<
any,
{ auth: AuthService; httpAuth: HttpAuthService; userInfo: UserInfoService }
>({
identity: identityMock,
tokenManager,
discovery,
});
const router = await createRouter({
logger,
identity: identityMock,
database: dbMock,
catalog: catalogApi,
discovery,
tokenManager,
signalService,
auth,
httpAuth,
userInfo,
});
let service = createServiceBuilder(module)
+1 -1
View File
@@ -27,7 +27,7 @@ export type NotificationPayload = {
title: string;
description?: string;
link?: string;
severity: NotificationSeverity;
severity?: NotificationSeverity;
topic?: string;
scope?: string;
icon?: string;
+1 -1
View File
@@ -24,7 +24,7 @@ export type NotificationPayload = {
link?: string;
// TODO: Add support for additional links
// additionalLinks?: string[];
severity: NotificationSeverity;
severity?: NotificationSeverity;
topic?: string;
scope?: string;
icon?: string;
-1
View File
@@ -51,7 +51,6 @@ export const notificationService: ServiceRef<NotificationService, 'plugin'>;
export type NotificationServiceOptions = {
auth: AuthService;
discovery: DiscoveryService;
pluginId: string;
};
// @public (undocumented)
+1 -3
View File
@@ -31,13 +31,11 @@ export const notificationService = createServiceRef<NotificationService>({
deps: {
auth: coreServices.auth,
discovery: coreServices.discovery,
pluginMetadata: coreServices.pluginMetadata,
},
factory({ auth, discovery, pluginMetadata }) {
factory({ auth, discovery }) {
return DefaultNotificationService.create({
auth,
discovery,
pluginId: pluginMetadata.getId(),
});
},
}),
@@ -42,7 +42,6 @@ describe('DefaultNotificationService', () => {
service = DefaultNotificationService.create({
auth,
discovery,
pluginId: 'test',
});
});
@@ -58,7 +57,7 @@ describe('DefaultNotificationService', () => {
`${await discovery.getBaseUrl('notifications')}/`,
async (req, res, ctx) => {
const json = await req.json();
expect(json).toEqual({ ...body, origin: 'plugin-test' });
expect(json).toEqual(body);
expect(req.headers.get('Authorization')).toBe(
mockCredentials.service.header({
onBehalfOf: await auth.getOwnServiceCredentials(),
@@ -83,7 +82,7 @@ describe('DefaultNotificationService', () => {
`${await discovery.getBaseUrl('notifications')}/`,
async (req, res, ctx) => {
const json = await req.json();
expect(json).toEqual({ ...body, origin: 'plugin-test' });
expect(json).toEqual(body);
expect(req.headers.get('Authorization')).toBe(
mockCredentials.service.header({
onBehalfOf: await auth.getOwnServiceCredentials(),
@@ -22,7 +22,6 @@ import { NotificationPayload } from '@backstage/plugin-notifications-common';
export type NotificationServiceOptions = {
auth: AuthService;
discovery: DiscoveryService;
pluginId: string;
};
/** @public */
@@ -45,17 +44,12 @@ export class DefaultNotificationService implements NotificationService {
private constructor(
private readonly discovery: DiscoveryService,
private readonly auth: AuthService,
private readonly pluginId: string,
) {}
static create(
options: NotificationServiceOptions,
): DefaultNotificationService {
return new DefaultNotificationService(
options.discovery,
options.auth,
options.pluginId,
);
return new DefaultNotificationService(options.discovery, options.auth);
}
async send(notification: NotificationSendOptions): Promise<void> {
@@ -65,13 +59,10 @@ export class DefaultNotificationService implements NotificationService {
onBehalfOf: await this.auth.getOwnServiceCredentials(),
targetPluginId: 'notifications',
});
const response = await fetch(`${baseUrl}/`, {
method: 'POST',
body: JSON.stringify({
...notification,
// TODO: Should retrieve this in the backend from service auth instead
origin: `plugin-${this.pluginId}`,
}),
body: JSON.stringify(notification),
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',