LDAP: SSO Configuration page (#91875)
* Init screen for LDAP UI * add ldap drawer * update routes * update definitions * add definitions for ldap configurations * improve readibility * remove whitespace * clean up * Adjust LdapSettingsPage * adjust form autocomplete from backend call
This commit is contained in:
@@ -18,7 +18,7 @@ name = "displayName"
|
||||
surname = "sn"
|
||||
username = "cn"
|
||||
member_of = "memberOf"
|
||||
email = "mail"
|
||||
email = "mail"
|
||||
|
||||
# Map ldap groups to grafana org roles
|
||||
[[servers.group_mappings]]
|
||||
|
||||
@@ -114,11 +114,7 @@ func GetLDAPConfig(cfg *setting.Cfg) *Config {
|
||||
// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
|
||||
// the config or it reads it and caches it first.
|
||||
func GetConfig(cfg *Config) (*ServersConfig, error) {
|
||||
if cfg != nil {
|
||||
if !cfg.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
} else if !cfg.Enabled {
|
||||
if cfg == nil || !cfg.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { AppEvents, GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { getBackendSrv, getAppEvents } from '@grafana/runtime';
|
||||
import { useStyles2, Alert, Box, Button, Field, Input, Stack, TextLink } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import config from 'app/core/config';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { Loader } from 'app/features/plugins/admin/components/Loader';
|
||||
import { LdapPayload, StoreState } from 'app/types';
|
||||
|
||||
const appEvents = getAppEvents();
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
ldapSsoSettings: state.ldap.ldapSsoSettings,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
text: 'LDAP',
|
||||
icon: 'shield',
|
||||
id: 'LDAP',
|
||||
};
|
||||
|
||||
const emptySettings: LdapPayload = {
|
||||
id: '',
|
||||
provider: '',
|
||||
source: '',
|
||||
settings: {
|
||||
activeSyncEnabled: false,
|
||||
allowSignUp: false,
|
||||
config: {
|
||||
servers: [
|
||||
{
|
||||
attributes: {},
|
||||
bind_dn: '',
|
||||
bind_password: '',
|
||||
client_cert: '',
|
||||
client_key: '',
|
||||
group_mappings: [],
|
||||
group_search_base_dns: [],
|
||||
group_search_filter: '',
|
||||
group_search_filter_user_attribute: '',
|
||||
host: '',
|
||||
min_tls_version: '',
|
||||
port: 389,
|
||||
root_ca_cert: '',
|
||||
search_base_dns: [],
|
||||
search_filter: '',
|
||||
skip_org_role_sync: false,
|
||||
ssl_skip_verify: false,
|
||||
start_tls: false,
|
||||
timeout: 10,
|
||||
tls_ciphers: [],
|
||||
tls_skip_verify: false,
|
||||
use_ssl: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
enabled: false,
|
||||
skipOrgRoleSync: false,
|
||||
syncCron: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const LdapSettingsPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const methods = useForm<LdapPayload>({ defaultValues: emptySettings });
|
||||
const { getValues, handleSubmit, register, reset } = methods;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
const payload = await getBackendSrv().get<LdapPayload>('/api/v1/sso-settings/ldap');
|
||||
if (!payload || !payload.settings || !payload.settings.config) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('ldap-settings-page.alert.error-fetching', 'Error fetching LDAP settings')],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reset(payload);
|
||||
setIsLoading(false);
|
||||
}
|
||||
init();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* Display warning if the feature flag is disabled
|
||||
*/
|
||||
if (!config.featureToggles.ssoSettingsLDAP) {
|
||||
return (
|
||||
<Alert title="invalid configuration">
|
||||
<Trans i18nKey="ldap-settings-page.alert.feature-flag-disabled">
|
||||
This page is only accessible by enabling the <strong>ssoSettingsLDAP</strong> feature flag.
|
||||
</Trans>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save payload to the backend
|
||||
* @param payload LdapPayload
|
||||
*/
|
||||
const putPayload = async (payload: LdapPayload) => {
|
||||
try {
|
||||
const result = await getBackendSrv().put('/api/v1/sso-settings/ldap', payload);
|
||||
if (result) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')],
|
||||
});
|
||||
}
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [t('ldap-settings-page.alert.saved', 'LDAP settings saved')],
|
||||
});
|
||||
} catch (error) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onErrors = () => {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('ldap-settings-page.alert.error-validate-form', 'Error validating LDAP settings')],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Button's Actions
|
||||
*/
|
||||
const submitAndEnableLdapSettings = (payload: LdapPayload) => {
|
||||
payload.settings.enabled = true;
|
||||
putPayload(payload);
|
||||
};
|
||||
const saveForm = () => {
|
||||
putPayload(getValues());
|
||||
};
|
||||
const discardForm = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await getBackendSrv().delete('/api/v1/sso-settings/ldap');
|
||||
const payload = await getBackendSrv().get<LdapPayload>('/api/v1/sso-settings/ldap');
|
||||
if (!payload || !payload.settings || !payload.settings.config) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('ldap-settings-page.alert.error-update', 'Error updating LDAP settings')],
|
||||
});
|
||||
return;
|
||||
}
|
||||
reset(payload);
|
||||
} catch (error) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')],
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const documentation = (
|
||||
<TextLink
|
||||
href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/ldap/"
|
||||
external
|
||||
>
|
||||
<Trans i18nKey="ldap-settings-page.documentation">documentation</Trans>
|
||||
</TextLink>
|
||||
);
|
||||
const subTitle = (
|
||||
<Trans i18nKey="ldap-settings-page.subtitle">
|
||||
The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in
|
||||
our {documentation}.
|
||||
</Trans>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navId="authentication" pageNav={pageNav} subTitle={subTitle}>
|
||||
<Page.Contents>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(submitAndEnableLdapSettings, onErrors)}>
|
||||
{isLoading && <Loader />}
|
||||
{!isLoading && (
|
||||
<section className={styles.form}>
|
||||
<h3>
|
||||
<Trans i18nKey="ldap-settings-page.title">Basic Settings</Trans>
|
||||
</h3>
|
||||
<Field
|
||||
label={t('ldap-settings-page.host.label', 'Server host')}
|
||||
description={t(
|
||||
'ldap-settings-page.host.description',
|
||||
'Hostname or IP address of the LDAP server you wish to connect to.'
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
id="host"
|
||||
placeholder={t('ldap-settings-page.host.placeholder', 'example: 127.0.0.1')}
|
||||
type="text"
|
||||
{...register('settings.config.servers.0.host', { required: true })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('ldap-settings-page.bind-dn.label', 'Bind DN')}
|
||||
description={t(
|
||||
'ldap-settings-page.bind-dn.description',
|
||||
'Distinguished name of the account used to bind and authenticate to the LDAP server.'
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
id="bind-dn"
|
||||
placeholder={t('ldap-settings-page.bind-dn.placeholder', 'example: cn=admin,dc=grafana,dc=org')}
|
||||
type="text"
|
||||
{...register('settings.config.servers.0.bind_dn')}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('ldap-settings-page.bind-password.label', 'Bind password')}>
|
||||
<Input
|
||||
id="bind-password"
|
||||
type="text"
|
||||
{...register('settings.config.servers.0.bind_password', { required: false })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('ldap-settings-page.search_filter.label', 'Search filter*')}
|
||||
description={t(
|
||||
'ldap-settings-page.search_filter.description',
|
||||
'LDAP search filter used to locate specific entries within the directory.'
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
id="search_filter"
|
||||
placeholder={t('ldap-settings-page.search_filter.placeholder', 'example: cn=%s')}
|
||||
type="text"
|
||||
{...register('settings.config.servers.0.search_filter', { required: true })}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('ldap-settings-page.search-base-dns.label', 'Search base DNS *')}
|
||||
description={t(
|
||||
'ldap-settings-page.search-base-dns.description',
|
||||
'An array of base dns to search through; separate by commas or spaces.'
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
id="search-base-dns"
|
||||
placeholder={t('ldap-settings-page.search-base-dns.placeholder', 'example: "dc=grafana.dc=org"')}
|
||||
type="text"
|
||||
{...register('settings.config.servers.0.search_base_dns', { required: true })}
|
||||
/>
|
||||
</Field>
|
||||
<Box display={'flex'} gap={2} marginTop={5}>
|
||||
<Stack alignItems={'center'} gap={2}>
|
||||
<Button type={'submit'}>
|
||||
<Trans i18nKey="ldap-settings-page.buttons-section.save-and-enable.button">Save and enable</Trans>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={saveForm}>
|
||||
<Trans i18nKey="ldap-settings-page.buttons-section.save.button">Save</Trans>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={discardForm}>
|
||||
<Trans i18nKey="ldap-settings-page.buttons-section.discard.button">Discard</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</section>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
form: css({
|
||||
width: theme.spacing(68),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export default connector(LdapSettingsPage);
|
||||
@@ -52,7 +52,7 @@ export const AuthConfigPageUnconnected = ({
|
||||
reportInteraction('authentication_ui_provider_clicked', { provider: providerType, enabled });
|
||||
};
|
||||
|
||||
// filter out saml from sso providers because it is already included in availableProviders
|
||||
// filter out saml and ldap from sso providers because it is already included in availableProviders
|
||||
providers = providers.filter((p) => p.provider !== 'saml');
|
||||
|
||||
// temporarily remove LDAP until its configuration form is ready
|
||||
|
||||
@@ -298,7 +298,11 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
},
|
||||
{
|
||||
path: '/admin/authentication/ldap',
|
||||
component: LdapPage,
|
||||
component: config.featureToggles.ssoSettingsLDAP
|
||||
? SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "LdapSettingsPage" */ 'app/features/admin/ldap/LdapSettingsPage')
|
||||
)
|
||||
: LdapPage,
|
||||
},
|
||||
{
|
||||
path: '/admin/authentication/:provider',
|
||||
|
||||
@@ -64,6 +64,46 @@ export interface LdapServerInfo {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface GroupMapping {
|
||||
group_dn?: string;
|
||||
org_id?: number;
|
||||
org_role?: string;
|
||||
grafana_admin?: boolean;
|
||||
}
|
||||
|
||||
export interface LdapAttributes {
|
||||
email?: string;
|
||||
member_of?: string;
|
||||
name?: string;
|
||||
surname?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface LdapServerConfig {
|
||||
attributes: LdapAttributes;
|
||||
bind_dn: string;
|
||||
bind_password?: string;
|
||||
client_cert: string;
|
||||
client_key: string;
|
||||
group_mappings: GroupMapping[];
|
||||
group_search_base_dns: string[];
|
||||
group_search_filter: string;
|
||||
group_search_filter_user_attribute: string;
|
||||
host: string;
|
||||
min_tls_version: string;
|
||||
port: number;
|
||||
root_ca_cert: string;
|
||||
search_base_dns: string[];
|
||||
search_filter: string;
|
||||
skip_org_role_sync: boolean;
|
||||
ssl_skip_verify: boolean;
|
||||
start_tls: boolean;
|
||||
timeout: number;
|
||||
tls_ciphers: string[];
|
||||
tls_skip_verify: boolean;
|
||||
use_ssl: boolean;
|
||||
}
|
||||
|
||||
export type LdapConnectionInfo = LdapServerInfo[];
|
||||
|
||||
export interface LdapState {
|
||||
@@ -73,4 +113,25 @@ export interface LdapState {
|
||||
connectionError?: LdapError;
|
||||
userError?: LdapError;
|
||||
ldapError?: LdapError;
|
||||
ldapSsoSettings?: LdapServerConfig;
|
||||
}
|
||||
|
||||
export interface LdapConfig {
|
||||
servers: LdapServerConfig[];
|
||||
}
|
||||
|
||||
export interface LdapSettings {
|
||||
activeSyncEnabled: boolean;
|
||||
allowSignUp: boolean;
|
||||
config: LdapConfig;
|
||||
enabled: boolean;
|
||||
skipOrgRoleSync: boolean;
|
||||
syncCron: string;
|
||||
}
|
||||
|
||||
export interface LdapPayload {
|
||||
id: string;
|
||||
provider: string;
|
||||
settings: LdapSettings;
|
||||
source: string;
|
||||
}
|
||||
|
||||
@@ -977,6 +977,53 @@
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
},
|
||||
"ldap-settings-page": {
|
||||
"alert": {
|
||||
"error-fetching": "Error fetching LDAP settings",
|
||||
"error-saving": "Error saving LDAP settings",
|
||||
"error-update": "Error updating LDAP settings",
|
||||
"error-validate-form": "Error validating LDAP settings",
|
||||
"feature-flag-disabled": "This page is only accessible by enabling the <1>ssoSettingsLDAP</1> feature flag.",
|
||||
"saved": "LDAP settings saved"
|
||||
},
|
||||
"bind-dn": {
|
||||
"description": "Distinguished name of the account used to bind and authenticate to the LDAP server.",
|
||||
"label": "Bind DN",
|
||||
"placeholder": "example: cn=admin,dc=grafana,dc=org"
|
||||
},
|
||||
"bind-password": {
|
||||
"label": "Bind password"
|
||||
},
|
||||
"buttons-section": {
|
||||
"discard": {
|
||||
"button": "Discard"
|
||||
},
|
||||
"save": {
|
||||
"button": "Save"
|
||||
},
|
||||
"save-and-enable": {
|
||||
"button": "Save and enable"
|
||||
}
|
||||
},
|
||||
"documentation": "documentation",
|
||||
"host": {
|
||||
"description": "Hostname or IP address of the LDAP server you wish to connect to.",
|
||||
"label": "Server host",
|
||||
"placeholder": "example: 127.0.0.1"
|
||||
},
|
||||
"search_filter": {
|
||||
"description": "LDAP search filter used to locate specific entries within the directory.",
|
||||
"label": "Search filter*",
|
||||
"placeholder": "example: cn=%s"
|
||||
},
|
||||
"search-base-dns": {
|
||||
"description": "An array of base dns to search through; separate by commas or spaces.",
|
||||
"label": "Search base DNS *",
|
||||
"placeholder": "example: \"dc=grafana.dc=org\""
|
||||
},
|
||||
"subtitle": "The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in our {documentation}.",
|
||||
"title": "Basic Settings"
|
||||
},
|
||||
"library-panel": {
|
||||
"add-modal": {
|
||||
"cancel": "Cancel",
|
||||
|
||||
@@ -977,6 +977,53 @@
|
||||
"refresh": "Ŗęƒřęşĥ"
|
||||
}
|
||||
},
|
||||
"ldap-settings-page": {
|
||||
"alert": {
|
||||
"error-fetching": "Ēřřőř ƒęŧčĥįʼnģ ĿĐÅP şęŧŧįʼnģş",
|
||||
"error-saving": "Ēřřőř şävįʼnģ ĿĐÅP şęŧŧįʼnģş",
|
||||
"error-update": "Ēřřőř ūpđäŧįʼnģ ĿĐÅP şęŧŧįʼnģş",
|
||||
"error-validate-form": "Ēřřőř väľįđäŧįʼnģ ĿĐÅP şęŧŧįʼnģş",
|
||||
"feature-flag-disabled": "Ŧĥįş päģę įş őʼnľy äččęşşįþľę þy ęʼnäþľįʼnģ ŧĥę <1>şşőŜęŧŧįʼnģşĿĐÅP</1> ƒęäŧūřę ƒľäģ.",
|
||||
"saved": "ĿĐÅP şęŧŧįʼnģş şävęđ"
|
||||
},
|
||||
"bind-dn": {
|
||||
"description": "Đįşŧįʼnģūįşĥęđ ʼnämę őƒ ŧĥę äččőūʼnŧ ūşęđ ŧő þįʼnđ äʼnđ äūŧĥęʼnŧįčäŧę ŧő ŧĥę ĿĐÅP şęřvęř.",
|
||||
"label": "ßįʼnđ ĐŃ",
|
||||
"placeholder": "ęχämpľę: čʼn=äđmįʼn,đč=ģřäƒäʼnä,đč=őřģ"
|
||||
},
|
||||
"bind-password": {
|
||||
"label": "ßįʼnđ päşşŵőřđ"
|
||||
},
|
||||
"buttons-section": {
|
||||
"discard": {
|
||||
"button": "Đįşčäřđ"
|
||||
},
|
||||
"save": {
|
||||
"button": "Ŝävę"
|
||||
},
|
||||
"save-and-enable": {
|
||||
"button": "Ŝävę äʼnđ ęʼnäþľę"
|
||||
}
|
||||
},
|
||||
"documentation": "đőčūmęʼnŧäŧįőʼn",
|
||||
"host": {
|
||||
"description": "Ħőşŧʼnämę őř ĨP äđđřęşş őƒ ŧĥę ĿĐÅP şęřvęř yőū ŵįşĥ ŧő čőʼnʼnęčŧ ŧő.",
|
||||
"label": "Ŝęřvęř ĥőşŧ",
|
||||
"placeholder": "ęχämpľę: 127.0.0.1"
|
||||
},
|
||||
"search_filter": {
|
||||
"description": "ĿĐÅP şęäřčĥ ƒįľŧęř ūşęđ ŧő ľőčäŧę şpęčįƒįč ęʼnŧřįęş ŵįŧĥįʼn ŧĥę đįřęčŧőřy.",
|
||||
"label": "Ŝęäřčĥ ƒįľŧęř*",
|
||||
"placeholder": "ęχämpľę: čʼn=%ş"
|
||||
},
|
||||
"search-base-dns": {
|
||||
"description": "Åʼn äřřäy őƒ þäşę đʼnş ŧő şęäřčĥ ŧĥřőūģĥ; şępäřäŧę þy čőmmäş őř şpäčęş.",
|
||||
"label": "Ŝęäřčĥ þäşę ĐŃŜ *",
|
||||
"placeholder": "ęχämpľę: \"đč=ģřäƒäʼnä.đč=őřģ\""
|
||||
},
|
||||
"subtitle": "Ŧĥę ĿĐÅP įʼnŧęģřäŧįőʼn įʼn Ğřäƒäʼnä äľľőŵş yőūř Ğřäƒäʼnä ūşęřş ŧő ľőģ įʼn ŵįŧĥ ŧĥęįř ĿĐÅP čřęđęʼnŧįäľş. Fįʼnđ őūŧ mőřę įʼn őūř {đőčūmęʼnŧäŧįőʼn}.",
|
||||
"title": "ßäşįč Ŝęŧŧįʼnģş"
|
||||
},
|
||||
"library-panel": {
|
||||
"add-modal": {
|
||||
"cancel": "Cäʼnčęľ",
|
||||
|
||||
Reference in New Issue
Block a user