diff --git a/packages/app/config.d.ts b/packages/app/config.d.ts
index 468e075846..56b41d945f 100644
--- a/packages/app/config.d.ts
+++ b/packages/app/config.d.ts
@@ -143,6 +143,36 @@ export interface Config {
module?: string;
importName?: string;
}[];
+ providerSettings?: {
+ title: string;
+ description: string;
+ provider: string;
+ }[];
+ scaffolderFieldExtensions?: {
+ module?: string;
+ importName?: string;
+ }[];
+ signInPage?: {
+ module?: string;
+ importName: string;
+ };
+ techdocsAddons?: {
+ module?: string;
+ importName?: string;
+ config?: {
+ props?: {
+ [key: string]: string;
+ };
+ };
+ }[];
+ themes?: {
+ module?: string;
+ id: string;
+ title: string;
+ variant: 'light' | 'dark';
+ icon: string;
+ importName?: string;
+ }[];
};
};
};
diff --git a/packages/app/src/components/AppBase/AppBase.tsx b/packages/app/src/components/AppBase/AppBase.tsx
index bc3d8f2a71..94b51293c7 100644
--- a/packages/app/src/components/AppBase/AppBase.tsx
+++ b/packages/app/src/components/AppBase/AppBase.tsx
@@ -37,6 +37,7 @@ const AppBase = () => {
AppRouter,
dynamicRoutes,
entityTabOverrides,
+ providerSettings,
scaffolderFieldExtensions,
} = useContext(DynamicRootContext);
@@ -123,7 +124,7 @@ const AppBase = () => {
}>
- {settingsPage}
+ {settingsPage(providerSettings)}
} />
} />
diff --git a/packages/app/src/components/DynamicRoot/DynamicRoot.tsx b/packages/app/src/components/DynamicRoot/DynamicRoot.tsx
index 1b603f769e..e26b34a3b5 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';
@@ -63,7 +67,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();
@@ -78,11 +84,13 @@ export const DynamicRoot = ({
menuItems,
entityTabs,
mountPoints,
+ providerSettings,
routeBindings,
routeBindingTargets,
scaffolderFieldExtensions,
techdocsAddons,
themes: pluginThemes,
+ signInPages,
} = extractDynamicConfig(dynamicPlugins);
const requiredModules = [
...pluginModules.map(({ scope, module }) => ({
@@ -117,6 +125,10 @@ export const DynamicRoot = ({
scope,
module,
})),
+ ...signInPages.map(({ scope, module }) => ({
+ scope,
+ module,
+ })),
];
const staticPlugins = Object.keys(staticPluginStore).reduce(
@@ -416,6 +428,25 @@ export const DynamicRoot = ({
[],
);
+ // the config allows for multiple sign-in pages, discover and use the first
+ // working instance but check all of them
+ const signInPage = signInPages
+ .map | undefined>(
+ ({ scope, module, importName }) => {
+ const candidate = allPlugins[scope]?.[module]?.[
+ importName
+ ] as React.ComponentType<{}>;
+ if (!candidate) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Plugin ${scope} is not configured properly: ${module}.${importName} not found, ignoring SignInPage: ${importName}`,
+ );
+ }
+ return candidate;
+ },
+ )
+ .find(candidate => candidate !== undefined);
+
if (!app.current) {
const filteredStaticThemes = themes.filter(
theme =>
@@ -437,7 +468,12 @@ export const DynamicRoot = ({
...remoteBackstagePlugins,
],
themes: [...filteredStaticThemes, ...dynamicThemeProviders],
- components: defaultAppComponents,
+ components: {
+ ...defaultAppComponents,
+ ...(signInPage && {
+ SignInPage: signInPage,
+ }),
+ } as Partial,
});
}
@@ -454,17 +490,17 @@ export const DynamicRoot = ({
dynamicRootConfig.techdocsAddons = techdocsAddonComponents;
// make the dynamic UI configuration available to DynamicRootContext consumers
- setComponents({
+ setComponentRegistry({
AppProvider: app.current.getProvider(),
AppRouter: app.current.getRouter(),
dynamicRoutes: dynamicRoutesComponents,
menuItems: dynamicRoutesMenuItems,
entityTabOverrides,
mountPoints: mountPointComponents,
+ providerSettings,
scaffolderFieldExtensions: scaffolderFieldExtensionComponents,
techdocsAddons: techdocsAddonComponents,
});
-
afterInit().then(({ default: Component }) => {
setChildComponent(() => Component);
});
@@ -480,17 +516,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/components/DynamicRoot/DynamicRootContext.tsx b/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx
index 5a1d6157f3..b8d04adc15 100644
--- a/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx
+++ b/packages/app/src/components/DynamicRoot/DynamicRootContext.tsx
@@ -128,11 +128,18 @@ export type TechdocsAddon = {
};
};
+export type ProviderSetting = {
+ title: string;
+ description: string;
+ provider: string;
+};
+
export type DynamicRootConfig = {
dynamicRoutes: ResolvedDynamicRoute[];
entityTabOverrides: EntityTabOverrides;
mountPoints: MountPoints;
menuItems: ResolvedMenuItem[];
+ providerSettings: ProviderSetting[];
scaffolderFieldExtensions: ScaffolderFieldExtension[];
techdocsAddons: TechdocsAddon[];
};
@@ -149,6 +156,7 @@ const DynamicRootContext = createContext({
entityTabOverrides: {},
mountPoints: {},
menuItems: [],
+ providerSettings: [],
scaffolderFieldExtensions: [],
techdocsAddons: [],
});
diff --git a/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx b/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx
index 7bc7888f84..d3c8ec95d8 100644
--- a/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx
+++ b/packages/app/src/components/DynamicRoot/ScalprumRoot.tsx
@@ -78,6 +78,7 @@ const ScalprumRoot = ({
mountPoints: {},
scaffolderFieldExtensions: [],
techdocsAddons: [],
+ providerSettings: [],
} as DynamicRootConfig,
};
return (
diff --git a/packages/app/src/components/UserSettings/SettingsPages.tsx b/packages/app/src/components/UserSettings/SettingsPages.tsx
index 2d04ae111e..dc63a09873 100644
--- a/packages/app/src/components/UserSettings/SettingsPages.tsx
+++ b/packages/app/src/components/UserSettings/SettingsPages.tsx
@@ -1,11 +1,83 @@
+import { ErrorBoundary } from '@backstage/core-components';
import {
+ AnyApiFactory,
+ ApiRef,
+ configApiRef,
+ ProfileInfoApi,
+ SessionApi,
+ useApi,
+ useApp,
+} from '@backstage/core-plugin-api';
+import {
+ DefaultProviderSettings,
+ ProviderSettingsItem,
SettingsLayout,
UserSettingsAuthProviders,
} from '@backstage/plugin-user-settings';
+import Star from '@mui/icons-material/Star';
+
+import { ProviderSetting } from '../DynamicRoot/DynamicRootContext';
import { GeneralPage } from './GeneralPage';
-export const settingsPage = (
+const DynamicProviderSettingsItem = ({
+ title,
+ description,
+ provider,
+}: {
+ title: string;
+ description: string;
+ provider: string;
+}) => {
+ const app = useApp();
+ // The provider API needs to be registered with the app
+ const apiRef = app
+ .getPlugins()
+ .flatMap(plugin => Array.from(plugin.getApis()))
+ .filter((api: AnyApiFactory) => api.api.id === provider)
+ .at(0)?.api;
+ if (!apiRef) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `No API factory found for provider ref "${provider}", hiding the related provider settings UI`,
+ );
+ return <>>;
+ }
+ return (
+ }
+ icon={Star}
+ />
+ );
+};
+
+const DynamicProviderSettings = ({
+ providerSettings,
+}: {
+ providerSettings: ProviderSetting[];
+}) => {
+ const configApi = useApi(configApiRef);
+ const providersConfig = configApi.getOptionalConfig('auth.providers');
+ const configuredProviders = providersConfig?.keys() || [];
+ return (
+ <>
+
+ {providerSettings.map(({ title, description, provider }) => (
+
+
+
+ ))}
+ >
+ );
+};
+
+export const settingsPage = (providerSettings: ProviderSetting[]) => (
@@ -14,7 +86,11 @@ export const settingsPage = (
path="auth-providers"
title="Authentication Providers"
>
-
+
+ }
+ />
);
diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts
index 31f89d4251..688f84ba25 100644
--- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts
+++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.test.ts
@@ -154,15 +154,30 @@ describe('extractDynamicConfig', () => {
menuItems: [],
mountPoints: [],
appIcons: [],
+ providerSettings: [],
routeBindingTargets: [],
apiFactories: [],
scaffolderFieldExtensions: [],
+ signInPages: [],
techdocsAddons: [],
themes: [],
});
});
it.each([
+ [
+ 'a SignInPage',
+ { signInPage: { importName: 'blah' } },
+ {
+ signInPages: [
+ {
+ importName: 'blah',
+ module: 'PluginRoot',
+ scope: 'janus-idp.plugin-foo',
+ },
+ ],
+ },
+ ],
[
'a dynamicRoute',
{ dynamicRoutes: [{ path: '/foo' }] },
@@ -496,6 +511,58 @@ describe('extractDynamicConfig', () => {
],
},
],
+ [
+ 'a providerSettings',
+ {
+ providerSettings: [
+ {
+ title: 'foo',
+ description: 'bar',
+ provider: 'foo.bar',
+ },
+ ],
+ },
+ {
+ providerSettings: [
+ {
+ title: 'foo',
+ description: 'bar',
+ provider: 'foo.bar',
+ },
+ ],
+ },
+ ],
+ [
+ 'multiple providerSettings',
+ {
+ providerSettings: [
+ {
+ title: 'foo1',
+ description: 'bar1',
+ provider: 'foo.bar1',
+ },
+ {
+ title: 'foo2',
+ description: 'bar2',
+ provider: 'foo.bar2',
+ },
+ ],
+ },
+ {
+ providerSettings: [
+ {
+ title: 'foo1',
+ description: 'bar1',
+ provider: 'foo.bar1',
+ },
+ {
+ title: 'foo2',
+ description: 'bar2',
+ provider: 'foo.bar2',
+ },
+ ],
+ },
+ ],
[
'a techdocs field extension',
{
@@ -569,8 +636,10 @@ describe('extractDynamicConfig', () => {
appIcons: [],
apiFactories: [],
scaffolderFieldExtensions: [],
+ signInPages: [],
techdocsAddons: [],
themes: [],
+ providerSettings: [],
...output,
});
});
diff --git a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
index 370aa803c9..7569c8a5e4 100644
--- a/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
+++ b/packages/app/src/utils/dynamicUI/extractDynamicConfig.ts
@@ -125,6 +125,18 @@ type ThemeEntry = {
importName: string;
};
+type SignInPageEntry = {
+ scope: string;
+ module: string;
+ importName: string;
+};
+
+type ProviderSetting = {
+ title: string;
+ description: string;
+ provider: string;
+};
+
type CustomProperties = {
pluginModule?: string;
dynamicRoutes?: (DynamicModuleEntry & {
@@ -142,7 +154,9 @@ type CustomProperties = {
mountPoints?: MountPoint[];
appIcons?: AppIcon[];
apiFactories?: ApiFactory[];
+ providerSettings?: ProviderSetting[];
scaffolderFieldExtensions?: ScaffolderFieldExtension[];
+ signInPage: SignInPageEntry;
techdocsAddons?: TechdocsAddon[];
themes?: ThemeEntry[];
};
@@ -163,9 +177,11 @@ type DynamicConfig = {
menuItems: MenuItem[];
entityTabs: EntityTabEntry[];
mountPoints: MountPoint[];
+ providerSettings: ProviderSetting[];
routeBindings: RouteBinding[];
routeBindingTargets: BindingTarget[];
scaffolderFieldExtensions: ScaffolderFieldExtension[];
+ signInPages: SignInPageEntry[];
techdocsAddons: TechdocsAddon[];
themes: ThemeEntry[];
};
@@ -188,10 +204,30 @@ function extractDynamicConfig(
mountPoints: [],
routeBindings: [],
routeBindingTargets: [],
+ providerSettings: [],
scaffolderFieldExtensions: [],
+ signInPages: [],
techdocsAddons: [],
themes: [],
};
+ config.signInPages = Object.entries(frontend).reduce(
+ (pluginSet, [scope, { signInPage }]) => {
+ if (!signInPage) {
+ return pluginSet;
+ }
+ const { importName, module } = signInPage;
+ if (!importName) {
+ return pluginSet;
+ }
+ pluginSet.push({
+ scope,
+ module: module ?? 'PluginRoot',
+ importName,
+ });
+ return pluginSet;
+ },
+ [],
+ );
config.pluginModules = Object.entries(frontend).reduce(
(pluginSet, [scope, customProperties]) => {
pluginSet.push({
@@ -330,6 +366,12 @@ function extractDynamicConfig(
},
[],
);
+ config.providerSettings = Object.entries(frontend).reduce(
+ (accProviderSettings, [_, { providerSettings = [] }]) => {
+ return [...accProviderSettings, ...providerSettings];
+ },
+ [],
+ );
return config;
}
diff --git a/packages/app/src/utils/test/TestRoot.tsx b/packages/app/src/utils/test/TestRoot.tsx
index 3d1709af7a..495d2d2146 100644
--- a/packages/app/src/utils/test/TestRoot.tsx
+++ b/packages/app/src/utils/test/TestRoot.tsx
@@ -42,6 +42,7 @@ const TestRoot = ({ children }: PropsWithChildren<{}>) => {
mountPoints: {},
entityTabOverrides: {},
scaffolderFieldExtensions: [],
+ providerSettings: [],
techdocsAddons: [],
}),
[current],
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'));