mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SSO: Display provider list (#78472)
* Load providers * Display providers * Rename * Remove redundant styles * Update Grid import * Return data in camelCase from the OAuth fb strategy * Update cards and remove empty state * Add comment * Add feature toggle * Update betterer * Add empty state * Fix configPath * Update betterer * Revert backend changes * Remove newline * Enable auth routes --------- Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
parent
5c451cbb7d
commit
1141dd62ab
@ -2571,27 +2571,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||
],
|
||||
"public/app/features/auth-config/AuthConfigPage.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "5"]
|
||||
],
|
||||
"public/app/features/auth-config/components/ConfigureAuthCTA.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/features/auth-config/components/ProviderCard.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "6"]
|
||||
],
|
||||
"public/app/features/canvas/element.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
@ -80,7 +80,7 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
|
||||
})
|
||||
}
|
||||
|
||||
if authConfigUIAvailable && hasAccess(evalAuthenticationSettings()) {
|
||||
if (authConfigUIAvailable && hasAccess(evalAuthenticationSettings())) || s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi) {
|
||||
configNodes = append(configNodes, &navtree.NavLink{
|
||||
Text: "Authentication",
|
||||
Id: "authentication",
|
||||
|
@ -1,31 +1,27 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { JSX, useEffect } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Grid, TextLink } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import ConfigureAuthCTA from './components/ConfigureAuthCTA';
|
||||
import { ProviderCard } from './components/ProviderCard';
|
||||
import { loadSettings } from './state/actions';
|
||||
import { AuthProviderInfo } from './types';
|
||||
import { getProviderUrl } from './utils';
|
||||
|
||||
import { getRegisteredAuthProviders } from '.';
|
||||
import { getRegisteredAuthProviders } from './index';
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
const { isLoading, providerStatuses } = state.authConfig;
|
||||
const { isLoading, providerStatuses, providers } = state.authConfig;
|
||||
return {
|
||||
isLoading,
|
||||
providerStatuses,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
|
||||
@ -35,125 +31,65 @@ const mapDispatchToProps = {
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSettings }: Props): JSX.Element => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
export const AuthConfigPageUnconnected = ({
|
||||
providerStatuses,
|
||||
isLoading,
|
||||
loadSettings,
|
||||
providers,
|
||||
}: Props): JSX.Element => {
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
const authProviders = getRegisteredAuthProviders();
|
||||
const enabledProviders = authProviders.filter((p) => providerStatuses[p.id]?.enabled);
|
||||
const configuresProviders = authProviders.filter(
|
||||
(p) => providerStatuses[p.id]?.configured && !providerStatuses[p.id]?.enabled
|
||||
);
|
||||
const availableProviders = authProviders.filter(
|
||||
(p) => !providerStatuses[p.id]?.enabled && !providerStatuses[p.id]?.configured && !providerStatuses[p.id]?.hide
|
||||
);
|
||||
const firstAvailableProvider = availableProviders?.length ? availableProviders[0] : null;
|
||||
|
||||
{
|
||||
/* TODO: make generic for the provider of the configuration or make the documentation point to a collection of all our providers */
|
||||
}
|
||||
const docsLink = (
|
||||
<a
|
||||
className="external-link"
|
||||
href="https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication/saml-ui/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
documentation.
|
||||
</a>
|
||||
);
|
||||
|
||||
const subTitle = <span>Manage your auth settings and configure single sign-on. Find out more in our {docsLink}</span>;
|
||||
|
||||
const onCTAClick = () => {
|
||||
reportInteraction('authentication_ui_created', { provider: firstAvailableProvider?.type });
|
||||
};
|
||||
const onProviderCardClick = (provider: AuthProviderInfo) => {
|
||||
reportInteraction('authentication_ui_provider_clicked', { provider: provider.type });
|
||||
const availableProviders = authProviders.filter((p) => !providerStatuses[p.id]?.hide);
|
||||
const onProviderCardClick = (providerType: string) => {
|
||||
reportInteraction('authentication_ui_provider_clicked', { provider: providerType });
|
||||
};
|
||||
|
||||
const providerList = availableProviders.length
|
||||
? [
|
||||
...availableProviders.map((p) => ({
|
||||
provider: p.id,
|
||||
settings: { ...providerStatuses[p.id], configPath: p.configPath, type: p.type },
|
||||
})),
|
||||
...providers,
|
||||
]
|
||||
: providers;
|
||||
return (
|
||||
<Page navId="authentication" subTitle={subTitle}>
|
||||
<Page
|
||||
navId="authentication"
|
||||
subTitle={
|
||||
<>
|
||||
Manage your auth settings and configure single sign-on. Find out more in our{' '}
|
||||
<TextLink href="https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication">
|
||||
documentation
|
||||
</TextLink>
|
||||
.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
<h3 className={styles.sectionHeader}>Configured authentication</h3>
|
||||
{!!enabledProviders?.length && (
|
||||
<div className={styles.cardsContainer}>
|
||||
{enabledProviders.map((provider) => (
|
||||
{!providerList.length ? (
|
||||
<ConfigureAuthCTA />
|
||||
) : (
|
||||
<Grid gap={3} minColumnWidth={34}>
|
||||
{providerList.map(({ provider, settings }) => (
|
||||
<ProviderCard
|
||||
key={provider.id}
|
||||
providerId={provider.id}
|
||||
displayName={providerStatuses[provider.id]?.name || provider.displayName}
|
||||
authType={provider.protocol}
|
||||
enabled={providerStatuses[provider.id]?.enabled}
|
||||
configPath={provider.configPath}
|
||||
key={provider}
|
||||
authType={settings.type || 'OAuth'}
|
||||
providerId={provider}
|
||||
displayName={provider}
|
||||
enabled={settings.enabled}
|
||||
onClick={() => onProviderCardClick(provider)}
|
||||
configPath={settings.configPath}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!enabledProviders?.length && firstAvailableProvider && !isEmpty(providerStatuses) && (
|
||||
<ConfigureAuthCTA
|
||||
title={`You have no ${firstAvailableProvider.type} configuration created at the moment`}
|
||||
buttonIcon="plus-circle"
|
||||
buttonLink={getProviderUrl(firstAvailableProvider)}
|
||||
buttonTitle={`Configure ${firstAvailableProvider.type}`}
|
||||
onClick={onCTAClick}
|
||||
/>
|
||||
)}
|
||||
{!!configuresProviders?.length && (
|
||||
<div className={styles.cardsContainer}>
|
||||
{configuresProviders.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.id}
|
||||
providerId={provider.id}
|
||||
displayName={providerStatuses[provider.id]?.name || provider.displayName}
|
||||
authType={provider.protocol}
|
||||
enabled={providerStatuses[provider.id]?.enabled}
|
||||
configPath={provider.configPath}
|
||||
onClick={() => onProviderCardClick(provider)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
cardsContainer: css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(288px, 1fr));
|
||||
gap: ${theme.spacing(3)};
|
||||
margin-bottom: ${theme.spacing(3)};
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
sectionHeader: css`
|
||||
margin-bottom: ${theme.spacing(3)};
|
||||
`,
|
||||
settingsSection: css`
|
||||
margin-top: ${theme.spacing(4)};
|
||||
`,
|
||||
settingName: css`
|
||||
padding-left: 25px;
|
||||
`,
|
||||
doclink: css`
|
||||
padding-bottom: 5px;
|
||||
padding-top: -5px;
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
a {
|
||||
color: ${theme.colors.info.name}; // use theme link color or any other color
|
||||
text-decoration: underline; // underline or none, as you prefer
|
||||
}
|
||||
`,
|
||||
settingValue: css`
|
||||
white-space: break-spaces;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export default connector(AuthConfigPageUnconnected);
|
||||
|
@ -2,57 +2,40 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { CallToActionCard, IconName, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { Icon, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
buttonIcon: IconName;
|
||||
buttonLink?: string;
|
||||
buttonTitle: string;
|
||||
buttonDisabled?: boolean;
|
||||
description?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
export interface Props {}
|
||||
|
||||
const ConfigureAuthCTA: React.FunctionComponent<Props> = ({
|
||||
title,
|
||||
buttonIcon,
|
||||
buttonLink,
|
||||
buttonTitle,
|
||||
buttonDisabled,
|
||||
description,
|
||||
onClick,
|
||||
}) => {
|
||||
const ConfigureAuthCTA: React.FunctionComponent<Props> = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const footer = description ? <span key="proTipFooter">{description}</span> : '';
|
||||
const ctaElementClassName = !description ? styles.button : '';
|
||||
|
||||
const ctaElement = (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
href={buttonLink}
|
||||
icon={buttonIcon}
|
||||
className={ctaElementClassName}
|
||||
data-testid={selectors.components.CallToActionCard.buttonV2(buttonTitle)}
|
||||
disabled={buttonDisabled}
|
||||
onClick={() => onClick && onClick()}
|
||||
>
|
||||
{buttonTitle}
|
||||
</LinkButton>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Stack gap={1} alignItems={'center'}>
|
||||
<Icon name={'cog'} />
|
||||
<Text>Configuration required</Text>
|
||||
</Stack>
|
||||
<Text variant={'bodySmall'} color={'secondary'}>
|
||||
You have no authentication configuration created at the moment.
|
||||
</Text>
|
||||
<TextLink href={'https://grafana.com/docs/grafana/latest/auth/overview/'} external>
|
||||
Refer to the documentation on how to configure authentication
|
||||
</TextLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <CallToActionCard className={styles.cta} message={title} footer={footer} callToActionElement={ctaElement} />;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
cta: css`
|
||||
text-align: center;
|
||||
`,
|
||||
button: css`
|
||||
margin-bottom: ${theme.spacing(2.5)};
|
||||
`,
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
padding: theme.spacing(3),
|
||||
width: 'max-content',
|
||||
margin: theme.spacing(3, 'auto'),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, Card, useStyles2, Icon } from '@grafana/ui';
|
||||
import { IconName, isIconName } from '@grafana/data';
|
||||
import { Badge, Card, Icon } from '@grafana/ui';
|
||||
|
||||
import { BASE_PATH } from '../constants';
|
||||
|
||||
export const LOGO_SIZE = '48px';
|
||||
import { getProviderUrl } from '../utils';
|
||||
|
||||
type Props = {
|
||||
providerId: string;
|
||||
@ -14,79 +11,36 @@ type Props = {
|
||||
enabled: boolean;
|
||||
configPath?: string;
|
||||
authType?: string;
|
||||
badges?: JSX.Element[];
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function ProviderCard({ providerId, displayName, enabled, configPath, authType, badges, onClick }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
configPath = BASE_PATH + (configPath || providerId);
|
||||
// TODO Remove when this is available from API
|
||||
const UIMap: Record<string, [IconName, string]> = {
|
||||
github: ['github', 'GitHub'],
|
||||
gitlab: ['gitlab', 'GitLab'],
|
||||
google: ['google', 'Google'],
|
||||
generic_oauth: ['lock', 'Generic OAuth'],
|
||||
grafana_com: ['grafana', 'Grafana.com'],
|
||||
azuread: ['microsoft', 'Azure AD'],
|
||||
okta: ['okta', 'Okta'],
|
||||
};
|
||||
|
||||
export function ProviderCard({ providerId, enabled, configPath, authType, onClick }: Props) {
|
||||
//@ts-expect-error
|
||||
const url = getProviderUrl({ configPath, id: providerId });
|
||||
const [iconName, displayName] = UIMap[providerId] || ['lock', providerId.toUpperCase()];
|
||||
return (
|
||||
<Card href={configPath} className={styles.container} onClick={() => onClick && onClick()}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.smallText}>{authType}</span>
|
||||
<span className={styles.name}>{displayName}</span>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.badgeContainer}>
|
||||
{enabled ? <Badge text="Enabled" color="green" icon="check" /> : <Badge text="Not enabled" color="blue" />}
|
||||
</div>
|
||||
<span className={styles.edit}>
|
||||
Edit
|
||||
<Icon color="blue" name={'arrow-right'} size="sm" />
|
||||
</span>
|
||||
</div>
|
||||
<Card href={url} onClick={onClick}>
|
||||
<Card.Heading>{displayName}</Card.Heading>
|
||||
<Card.Meta>{authType}</Card.Meta>
|
||||
{isIconName(iconName) && (
|
||||
<Card.Figure>
|
||||
<Icon name={iconName} size={'xxxl'} />
|
||||
</Card.Figure>
|
||||
)}
|
||||
<Card.Actions>
|
||||
<Badge text={enabled ? 'Enabled' : 'Not enabled'} color={enabled ? 'green' : 'blue'} />
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
min-height: ${theme.spacing(18)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
border-radius: ${theme.spacing(0.5)};
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
`,
|
||||
header: css`
|
||||
margin-top: -${theme.spacing(2)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: flex-start;
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
footer: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`,
|
||||
name: css`
|
||||
align-self: flex-start;
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
color: ${theme.colors.text.primary};
|
||||
margin: 0;
|
||||
margin-top: ${theme.spacing(-1)};
|
||||
`,
|
||||
smallText: css`
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
color: ${theme.colors.text.secondary};
|
||||
padding: ${theme.spacing(1)} 0; // Add some padding
|
||||
max-width: 90%; // Add a max-width to prevent text from stretching too wide
|
||||
`,
|
||||
badgeContainer: css`
|
||||
display: flex;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
edit: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${theme.colors.text.link};
|
||||
gap: ${theme.spacing(0.5)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,18 +1,27 @@
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||
import { config, getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, Settings, ThunkResult, UpdateSettingsQuery } from 'app/types';
|
||||
|
||||
import { getAuthProviderStatus, getRegisteredAuthProviders } from '..';
|
||||
import { getAuthProviderStatus, getRegisteredAuthProviders, SSOProvider } from '..';
|
||||
import { AuthProviderStatus, SettingsError } from '../types';
|
||||
|
||||
import { loadingBegin, loadingEnd, providerStatusesLoaded, resetError, setError, settingsUpdated } from './reducers';
|
||||
import {
|
||||
loadingBegin,
|
||||
loadingEnd,
|
||||
providersLoaded,
|
||||
providerStatusesLoaded,
|
||||
resetError,
|
||||
setError,
|
||||
settingsUpdated,
|
||||
} from './reducers';
|
||||
|
||||
export function loadSettings(): ThunkResult<Promise<Settings>> {
|
||||
return async (dispatch) => {
|
||||
if (contextSrv.hasPermission(AccessControlAction.SettingsRead)) {
|
||||
dispatch(loadingBegin());
|
||||
dispatch(loadProviders());
|
||||
const result = await getBackendSrv().get('/api/admin/settings');
|
||||
dispatch(settingsUpdated(result));
|
||||
await dispatch(loadProviderStatuses());
|
||||
@ -22,6 +31,17 @@ export function loadSettings(): ThunkResult<Promise<Settings>> {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadProviders(): ThunkResult<Promise<SSOProvider[]>> {
|
||||
return async (dispatch) => {
|
||||
if (!config.featureToggles.ssoSettingsApi) {
|
||||
return [];
|
||||
}
|
||||
const result = await getBackendSrv().get('/api/v1/sso-settings');
|
||||
dispatch(providersLoaded(result));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function loadProviderStatuses(): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const registeredProviders = getRegisteredAuthProviders();
|
||||
@ -33,8 +53,7 @@ export function loadProviderStatuses(): ThunkResult<void> {
|
||||
const statuses = await Promise.all(getStatusPromises);
|
||||
for (let i = 0; i < registeredProviders.length; i++) {
|
||||
const provider = registeredProviders[i];
|
||||
const status = statuses[i];
|
||||
providerStatuses[provider.id] = status;
|
||||
providerStatuses[provider.id] = statuses[i];
|
||||
}
|
||||
dispatch(providerStatusesLoaded(providerStatuses));
|
||||
};
|
||||
|
@ -2,12 +2,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Settings } from 'app/types';
|
||||
|
||||
import { SettingsError, AuthProviderStatus, AuthConfigState } from '../types';
|
||||
import { SettingsError, AuthProviderStatus, AuthConfigState, SSOProvider } from '../types';
|
||||
|
||||
export const initialState: AuthConfigState = {
|
||||
settings: {},
|
||||
providerStatuses: {},
|
||||
isLoading: false,
|
||||
providers: [],
|
||||
};
|
||||
|
||||
const authConfigSlice = createSlice({
|
||||
@ -38,6 +39,9 @@ const authConfigSlice = createSlice({
|
||||
resetWarning: (state): AuthConfigState => {
|
||||
return { ...state, warning: undefined };
|
||||
},
|
||||
providersLoaded: (state, action: PayloadAction<SSOProvider[]>): AuthConfigState => {
|
||||
return { ...state, providers: action.payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -50,6 +54,7 @@ export const {
|
||||
resetError,
|
||||
setWarning,
|
||||
resetWarning,
|
||||
providersLoaded,
|
||||
} = authConfigSlice.actions;
|
||||
|
||||
export const authConfigReducer = authConfigSlice.reducer;
|
||||
|
@ -10,12 +10,25 @@ export interface AuthProviderInfo {
|
||||
|
||||
export type GetStatusHook = () => Promise<AuthProviderStatus>;
|
||||
|
||||
export type SSOProvider = {
|
||||
provider: string;
|
||||
settings: {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
|
||||
// Legacy fields
|
||||
configPath?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface AuthConfigState {
|
||||
settings: Settings;
|
||||
providerStatuses: Record<string, AuthProviderStatus>;
|
||||
isLoading?: boolean;
|
||||
updateError?: SettingsError;
|
||||
warning?: SettingsError;
|
||||
providers: SSOProvider[];
|
||||
}
|
||||
|
||||
export interface AuthProviderStatus {
|
||||
|
@ -269,7 +269,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
path: '/admin/authentication',
|
||||
roles: () => contextSrv.evaluatePermission([AccessControlAction.SettingsWrite]),
|
||||
component:
|
||||
config.licenseInfo.enabledFeatures?.saml || config.ldapEnabled
|
||||
config.licenseInfo.enabledFeatures?.saml || config.ldapEnabled || config.featureToggles.ssoSettingsApi
|
||||
? SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AdminAuthentication" */ 'app/features/auth-config/AuthConfigPage')
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user