From 2325d1e337172563a072a6bd224dfabcab9a3a08 Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Wed, 8 Jan 2025 14:26:01 -0500 Subject: [PATCH] feat(app): dynamic authentication provider support This change adds support for loading authentication providers or modules from dynamic plugins. An environment variable ENABLE_AUTH_PROVIDER_MODULE_OVERRIDE controls whether or not the backend installs the default authentication provider module. When this override is enabled dynamic plugins can be used to supply custom authentication providers. This change also adds a "components" configuration for frontend dynamic plugins, which can be used to supply overrides for the AppComponents option. This is required for dynamic plugins to be able to provide a custom SignInPage component, for example: ``` frontend: my-plugin-package: components: - name: SignInPage module: PluginRoot importName: SignInPage ``` Where the named export SignInPage will be mapped to `components.SignInPage` when the frontend is initialized. Signed-off-by: Stan Lewis --- .../components/DynamicRoot/DynamicRoot.tsx | 47 +++++++++++++++---- .../dynamicUI/extractDynamicConfig.test.ts | 16 +++++++ .../utils/dynamicUI/extractDynamicConfig.ts | 27 ++++++++++- packages/backend/src/index.ts | 6 ++- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx index 55b3751221..127e0c7dd7 100644 --- a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx +++ b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx @@ -3,7 +3,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createApp } from '@backstage/app-defaults'; import { BackstageApp } from '@backstage/core-app-api'; -import { AnyApiFactory, BackstagePlugin } from '@backstage/core-plugin-api'; +import { + AnyApiFactory, + AppComponents, + BackstagePlugin, +} from '@backstage/core-plugin-api'; import { useThemes } from '@redhat-developer/red-hat-developer-hub-theme'; import { AppsConfig } from '@scalprum/core'; @@ -61,7 +65,9 @@ export const DynamicRoot = ({ React.ComponentType | undefined >(undefined); // registry of remote components loaded at bootstrap - const [components, setComponents] = useState(); + const [componentRegistry, setComponentRegistry] = useState< + ComponentRegistry | undefined + >(); const { initialized, pluginStore, api: scalprumApi } = useScalprum(); const themes = useThemes(); @@ -72,6 +78,7 @@ export const DynamicRoot = ({ pluginModules, apiFactories, appIcons, + components, dynamicRoutes, menuItems, entityTabs, @@ -86,6 +93,10 @@ export const DynamicRoot = ({ scope, module, })), + ...components.map(({ scope, module }) => ({ + scope, + module, + })), ...routeBindingTargets.map(({ scope, module }) => ({ scope, module, @@ -172,6 +183,23 @@ export const DynamicRoot = ({ ), ); + const appComponents = components.reduce>( + (componentMap, { scope, module, importName, name }) => { + if (typeof allPlugins[scope]?.[module]?.[importName] !== 'undefined') { + componentMap[name] = allPlugins[scope]?.[module]?.[ + importName + ] as React.ComponentType; + } else { + // eslint-disable-next-line no-console + console.warn( + `Plugin ${scope} is not configured properly: ${module}.${importName} not found, ignoring AppComponent: ${name}`, + ); + } + return componentMap; + }, + {}, + ); + let icons = Object.fromEntries( appIcons.reduce<[string, React.ComponentType<{}>][]>( (acc, { scope, module, importName, name }) => { @@ -408,7 +436,10 @@ export const DynamicRoot = ({ ...remoteBackstagePlugins, ], themes: [...filteredStaticThemes, ...dynamicThemeProviders], - components: defaultAppComponents, + components: { + ...defaultAppComponents, + ...appComponents, + } as Partial, }); } @@ -424,7 +455,7 @@ export const DynamicRoot = ({ scaffolderFieldExtensionComponents; // make the dynamic UI configuration available to DynamicRootContext consumers - setComponents({ + setComponentRegistry({ AppProvider: app.current.getProvider(), AppRouter: app.current.getRouter(), dynamicRoutes: dynamicRoutesComponents, @@ -449,17 +480,17 @@ export const DynamicRoot = ({ ]); useEffect(() => { - if (initialized && !components) { + if (initialized && !componentRegistry) { initializeRemoteModules(); } - }, [initialized, components, initializeRemoteModules]); + }, [initialized, componentRegistry, initializeRemoteModules]); - if (!initialized || !components) { + if (!initialized || !componentRegistry) { return ; } return ( - + {ChildComponent ? : } ); diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts index e24757b854..a6bd5922cf 100644 --- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts +++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts @@ -148,6 +148,7 @@ describe('extractDynamicConfig', () => { const config = extractDynamicConfig(source as DynamicPluginConfig); expect(config).toEqual({ pluginModules: [], + components: [], routeBindings: [], dynamicRoutes: [], entityTabs: [], @@ -162,6 +163,20 @@ describe('extractDynamicConfig', () => { }); it.each([ + [ + 'a component', + { components: [{ name: 'foo', importName: 'blah' }] }, + { + components: [ + { + importName: 'blah', + module: 'PluginRoot', + name: 'foo', + scope: 'janus-idp.plugin-foo', + }, + ], + }, + ], [ 'a dynamicRoute', { dynamicRoutes: [{ path: '/foo' }] }, @@ -506,6 +521,7 @@ describe('extractDynamicConfig', () => { scope: 'janus-idp.plugin-foo', }, ], + components: [], routeBindings: [], routeBindingTargets: [], dynamicRoutes: [], diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts index f89f8b22b9..dd430dbecd 100644 --- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts +++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts @@ -1,5 +1,5 @@ import { Entity } from '@backstage/catalog-model'; -import { ApiHolder } from '@backstage/core-plugin-api'; +import { ApiHolder, AppComponents } from '@backstage/core-plugin-api'; import { isKind } from '@backstage/plugin-catalog'; import { hasAnnotation, isType } from '../../components/catalog/utils'; @@ -116,8 +116,17 @@ type ThemeEntry = { importName: string; }; +type ComponentEntry = { + scope: string; + module: string; + id: string; + importName: string; + name: keyof AppComponents; +}; + type CustomProperties = { pluginModule?: string; + components: ComponentEntry[]; dynamicRoutes?: (DynamicModuleEntry & { importName?: string; module?: string; @@ -149,6 +158,7 @@ type DynamicConfig = { pluginModules: PluginModule[]; apiFactories: ApiFactory[]; appIcons: AppIcon[]; + components: ComponentEntry[]; dynamicRoutes: DynamicRoute[]; menuItems: MenuItem[]; entityTabs: EntityTabEntry[]; @@ -171,6 +181,7 @@ function extractDynamicConfig( pluginModules: [], apiFactories: [], appIcons: [], + components: [], dynamicRoutes: [], menuItems: [], entityTabs: [], @@ -190,6 +201,20 @@ function extractDynamicConfig( }, [], ); + config.components = Object.entries(frontend).reduce( + (pluginSet, [scope, customProperties]) => { + pluginSet.push( + ...(customProperties.components ?? []).map(component => ({ + ...component, + module: component.module ?? 'PluginRoot', + importName: component.importName ?? 'default', + scope, + })), + ); + return pluginSet; + }, + [], + ); config.dynamicRoutes = Object.entries(frontend).reduce( (pluginSet, [scope, customProperties]) => { pluginSet.push( diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 812796b107..31f0aa6414 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -91,7 +91,11 @@ backend.add(rbacDynamicPluginsProvider); backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); -backend.add(import('./modules/authProvidersModule')); +if (process.env.ENABLE_AUTH_PROVIDER_MODULE_OVERRIDE !== 'true') { + backend.add(import('./modules/authProvidersModule')); +} else { + staticLogger.info(`Default authentication provider module disabled`); +} backend.add(import('@internal/plugin-dynamic-plugins-info-backend')); backend.add(import('@internal/plugin-scalprum-backend'));