From a0e9c2590d934f50832a4d7db8a337e2409dc4ac Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Mon, 22 Jan 2024 13:42:22 +0100 Subject: [PATCH 1/3] feat(notifications): turn notifications-backend into a dynamic plugin --- plugins/notifications-backend/config.d.ts | 9 +++++++++ plugins/notifications-backend/package.json | 14 ++++++++++++-- .../notifications-backend/src/dynamic/alpha.ts | 16 ++++++++++++++++ .../notifications-backend/src/dynamic/index.ts | 11 +++++++++++ plugins/notifications-backend/src/index.ts | 1 + .../notifications-backend/src/service/auth.ts | 7 +++++-- .../notifications-backend/src/service/router.ts | 11 ++++++++++- .../notifications-backend/src/service/types.ts | 15 ++++++++------- 8 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 plugins/notifications-backend/config.d.ts create mode 100644 plugins/notifications-backend/src/dynamic/alpha.ts create mode 100644 plugins/notifications-backend/src/dynamic/index.ts diff --git a/plugins/notifications-backend/config.d.ts b/plugins/notifications-backend/config.d.ts new file mode 100644 index 0000000000..8124f310e3 --- /dev/null +++ b/plugins/notifications-backend/config.d.ts @@ -0,0 +1,9 @@ +export interface Config { + notifications?: { + /* Workaround for issues with external caller JWT token creation. + When following config option is not provided and the request "authentication" header is missing, the request is ALLOWED by default + When following option is present, the request must contain either a valid JWT token or that provided shared secret in the "notifications-secret" header + */ + externalCallerSecret?: string; + }; +} diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 1af3eec546..7f138f9bdf 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -21,17 +21,22 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack", "tsc": "tsc", - "openapi": "./scripts/openapi.sh" + "openapi": "./scripts/openapi.sh", + "export-dynamic": "janus-cli package export-dynamic-plugin" }, + "configSchema": "config.d.ts", "dependencies": { "@backstage/backend-common": "^0.19.8", "@backstage/backend-openapi-utils": "^0.1.0", "@backstage/catalog-client": "^1.4.5", "@backstage/config": "^1.1.1", "@backstage/errors": "^1.2.3", + "@backstage/backend-plugin-api": "^0.6.6", + "@backstage/backend-plugin-manager": "npm:@janus-idp/backend-plugin-manager@0.0.2-janus.5", "@backstage/plugin-auth-node": "^0.4.0", "@backstage/plugin-permission-common": "^0.7.9", "@backstage/plugin-permission-node": "^0.7.17", + "@backstage/plugin-scaffolder-node": "^0.2.6", "ajv-formats": "^2.1.1", "express": "^4.18.2", "express-promise-router": "^4.1.1", @@ -49,6 +54,7 @@ "@backstage/cli": "0.23.0", "@types/express": "*", "@types/supertest": "2.0.16", + "@janus-idp/cli": "1.4.7", "js-yaml-cli": "^0.6.0", "knex-mock-client": "2.0.0", "msw": "1.3.2", @@ -56,6 +62,10 @@ "supertest": "6.3.3" }, "files": [ - "dist" + "dist", + "dist-dynamic/*.*", + "dist-dynamic/dist/**", + "dist-dynamic/alpha/*", + "config.d.ts" ] } diff --git a/plugins/notifications-backend/src/dynamic/alpha.ts b/plugins/notifications-backend/src/dynamic/alpha.ts new file mode 100644 index 0000000000..4a63151a28 --- /dev/null +++ b/plugins/notifications-backend/src/dynamic/alpha.ts @@ -0,0 +1,16 @@ +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { BackendDynamicPluginInstaller } from '@backstage/backend-plugin-manager'; + +export const dynamicPluginInstaller: BackendDynamicPluginInstaller = { + kind: 'new', + install: createBackendModule({ + moduleId: 'scaffolder-backend-notifications', + pluginId: 'scaffolder', + register(env) { + env.registerInit({ + deps: {}, + async init() {}, + }); + }, + }), +}; diff --git a/plugins/notifications-backend/src/dynamic/index.ts b/plugins/notifications-backend/src/dynamic/index.ts new file mode 100644 index 0000000000..1441c6d7f3 --- /dev/null +++ b/plugins/notifications-backend/src/dynamic/index.ts @@ -0,0 +1,11 @@ +import { BackendDynamicPluginInstaller } from '@backstage/backend-plugin-manager'; + +import { createRouter } from '../service/router'; + +export const dynamicPluginInstaller: BackendDynamicPluginInstaller = { + kind: 'legacy', + router: { + pluginID: 'notifications', + createPlugin: createRouter, + }, +}; diff --git a/plugins/notifications-backend/src/index.ts b/plugins/notifications-backend/src/index.ts index 7d319394d7..ace981dc8c 100644 --- a/plugins/notifications-backend/src/index.ts +++ b/plugins/notifications-backend/src/index.ts @@ -1,2 +1,3 @@ export * from './service/router'; export * from './service/permissions'; +export * from './dynamic'; diff --git a/plugins/notifications-backend/src/service/auth.ts b/plugins/notifications-backend/src/service/auth.ts index 009fdcb1f1..7bb77272e9 100644 --- a/plugins/notifications-backend/src/service/auth.ts +++ b/plugins/notifications-backend/src/service/auth.ts @@ -13,7 +13,7 @@ import { RouterOptions } from './types'; export type GetLoggedInUserOptions = Pick< RouterOptions, - 'identity' | 'tokenManager' | 'externalCallerSecret' + 'identity' | 'tokenManager' | 'config' >; export type CheckUserPermission = GetLoggedInUserOptions & Pick; @@ -23,9 +23,12 @@ export type CheckUserPermission = GetLoggedInUserOptions & */ export const getLoggedInUser = async ( request: express.Request, - { identity, tokenManager, externalCallerSecret }: GetLoggedInUserOptions, + { identity, tokenManager, config }: GetLoggedInUserOptions, ): Promise => { const identityResponse = await identity.getIdentity({ request }); + const externalCallerSecret = config.getOptionalString( + 'notifications.externalCallerSecret', + ); // To properly set identity, see packages/backend/src/plugins/auth.ts or https://backstage.io/docs/auth/identity-resolver if (identityResponse) { diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index 8477433f16..79ab0c096d 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -1,3 +1,5 @@ +import { CatalogClient } from '@backstage/catalog-client'; + import { fullFormats } from 'ajv-formats/dist/formats'; import express from 'express'; import Router from 'express-promise-router'; @@ -23,7 +25,14 @@ import { RouterOptions } from './types'; export async function createRouter( options: RouterOptions, ): Promise { - const { logger, dbConfig, catalogClient } = options; + const { logger, database, discovery, config } = options; + + // workaround for creating the database when client is not sqlite + const existingDbClient = await database.getClient(); + existingDbClient.destroy(); + + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const dbConfig = config.getConfig('backend.database'); // create DB client and tables if (!dbConfig) { diff --git a/plugins/notifications-backend/src/service/types.ts b/plugins/notifications-backend/src/service/types.ts index c5049e025d..9d2a3e60e5 100644 --- a/plugins/notifications-backend/src/service/types.ts +++ b/plugins/notifications-backend/src/service/types.ts @@ -1,5 +1,8 @@ -import { TokenManager } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; +import { + PluginDatabaseManager, + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { Config } from '@backstage/config'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -8,14 +11,12 @@ import { Logger } from 'winston'; export interface RouterOptions { logger: Logger; - dbConfig: Config; - catalogClient: CatalogClient; identity: IdentityApi; permissions: PermissionEvaluator; tokenManager: TokenManager; - - // Workaround - see auth.ts - externalCallerSecret?: string; + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; + config: Config; } export type NotificationsFilterRequest = { From 3f1a938ab1fb3a11a0eb9339f48a3f3424b82c3d Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Mon, 22 Jan 2024 13:54:48 +0100 Subject: [PATCH 2/3] feat(notifications): turn notifications into a frontend dynamic plugin --- plugins/notifications/app-config.janus-idp.yaml | 14 ++++++++++++++ plugins/notifications/package.json | 12 +++++++++++- plugins/notifications/src/index.ts | 2 ++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 plugins/notifications/app-config.janus-idp.yaml diff --git a/plugins/notifications/app-config.janus-idp.yaml b/plugins/notifications/app-config.janus-idp.yaml new file mode 100644 index 0000000000..f6ccab6a93 --- /dev/null +++ b/plugins/notifications/app-config.janus-idp.yaml @@ -0,0 +1,14 @@ +dynamicPlugins: + frontend: + janus-idp.backstage-plugin-notifications: + appIcons: + - name: notificationsIcon + module: NotificationsPlugin + importName: NotificationsIcon + dynamicRoutes: + - path: /notifications + importName: NotificationsPage + module: NotificationsPlugin + menuItem: + icon: notificationsIcon + text: Notifications diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 2719000bc0..3d22c5560e 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -16,6 +16,7 @@ "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", + "export-dynamic": "janus-cli package export-dynamic-plugin", "lint": "backstage-cli package lint", "test": "backstage-cli package test --passWithNoTests --coverage", "clean": "backstage-cli package clean", @@ -45,6 +46,7 @@ "@backstage/core-app-api": "1.11.0", "@backstage/dev-utils": "1.0.22", "@backstage/test-utils": "^1.4.4", + "@janus-idp/cli": "1.4.7", "@openapitools/openapi-generator-cli": "^2.7.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^12.1.5", @@ -52,8 +54,16 @@ "@types/node": "*", "msw": "1.3.2" }, + "scalprum": { + "name": "janus-idp.backstage-plugin-notifications", + "exposedModules": { + "NotificationsPlugin": "./src/index.ts" + } + }, "files": [ - "dist" + "dist", + "dist-scalprum", + "app-config.janus-idp.yaml" ], "repository": "github:janus-idp/backstage-plugins", "keywords": [ diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index b6df883e3b..58b3ceeb4c 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -17,3 +17,5 @@ export { NOTIFICATIONS_ROUTE } from './constants'; // selected components for export export { NotificationsSidebarItem } from './components/NotificationsSidebarItem'; export { usePollingEffect } from './components/usePollingEffect'; + +export { default as NotificationsIcon } from '@material-ui/icons/Notifications'; From 7267c2358a22516156924ba1fec522bc5d175d77 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Tue, 23 Jan 2024 13:41:35 +0100 Subject: [PATCH 3/3] chore(notifications): update README for dynamic plugins --- plugins/notifications-backend/README.md | 47 +++++++++++++------------ plugins/notifications/README.md | 7 ++++ 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md index 19642fc9d6..fe532ea2f0 100644 --- a/plugins/notifications-backend/README.md +++ b/plugins/notifications-backend/README.md @@ -4,13 +4,23 @@ This Backstage backend plugin provides REST API endpoint for the notifications. It's backed by a relational database, so far tested with PostgreSQL. +## Deploying as a dynamic plugin + +The notifications backend plugin can be loaded either as a static or a dynamic plugin. + +To install it as a dynamic plugin, please follow instructions here: https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md#installing-a-dynamic-plugin-package-in-the-showcase + +To install it as a static plugin, several steps are required as described below. + +In any case, do not miss the info about configuration and especially about creating entities in Catalog as described below. + ## Getting started The plugin uses a relational database to persist messages, it has been tested with the SQLite and PostgreSQL. Upon backend's plugin start, the `backstage_plugin_notifications` database and its tables are created automatically. -### Optional: PostgreSQL +### Optional: PostgreSQL setup **To use the Backstage's default SQLite, no specific configuration is needed.** @@ -58,6 +68,8 @@ If PostgreSQL is used, additional configuration in the in the `app-config.yaml` store: memory ``` +## Deploy as a static plugin + ### Add NPM dependency ``` @@ -80,25 +92,14 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - // workaround for creating the database when client is not sqlite - const dbClient = await env.database.getClient() - dbClient.destroy() - - const catalogClient = new CatalogClient({ discoveryApi: env.discovery }); - const dbConfig = env.config.getConfig('backend.database'); - // Following is optional - const externalCallerSecret = env.config.getOptionalString( - 'notifications.externalCallerSecret', - ); - return await createRouter({ identity: env.identity, logger: env.logger, permissions: env.permissions, tokenManager: env.tokenManager, - dbConfig, - catalogClient, - externalCallerSecret, + database: env.database, + discovery: env.discovery, + config: env.config, }); } ``` @@ -120,7 +121,7 @@ const notificationsEnv = useHotMemoize(module, () => apiRouter.use('/notifications', await notifications(notificationsEnv)); ``` -### Configure +## Configure #### Optional: Plugin's configuration @@ -148,7 +149,7 @@ Notes: - The `externalCallerSecret` is an workaround, exclusive use of JWT tokens will probably be required in the future. - Sharing the same shared secret with the "auth.secret" option is not recommended. -#### Authentication +## Authentication Please refer https://backstage.io/docs/auth/ to set-up authentication. @@ -159,13 +160,13 @@ Refer https://backstage.io/docs/auth/identity-resolver for details. For the purpose of development, there is `users.yaml` listing example data created. -#### Authorization +## Authorization Every service endpoint is guarded by a permission check, enabled by default. It is up to particular deployment to provide corresponding permission policies based on https://backstage.io/docs/permissions/writing-a-policy. To register your permission policies, refer https://backstage.io/docs/permissions/getting-started#integrating-the-permission-framework-with-your-backstage-instance. -#### Service-to-service and External Calls +### Service-to-service and External Calls The notification-backend is expected to be called by FE plugins (including the Notifications frontend-end plugin), other backend plugins or external services. @@ -174,9 +175,9 @@ To configure those two flows, refer - https://backstage.io/docs/auth/service-to-service-auth. - https://backstage.io/docs/auth/service-to-service-auth#usage-in-external-callers -#### Catalog +### Important: User entities in Catalog -The notifications require target users or groups (as receivers) to be listed in the Catalog. +_The notifications require target users or groups (as receivers) to be listed in the Catalog._ As an example how to do it, add following to the config: @@ -186,12 +187,12 @@ catalog: entityFilename: catalog-info.yaml pullRequestBranchName: backstage-integration rules: - # *** Here is new change: + # *** Here is a new change: - allow: [Component, System, API, Resource, Location, User, Group] locations: # Local example data, file locations are relative to the backend process, typically `packages/backend` - type: file - # *** Here is new change, referes to a file stored in the root of the Backstage: + # *** Here is a new change, referes to a file stored in the root of the Backstage: target: ../../users.yaml ``` diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md index d2bac3f656..4efe8767c1 100644 --- a/plugins/notifications/README.md +++ b/plugins/notifications/README.md @@ -13,6 +13,13 @@ This Backstage front-end plugin provides: Have `@janus-idp/plugin-notifications-backend` installed and running. +### Installing as a dynamic plugin? + +The sections below are relevant for static plugins. If the plugin is expected to be installed as a dynamic one: + +- follow https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md#installing-a-dynamic-plugin-package-in-the-showcase +- add content of `app-config.janus-idp.yaml` into `app-config.local.yaml`. + ### Add NPM dependency ```