Skip to content

Commit eecd566

Browse files
authored
LDAP: SSO Configuration page (grafana#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
1 parent cd4b7ef commit eecd566

File tree

8 files changed

+457
-8
lines changed

8 files changed

+457
-8
lines changed

devenv/docker/blocks/auth/authentik/ldap_authentik.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ name = "displayName"
1818
surname = "sn"
1919
username = "cn"
2020
member_of = "memberOf"
21-
email = "mail"
21+
email = "mail"
2222

2323
# Map ldap groups to grafana org roles
2424
[[servers.group_mappings]]

pkg/services/ldap/settings.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,7 @@ func GetLDAPConfig(cfg *setting.Cfg) *Config {
114114
// GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
115115
// the config or it reads it and caches it first.
116116
func GetConfig(cfg *Config) (*ServersConfig, error) {
117-
if cfg != nil {
118-
if !cfg.Enabled {
119-
return nil, nil
120-
}
121-
} else if !cfg.Enabled {
117+
if cfg == nil || !cfg.Enabled {
122118
return nil, nil
123119
}
124120

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { css } from '@emotion/css';
2+
import { useEffect, useState } from 'react';
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
import { connect } from 'react-redux';
5+
6+
import { AppEvents, GrafanaTheme2, NavModelItem } from '@grafana/data';
7+
import { getBackendSrv, getAppEvents } from '@grafana/runtime';
8+
import { useStyles2, Alert, Box, Button, Field, Input, Stack, TextLink } from '@grafana/ui';
9+
import { Page } from 'app/core/components/Page/Page';
10+
import config from 'app/core/config';
11+
import { t, Trans } from 'app/core/internationalization';
12+
import { Loader } from 'app/features/plugins/admin/components/Loader';
13+
import { LdapPayload, StoreState } from 'app/types';
14+
15+
const appEvents = getAppEvents();
16+
17+
const mapStateToProps = (state: StoreState) => ({
18+
ldapSsoSettings: state.ldap.ldapSsoSettings,
19+
});
20+
21+
const mapDispatchToProps = {};
22+
23+
const connector = connect(mapStateToProps, mapDispatchToProps);
24+
25+
const pageNav: NavModelItem = {
26+
text: 'LDAP',
27+
icon: 'shield',
28+
id: 'LDAP',
29+
};
30+
31+
const emptySettings: LdapPayload = {
32+
id: '',
33+
provider: '',
34+
source: '',
35+
settings: {
36+
activeSyncEnabled: false,
37+
allowSignUp: false,
38+
config: {
39+
servers: [
40+
{
41+
attributes: {},
42+
bind_dn: '',
43+
bind_password: '',
44+
client_cert: '',
45+
client_key: '',
46+
group_mappings: [],
47+
group_search_base_dns: [],
48+
group_search_filter: '',
49+
group_search_filter_user_attribute: '',
50+
host: '',
51+
min_tls_version: '',
52+
port: 389,
53+
root_ca_cert: '',
54+
search_base_dns: [],
55+
search_filter: '',
56+
skip_org_role_sync: false,
57+
ssl_skip_verify: false,
58+
start_tls: false,
59+
timeout: 10,
60+
tls_ciphers: [],
61+
tls_skip_verify: false,
62+
use_ssl: false,
63+
},
64+
],
65+
},
66+
enabled: false,
67+
skipOrgRoleSync: false,
68+
syncCron: '',
69+
},
70+
};
71+
72+
export const LdapSettingsPage = () => {
73+
const [isLoading, setIsLoading] = useState(true);
74+
75+
const methods = useForm<LdapPayload>({ defaultValues: emptySettings });
76+
const { getValues, handleSubmit, register, reset } = methods;
77+
78+
const styles = useStyles2(getStyles);
79+
80+
useEffect(() => {
81+
async function init() {
82+
const payload = await getBackendSrv().get<LdapPayload>('/api/v1/sso-settings/ldap');
83+
if (!payload || !payload.settings || !payload.settings.config) {
84+
appEvents.publish({
85+
type: AppEvents.alertError.name,
86+
payload: [t('ldap-settings-page.alert.error-fetching', 'Error fetching LDAP settings')],
87+
});
88+
return;
89+
}
90+
91+
reset(payload);
92+
setIsLoading(false);
93+
}
94+
init();
95+
}, [reset]);
96+
97+
/**
98+
* Display warning if the feature flag is disabled
99+
*/
100+
if (!config.featureToggles.ssoSettingsLDAP) {
101+
return (
102+
<Alert title="invalid configuration">
103+
<Trans i18nKey="ldap-settings-page.alert.feature-flag-disabled">
104+
This page is only accessible by enabling the <strong>ssoSettingsLDAP</strong> feature flag.
105+
</Trans>
106+
</Alert>
107+
);
108+
}
109+
110+
/**
111+
* Save payload to the backend
112+
* @param payload LdapPayload
113+
*/
114+
const putPayload = async (payload: LdapPayload) => {
115+
try {
116+
const result = await getBackendSrv().put('/api/v1/sso-settings/ldap', payload);
117+
if (result) {
118+
appEvents.publish({
119+
type: AppEvents.alertError.name,
120+
payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')],
121+
});
122+
}
123+
appEvents.publish({
124+
type: AppEvents.alertSuccess.name,
125+
payload: [t('ldap-settings-page.alert.saved', 'LDAP settings saved')],
126+
});
127+
} catch (error) {
128+
appEvents.publish({
129+
type: AppEvents.alertError.name,
130+
payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')],
131+
});
132+
}
133+
};
134+
135+
const onErrors = () => {
136+
appEvents.publish({
137+
type: AppEvents.alertError.name,
138+
payload: [t('ldap-settings-page.alert.error-validate-form', 'Error validating LDAP settings')],
139+
});
140+
};
141+
142+
/**
143+
* Button's Actions
144+
*/
145+
const submitAndEnableLdapSettings = (payload: LdapPayload) => {
146+
payload.settings.enabled = true;
147+
putPayload(payload);
148+
};
149+
const saveForm = () => {
150+
putPayload(getValues());
151+
};
152+
const discardForm = async () => {
153+
try {
154+
setIsLoading(true);
155+
await getBackendSrv().delete('/api/v1/sso-settings/ldap');
156+
const payload = await getBackendSrv().get<LdapPayload>('/api/v1/sso-settings/ldap');
157+
if (!payload || !payload.settings || !payload.settings.config) {
158+
appEvents.publish({
159+
type: AppEvents.alertError.name,
160+
payload: [t('ldap-settings-page.alert.error-update', 'Error updating LDAP settings')],
161+
});
162+
return;
163+
}
164+
reset(payload);
165+
} catch (error) {
166+
appEvents.publish({
167+
type: AppEvents.alertError.name,
168+
payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')],
169+
});
170+
} finally {
171+
setIsLoading(false);
172+
}
173+
};
174+
175+
const documentation = (
176+
<TextLink
177+
href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/ldap/"
178+
external
179+
>
180+
<Trans i18nKey="ldap-settings-page.documentation">documentation</Trans>
181+
</TextLink>
182+
);
183+
const subTitle = (
184+
<Trans i18nKey="ldap-settings-page.subtitle">
185+
The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in
186+
our {documentation}.
187+
</Trans>
188+
);
189+
190+
return (
191+
<Page navId="authentication" pageNav={pageNav} subTitle={subTitle}>
192+
<Page.Contents>
193+
<FormProvider {...methods}>
194+
<form onSubmit={handleSubmit(submitAndEnableLdapSettings, onErrors)}>
195+
{isLoading && <Loader />}
196+
{!isLoading && (
197+
<section className={styles.form}>
198+
<h3>
199+
<Trans i18nKey="ldap-settings-page.title">Basic Settings</Trans>
200+
</h3>
201+
<Field
202+
label={t('ldap-settings-page.host.label', 'Server host')}
203+
description={t(
204+
'ldap-settings-page.host.description',
205+
'Hostname or IP address of the LDAP server you wish to connect to.'
206+
)}
207+
>
208+
<Input
209+
id="host"
210+
placeholder={t('ldap-settings-page.host.placeholder', 'example: 127.0.0.1')}
211+
type="text"
212+
{...register('settings.config.servers.0.host', { required: true })}
213+
/>
214+
</Field>
215+
<Field
216+
label={t('ldap-settings-page.bind-dn.label', 'Bind DN')}
217+
description={t(
218+
'ldap-settings-page.bind-dn.description',
219+
'Distinguished name of the account used to bind and authenticate to the LDAP server.'
220+
)}
221+
>
222+
<Input
223+
id="bind-dn"
224+
placeholder={t('ldap-settings-page.bind-dn.placeholder', 'example: cn=admin,dc=grafana,dc=org')}
225+
type="text"
226+
{...register('settings.config.servers.0.bind_dn')}
227+
/>
228+
</Field>
229+
<Field label={t('ldap-settings-page.bind-password.label', 'Bind password')}>
230+
<Input
231+
id="bind-password"
232+
type="text"
233+
{...register('settings.config.servers.0.bind_password', { required: false })}
234+
/>
235+
</Field>
236+
<Field
237+
label={t('ldap-settings-page.search_filter.label', 'Search filter*')}
238+
description={t(
239+
'ldap-settings-page.search_filter.description',
240+
'LDAP search filter used to locate specific entries within the directory.'
241+
)}
242+
>
243+
<Input
244+
id="search_filter"
245+
placeholder={t('ldap-settings-page.search_filter.placeholder', 'example: cn=%s')}
246+
type="text"
247+
{...register('settings.config.servers.0.search_filter', { required: true })}
248+
/>
249+
</Field>
250+
<Field
251+
label={t('ldap-settings-page.search-base-dns.label', 'Search base DNS *')}
252+
description={t(
253+
'ldap-settings-page.search-base-dns.description',
254+
'An array of base dns to search through; separate by commas or spaces.'
255+
)}
256+
>
257+
<Input
258+
id="search-base-dns"
259+
placeholder={t('ldap-settings-page.search-base-dns.placeholder', 'example: "dc=grafana.dc=org"')}
260+
type="text"
261+
{...register('settings.config.servers.0.search_base_dns', { required: true })}
262+
/>
263+
</Field>
264+
<Box display={'flex'} gap={2} marginTop={5}>
265+
<Stack alignItems={'center'} gap={2}>
266+
<Button type={'submit'}>
267+
<Trans i18nKey="ldap-settings-page.buttons-section.save-and-enable.button">Save and enable</Trans>
268+
</Button>
269+
<Button variant="secondary" onClick={saveForm}>
270+
<Trans i18nKey="ldap-settings-page.buttons-section.save.button">Save</Trans>
271+
</Button>
272+
<Button variant="secondary" onClick={discardForm}>
273+
<Trans i18nKey="ldap-settings-page.buttons-section.discard.button">Discard</Trans>
274+
</Button>
275+
</Stack>
276+
</Box>
277+
</section>
278+
)}
279+
</form>
280+
</FormProvider>
281+
</Page.Contents>
282+
</Page>
283+
);
284+
};
285+
286+
function getStyles(theme: GrafanaTheme2) {
287+
return {
288+
form: css({
289+
width: theme.spacing(68),
290+
}),
291+
};
292+
}
293+
294+
export default connector(LdapSettingsPage);

public/app/features/auth-config/AuthProvidersListPage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const AuthConfigPageUnconnected = ({
5252
reportInteraction('authentication_ui_provider_clicked', { provider: providerType, enabled });
5353
};
5454

55-
// filter out saml from sso providers because it is already included in availableProviders
55+
// filter out saml and ldap from sso providers because it is already included in availableProviders
5656
providers = providers.filter((p) => p.provider !== 'saml');
5757

5858
// temporarily remove LDAP until its configuration form is ready

public/app/routes/routes.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,11 @@ export function getAppRoutes(): RouteDescriptor[] {
298298
},
299299
{
300300
path: '/admin/authentication/ldap',
301-
component: LdapPage,
301+
component: config.featureToggles.ssoSettingsLDAP
302+
? SafeDynamicImport(
303+
() => import(/* webpackChunkName: "LdapSettingsPage" */ 'app/features/admin/ldap/LdapSettingsPage')
304+
)
305+
: LdapPage,
302306
},
303307
{
304308
path: '/admin/authentication/:provider',

0 commit comments

Comments
 (0)