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.", "1"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[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{
|
configNodes = append(configNodes, &navtree.NavLink{
|
||||||
Text: "Authentication",
|
Text: "Authentication",
|
||||||
Id: "authentication",
|
Id: "authentication",
|
||||||
|
@ -1,31 +1,27 @@
|
|||||||
import { css } from '@emotion/css';
|
import React, { JSX, useEffect } from 'react';
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
import ConfigureAuthCTA from './components/ConfigureAuthCTA';
|
import ConfigureAuthCTA from './components/ConfigureAuthCTA';
|
||||||
import { ProviderCard } from './components/ProviderCard';
|
import { ProviderCard } from './components/ProviderCard';
|
||||||
import { loadSettings } from './state/actions';
|
import { loadSettings } from './state/actions';
|
||||||
import { AuthProviderInfo } from './types';
|
|
||||||
import { getProviderUrl } from './utils';
|
|
||||||
|
|
||||||
import { getRegisteredAuthProviders } from '.';
|
import { getRegisteredAuthProviders } from './index';
|
||||||
|
|
||||||
interface OwnProps {}
|
interface OwnProps {}
|
||||||
|
|
||||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState) {
|
function mapStateToProps(state: StoreState) {
|
||||||
const { isLoading, providerStatuses } = state.authConfig;
|
const { isLoading, providerStatuses, providers } = state.authConfig;
|
||||||
return {
|
return {
|
||||||
isLoading,
|
isLoading,
|
||||||
providerStatuses,
|
providerStatuses,
|
||||||
|
providers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,125 +31,65 @@ const mapDispatchToProps = {
|
|||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSettings }: Props): JSX.Element => {
|
export const AuthConfigPageUnconnected = ({
|
||||||
const styles = useStyles2(getStyles);
|
providerStatuses,
|
||||||
|
isLoading,
|
||||||
|
loadSettings,
|
||||||
|
providers,
|
||||||
|
}: Props): JSX.Element => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}, [loadSettings]);
|
}, [loadSettings]);
|
||||||
|
|
||||||
const authProviders = getRegisteredAuthProviders();
|
const authProviders = getRegisteredAuthProviders();
|
||||||
const enabledProviders = authProviders.filter((p) => providerStatuses[p.id]?.enabled);
|
const availableProviders = authProviders.filter((p) => !providerStatuses[p.id]?.hide);
|
||||||
const configuresProviders = authProviders.filter(
|
const onProviderCardClick = (providerType: string) => {
|
||||||
(p) => providerStatuses[p.id]?.configured && !providerStatuses[p.id]?.enabled
|
reportInteraction('authentication_ui_provider_clicked', { provider: providerType });
|
||||||
);
|
|
||||||
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 providerList = availableProviders.length
|
||||||
|
? [
|
||||||
|
...availableProviders.map((p) => ({
|
||||||
|
provider: p.id,
|
||||||
|
settings: { ...providerStatuses[p.id], configPath: p.configPath, type: p.type },
|
||||||
|
})),
|
||||||
|
...providers,
|
||||||
|
]
|
||||||
|
: providers;
|
||||||
return (
|
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}>
|
<Page.Contents isLoading={isLoading}>
|
||||||
<h3 className={styles.sectionHeader}>Configured authentication</h3>
|
{!providerList.length ? (
|
||||||
{!!enabledProviders?.length && (
|
<ConfigureAuthCTA />
|
||||||
<div className={styles.cardsContainer}>
|
) : (
|
||||||
{enabledProviders.map((provider) => (
|
<Grid gap={3} minColumnWidth={34}>
|
||||||
|
{providerList.map(({ provider, settings }) => (
|
||||||
<ProviderCard
|
<ProviderCard
|
||||||
key={provider.id}
|
key={provider}
|
||||||
providerId={provider.id}
|
authType={settings.type || 'OAuth'}
|
||||||
displayName={providerStatuses[provider.id]?.name || provider.displayName}
|
providerId={provider}
|
||||||
authType={provider.protocol}
|
displayName={provider}
|
||||||
enabled={providerStatuses[provider.id]?.enabled}
|
enabled={settings.enabled}
|
||||||
configPath={provider.configPath}
|
|
||||||
onClick={() => onProviderCardClick(provider)}
|
onClick={() => onProviderCardClick(provider)}
|
||||||
|
configPath={settings.configPath}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Grid>
|
||||||
)}
|
|
||||||
{!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>
|
|
||||||
)}
|
)}
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</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);
|
export default connector(AuthConfigPageUnconnected);
|
||||||
|
@ -2,57 +2,40 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { Icon, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
||||||
import { CallToActionCard, IconName, LinkButton, useStyles2 } from '@grafana/ui';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {}
|
||||||
title: string;
|
|
||||||
buttonIcon: IconName;
|
|
||||||
buttonLink?: string;
|
|
||||||
buttonTitle: string;
|
|
||||||
buttonDisabled?: boolean;
|
|
||||||
description?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigureAuthCTA: React.FunctionComponent<Props> = ({
|
const ConfigureAuthCTA: React.FunctionComponent<Props> = () => {
|
||||||
title,
|
|
||||||
buttonIcon,
|
|
||||||
buttonLink,
|
|
||||||
buttonTitle,
|
|
||||||
buttonDisabled,
|
|
||||||
description,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const footer = description ? <span key="proTipFooter">{description}</span> : '';
|
return (
|
||||||
const ctaElementClassName = !description ? styles.button : '';
|
<div className={styles.container}>
|
||||||
|
<Stack gap={1} alignItems={'center'}>
|
||||||
const ctaElement = (
|
<Icon name={'cog'} />
|
||||||
<LinkButton
|
<Text>Configuration required</Text>
|
||||||
size="lg"
|
</Stack>
|
||||||
href={buttonLink}
|
<Text variant={'bodySmall'} color={'secondary'}>
|
||||||
icon={buttonIcon}
|
You have no authentication configuration created at the moment.
|
||||||
className={ctaElementClassName}
|
</Text>
|
||||||
data-testid={selectors.components.CallToActionCard.buttonV2(buttonTitle)}
|
<TextLink href={'https://grafana.com/docs/grafana/latest/auth/overview/'} external>
|
||||||
disabled={buttonDisabled}
|
Refer to the documentation on how to configure authentication
|
||||||
onClick={() => onClick && onClick()}
|
</TextLink>
|
||||||
>
|
</div>
|
||||||
{buttonTitle}
|
|
||||||
</LinkButton>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return <CallToActionCard className={styles.cta} message={title} footer={footer} callToActionElement={ctaElement} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
cta: css`
|
container: css({
|
||||||
text-align: center;
|
display: 'flex',
|
||||||
`,
|
flexDirection: 'column',
|
||||||
button: css`
|
gap: theme.spacing(2),
|
||||||
margin-bottom: ${theme.spacing(2.5)};
|
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 React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { IconName, isIconName } from '@grafana/data';
|
||||||
import { Badge, Card, useStyles2, Icon } from '@grafana/ui';
|
import { Badge, Card, Icon } from '@grafana/ui';
|
||||||
|
|
||||||
import { BASE_PATH } from '../constants';
|
import { getProviderUrl } from '../utils';
|
||||||
|
|
||||||
export const LOGO_SIZE = '48px';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
@ -14,79 +11,36 @@ type Props = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
configPath?: string;
|
configPath?: string;
|
||||||
authType?: string;
|
authType?: string;
|
||||||
badges?: JSX.Element[];
|
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProviderCard({ providerId, displayName, enabled, configPath, authType, badges, onClick }: Props) {
|
// TODO Remove when this is available from API
|
||||||
const styles = useStyles2(getStyles);
|
const UIMap: Record<string, [IconName, string]> = {
|
||||||
configPath = BASE_PATH + (configPath || providerId);
|
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 (
|
return (
|
||||||
<Card href={configPath} className={styles.container} onClick={() => onClick && onClick()}>
|
<Card href={url} onClick={onClick}>
|
||||||
<div className={styles.header}>
|
<Card.Heading>{displayName}</Card.Heading>
|
||||||
<span className={styles.smallText}>{authType}</span>
|
<Card.Meta>{authType}</Card.Meta>
|
||||||
<span className={styles.name}>{displayName}</span>
|
{isIconName(iconName) && (
|
||||||
</div>
|
<Card.Figure>
|
||||||
<div className={styles.footer}>
|
<Icon name={iconName} size={'xxxl'} />
|
||||||
<div className={styles.badgeContainer}>
|
</Card.Figure>
|
||||||
{enabled ? <Badge text="Enabled" color="green" icon="check" /> : <Badge text="Not enabled" color="blue" />}
|
)}
|
||||||
</div>
|
<Card.Actions>
|
||||||
<span className={styles.edit}>
|
<Badge text={enabled ? 'Enabled' : 'Not enabled'} color={enabled ? 'green' : 'blue'} />
|
||||||
Edit
|
</Card.Actions>
|
||||||
<Icon color="blue" name={'arrow-right'} size="sm" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</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 { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { getBackendSrv, isFetchError } from '@grafana/runtime';
|
import { config, getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { AccessControlAction, Settings, ThunkResult, UpdateSettingsQuery } from 'app/types';
|
import { AccessControlAction, Settings, ThunkResult, UpdateSettingsQuery } from 'app/types';
|
||||||
|
|
||||||
import { getAuthProviderStatus, getRegisteredAuthProviders } from '..';
|
import { getAuthProviderStatus, getRegisteredAuthProviders, SSOProvider } from '..';
|
||||||
import { AuthProviderStatus, SettingsError } from '../types';
|
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>> {
|
export function loadSettings(): ThunkResult<Promise<Settings>> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
if (contextSrv.hasPermission(AccessControlAction.SettingsRead)) {
|
if (contextSrv.hasPermission(AccessControlAction.SettingsRead)) {
|
||||||
dispatch(loadingBegin());
|
dispatch(loadingBegin());
|
||||||
|
dispatch(loadProviders());
|
||||||
const result = await getBackendSrv().get('/api/admin/settings');
|
const result = await getBackendSrv().get('/api/admin/settings');
|
||||||
dispatch(settingsUpdated(result));
|
dispatch(settingsUpdated(result));
|
||||||
await dispatch(loadProviderStatuses());
|
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> {
|
export function loadProviderStatuses(): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
const registeredProviders = getRegisteredAuthProviders();
|
const registeredProviders = getRegisteredAuthProviders();
|
||||||
@ -33,8 +53,7 @@ export function loadProviderStatuses(): ThunkResult<void> {
|
|||||||
const statuses = await Promise.all(getStatusPromises);
|
const statuses = await Promise.all(getStatusPromises);
|
||||||
for (let i = 0; i < registeredProviders.length; i++) {
|
for (let i = 0; i < registeredProviders.length; i++) {
|
||||||
const provider = registeredProviders[i];
|
const provider = registeredProviders[i];
|
||||||
const status = statuses[i];
|
providerStatuses[provider.id] = statuses[i];
|
||||||
providerStatuses[provider.id] = status;
|
|
||||||
}
|
}
|
||||||
dispatch(providerStatusesLoaded(providerStatuses));
|
dispatch(providerStatusesLoaded(providerStatuses));
|
||||||
};
|
};
|
||||||
|
@ -2,12 +2,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
|
|
||||||
import { Settings } from 'app/types';
|
import { Settings } from 'app/types';
|
||||||
|
|
||||||
import { SettingsError, AuthProviderStatus, AuthConfigState } from '../types';
|
import { SettingsError, AuthProviderStatus, AuthConfigState, SSOProvider } from '../types';
|
||||||
|
|
||||||
export const initialState: AuthConfigState = {
|
export const initialState: AuthConfigState = {
|
||||||
settings: {},
|
settings: {},
|
||||||
providerStatuses: {},
|
providerStatuses: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
providers: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const authConfigSlice = createSlice({
|
const authConfigSlice = createSlice({
|
||||||
@ -38,6 +39,9 @@ const authConfigSlice = createSlice({
|
|||||||
resetWarning: (state): AuthConfigState => {
|
resetWarning: (state): AuthConfigState => {
|
||||||
return { ...state, warning: undefined };
|
return { ...state, warning: undefined };
|
||||||
},
|
},
|
||||||
|
providersLoaded: (state, action: PayloadAction<SSOProvider[]>): AuthConfigState => {
|
||||||
|
return { ...state, providers: action.payload };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,6 +54,7 @@ export const {
|
|||||||
resetError,
|
resetError,
|
||||||
setWarning,
|
setWarning,
|
||||||
resetWarning,
|
resetWarning,
|
||||||
|
providersLoaded,
|
||||||
} = authConfigSlice.actions;
|
} = authConfigSlice.actions;
|
||||||
|
|
||||||
export const authConfigReducer = authConfigSlice.reducer;
|
export const authConfigReducer = authConfigSlice.reducer;
|
||||||
|
@ -10,12 +10,25 @@ export interface AuthProviderInfo {
|
|||||||
|
|
||||||
export type GetStatusHook = () => Promise<AuthProviderStatus>;
|
export type GetStatusHook = () => Promise<AuthProviderStatus>;
|
||||||
|
|
||||||
|
export type SSOProvider = {
|
||||||
|
provider: string;
|
||||||
|
settings: {
|
||||||
|
enabled: boolean;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
// Legacy fields
|
||||||
|
configPath?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface AuthConfigState {
|
export interface AuthConfigState {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
providerStatuses: Record<string, AuthProviderStatus>;
|
providerStatuses: Record<string, AuthProviderStatus>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
updateError?: SettingsError;
|
updateError?: SettingsError;
|
||||||
warning?: SettingsError;
|
warning?: SettingsError;
|
||||||
|
providers: SSOProvider[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthProviderStatus {
|
export interface AuthProviderStatus {
|
||||||
|
@ -269,7 +269,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
path: '/admin/authentication',
|
path: '/admin/authentication',
|
||||||
roles: () => contextSrv.evaluatePermission([AccessControlAction.SettingsWrite]),
|
roles: () => contextSrv.evaluatePermission([AccessControlAction.SettingsWrite]),
|
||||||
component:
|
component:
|
||||||
config.licenseInfo.enabledFeatures?.saml || config.ldapEnabled
|
config.licenseInfo.enabledFeatures?.saml || config.ldapEnabled || config.featureToggles.ssoSettingsApi
|
||||||
? SafeDynamicImport(
|
? SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AdminAuthentication" */ 'app/features/auth-config/AuthConfigPage')
|
() => import(/* webpackChunkName: "AdminAuthentication" */ 'app/features/auth-config/AuthConfigPage')
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user