Skip to content

Commit 9fbfaf0

Browse files
authored
SSO LDAP: Add LDAP settings drawer (grafana#92199)
* Add LdapDrawer component * Add success message for discarding LDAP settings * Add Use SSL tooltip * Add min tls version field * Change alert error to alert success * Add tls ciphers field * Update i18n * rename ssl for tls * rename ldap to LDAP
1 parent 922babb commit 9fbfaf0

File tree

5 files changed

+454
-2
lines changed

5 files changed

+454
-2
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { css } from '@emotion/css';
2+
import { useFormContext } from 'react-hook-form';
3+
4+
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
5+
import {
6+
useStyles2,
7+
CollapsableSection,
8+
Divider,
9+
Drawer,
10+
Field,
11+
Icon,
12+
Input,
13+
Label,
14+
Select,
15+
Stack,
16+
Switch,
17+
Text,
18+
TextLink,
19+
Tooltip,
20+
} from '@grafana/ui';
21+
import { t, Trans } from 'app/core/internationalization';
22+
import { LdapPayload } from 'app/types';
23+
24+
interface Props {
25+
onClose: () => void;
26+
}
27+
28+
const tlsOptions: Array<SelectableValue<string>> = ['TLS1.2', 'TLS1.3'].map((v) => ({ label: v, value: v }));
29+
30+
export const LdapDrawerComponent = ({ onClose }: Props) => {
31+
const styles = useStyles2(getStyles);
32+
const { register, setValue, watch } = useFormContext<LdapPayload>();
33+
34+
const groupMappingsLabel = (
35+
<Label
36+
className={styles.sectionLabel}
37+
description={t('ldap-drawer.group-mapping-section.description', 'Map LDAP groups to Grafana org roles')}
38+
>
39+
<Trans i18nKey="ldap-drawer.group-mapping-section.label">Group mapping</Trans>
40+
</Label>
41+
);
42+
43+
const useTlsDescription = (
44+
<Trans i18nKey="ldap-drawer.extra-security-section.use-ssl.tooltip">
45+
For a complete list of supported ciphers and TLS versions, refer to:{' '}
46+
{
47+
<TextLink style={{ fontSize: 'inherit' }} href="https://go.dev/src/crypto/tls/cipher_suites.go" external>
48+
https://go.dev/src/crypto/tls/cipher_suites.go
49+
</TextLink>
50+
}
51+
</Trans>
52+
);
53+
54+
return (
55+
<Drawer title={t('ldap-drawer.title', 'Advanced Settings')} onClose={onClose}>
56+
<CollapsableSection label={t('ldap-drawer.misc-section.label', 'Misc')} isOpen={true}>
57+
<Field
58+
label={t('ldap-drawer.misc-section.allow-sign-up.label', 'Allow sign up')}
59+
description={t(
60+
'ldap-drawer.misc-section.allow-sign-up.descrition',
61+
'If not enabled, only existing Grafana users can log in using LDAP'
62+
)}
63+
>
64+
<Switch id="allow-sign-up" {...register('settings.allowSignUp')} />
65+
</Field>
66+
<Field
67+
label={t('ldap-drawer.misc-section.port.label', 'Port')}
68+
description={t(
69+
'ldap-drawer.misc-section.port.description',
70+
'Default port is 389 without SSL or 636 with SSL'
71+
)}
72+
>
73+
<Input
74+
id="port"
75+
placeholder={t('ldap-drawer.misc-section.port.placeholder', '389')}
76+
type="number"
77+
{...register('settings.config.servers.0.port', { valueAsNumber: true })}
78+
/>
79+
</Field>
80+
<Field
81+
label={t('ldap-drawer.misc-section.timeout.label', 'Timeout')}
82+
description={t(
83+
'ldap-drawer.misc-section.timeout.description',
84+
'Timeout in seconds for the connection to the LDAP server'
85+
)}
86+
>
87+
<Input
88+
id="timeout"
89+
placeholder={t('ldap-drawer.misc-section.timeout.placeholder', '389')}
90+
type="number"
91+
{...register('settings.config.servers.0.timeout', { valueAsNumber: true })}
92+
/>
93+
</Field>
94+
</CollapsableSection>
95+
<CollapsableSection label={t('ldap-drawer.attributes-section.label', 'Attributes')} isOpen={true}>
96+
<Text color="secondary">
97+
<Trans i18nKey="ldap-drawer.attributes-section.description">
98+
Specify the LDAP attributes that map to the user&lsquo;s given name, surname, and email address, ensuring
99+
the application correctly retrieves and displays user information.
100+
</Trans>
101+
</Text>
102+
<Field htmlFor="name" label={t('ldap-drawer.attributes-section.name.label', 'Name')}>
103+
<Input id="name" {...register('settings.config.servers.0.attributes.name')} />
104+
</Field>
105+
<Field htmlFor="surname" label={t('ldap-drawer.attributes-section.surname.label', 'Surname')}>
106+
<Input id="surname" {...register('settings.config.servers.0.attributes.surname')} />
107+
</Field>
108+
<Field htmlFor="username" label={t('ldap-drawer.attributes-section.username.label', 'Username')}>
109+
<Input id="username" {...register('settings.config.servers.0.attributes.username')} />
110+
</Field>
111+
<Field htmlFor="member-of" label={t('ldap-drawer.attributes-section.member-of.label', 'Member Of')}>
112+
<Input id="member-of" {...register('settings.config.servers.0.attributes.member_of')} />
113+
</Field>
114+
<Field htmlFor="email" label={t('ldap-drawer.attributes-section.email.label', 'Email')}>
115+
<Input id="email" {...register('settings.config.servers.0.attributes.email')} />
116+
</Field>
117+
</CollapsableSection>
118+
<CollapsableSection label={groupMappingsLabel} isOpen={true}>
119+
<Field
120+
htmlFor="skip-org-role-sync"
121+
label={t('ldap-drawer.group-mapping-section.skip-org-role-sync.label', 'Skip organization role sync')}
122+
description={t(
123+
'ldap-drawer.group-mapping-section.skip-org-role-sync.description',
124+
'Prevent synchronizing users’ organization roles from your IdP'
125+
)}
126+
>
127+
<Switch id="skip-org-role-sync" {...register('settings.config.servers.0.skip_org_role_sync')} />
128+
</Field>
129+
<Field
130+
htmlFor="group-search-filter"
131+
label={t('ldap-drawer.group-mapping-section.group-search-filter.label', 'Group search filter')}
132+
description={t(
133+
'ldap-drawer.group-mapping-section.group-search-filter.description',
134+
'Used to filter and identify group entries within the directory'
135+
)}
136+
>
137+
<Input id="group-search-filter" {...register('settings.config.servers.0.group_search_filter')} />
138+
</Field>
139+
<Field
140+
htmlFor="group-search-base-dns"
141+
label={t('ldap-drawer.group-mapping-section.group-search-base-dns.label', 'Group search base DNS')}
142+
description={t(
143+
'ldap-drawer.group-mapping-section.group-search-base-dns.description',
144+
'Separate by commas or spaces'
145+
)}
146+
>
147+
<Input
148+
id="group-search-base-dns"
149+
onChange={({ currentTarget: { value } }) =>
150+
setValue('settings.config.servers.0.group_search_base_dns', [value])
151+
}
152+
/>
153+
</Field>
154+
<Field
155+
htmlFor="group-search-filter-user-attribute"
156+
label={t(
157+
'ldap-drawer.group-mapping-section.group-search-filter-user-attribute.label',
158+
'Group name attribute'
159+
)}
160+
description={t(
161+
'ldap-drawer.group-mapping-section.group-search-filter-user-attribute.description',
162+
'Identifies users within group entries for filtering purposes'
163+
)}
164+
>
165+
<Input
166+
id="group-search-filter-user-attribute"
167+
{...register('settings.config.servers.0.group_search_filter_user_attribute')}
168+
/>
169+
</Field>
170+
<Divider />
171+
</CollapsableSection>
172+
<CollapsableSection
173+
label={t('ldap-drawer.extra-security-section.label', 'Extra security measures')}
174+
isOpen={true}
175+
>
176+
<Field
177+
label={t('ldap-drawer.extra-security-section.use-ssl.label', 'Use SSL')}
178+
description={t(
179+
'ldap-drawer.extra-security-section.use-ssl.description',
180+
'Set to true if LDAP server should use TLS connection (either with STARTTLS or LDAPS)'
181+
)}
182+
>
183+
<Stack>
184+
<Switch id="use-ssl" {...register('settings.config.servers.0.use_ssl')} />
185+
<Tooltip content={useTlsDescription} interactive>
186+
<Icon name="info-circle" />
187+
</Tooltip>
188+
</Stack>
189+
</Field>
190+
{watch('settings.config.servers.0.use_ssl') && (
191+
<>
192+
<Field
193+
label={t('ldap-drawer.extra-security-section.start-tls.label', 'Start TLS')}
194+
description={t(
195+
'ldap-drawer.extra-security-section.start-tls.description',
196+
'If set to true, use LDAP with STARTTLS instead of LDAPS'
197+
)}
198+
>
199+
<Switch id="start-tls" {...register('settings.config.servers.0.start_tls')} />
200+
</Field>
201+
<Field
202+
htmlFor="min-tls-version"
203+
label={t('ldap-drawer.extra-security-section.min-tls-version.label', 'Min TLS version')}
204+
description={t(
205+
'ldap-drawer.extra-security-section.min-tls-version.description',
206+
'This is the minimum TLS version allowed. Accepted values are: TLS1.2, TLS1.3.'
207+
)}
208+
>
209+
<Select
210+
id="min-tls-version"
211+
options={tlsOptions}
212+
value={watch('settings.config.servers.0.min_tls_version')}
213+
onChange={({ value }) => setValue('settings.config.servers.0.min_tls_version', value)}
214+
/>
215+
</Field>
216+
<Field
217+
label={t('ldap-drawer.extra-security-section.tls-ciphers.label', 'TLS ciphers')}
218+
description={t(
219+
'ldap-drawer.extra-security-section.tls-ciphers.description',
220+
'List of comma- or space-separated ciphers'
221+
)}
222+
>
223+
<Input
224+
id="tls-ciphers"
225+
placeholder={t(
226+
'ldap-drawer.extra-security-section.tls-ciphers.placeholder',
227+
'e.g. ["TLS_AES_256_GCM_SHA384"]'
228+
)}
229+
value={watch('settings.config.servers.0.tls_ciphers')}
230+
onChange={({ currentTarget: { value } }) =>
231+
setValue(
232+
'settings.config.servers.0.tls_ciphers',
233+
value?.split(/,|\s/).map((v: string) => v.trim())
234+
)
235+
}
236+
/>
237+
</Field>
238+
</>
239+
)}
240+
</CollapsableSection>
241+
</Drawer>
242+
);
243+
};
244+
245+
function getStyles(theme: GrafanaTheme2) {
246+
return {
247+
sectionLabel: css({
248+
fontSize: theme.typography.size.lg,
249+
}),
250+
};
251+
}

public/app/features/admin/ldap/LdapSettingsPage.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { connect } from 'react-redux';
55

66
import { AppEvents, GrafanaTheme2, NavModelItem } from '@grafana/data';
77
import { getBackendSrv, getAppEvents } from '@grafana/runtime';
8-
import { useStyles2, Alert, Box, Button, Field, Input, Stack, TextLink } from '@grafana/ui';
8+
import { useStyles2, Alert, Box, Button, Field, Input, Stack, Text, TextLink } from '@grafana/ui';
99
import { Page } from 'app/core/components/Page/Page';
1010
import config from 'app/core/config';
1111
import { t, Trans } from 'app/core/internationalization';
1212
import { Loader } from 'app/features/plugins/admin/components/Loader';
1313
import { LdapPayload, StoreState } from 'app/types';
1414

15+
import { LdapDrawerComponent } from './LdapDrawer';
16+
1517
const appEvents = getAppEvents();
1618

1719
const mapStateToProps = (state: StoreState) => ({
@@ -71,6 +73,7 @@ const emptySettings: LdapPayload = {
7173

7274
export const LdapSettingsPage = () => {
7375
const [isLoading, setIsLoading] = useState(true);
76+
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
7477

7578
const methods = useForm<LdapPayload>({ defaultValues: emptySettings });
7679
const { getValues, handleSubmit, register, reset } = methods;
@@ -161,6 +164,10 @@ export const LdapSettingsPage = () => {
161164
});
162165
return;
163166
}
167+
appEvents.publish({
168+
type: AppEvents.alertSuccess.name,
169+
payload: [t('ldap-settings-page.alert.discard-success', 'LDAP settings discarded')],
170+
});
164171
reset(payload);
165172
} catch (error) {
166173
appEvents.publish({
@@ -261,6 +268,23 @@ export const LdapSettingsPage = () => {
261268
{...register('settings.config.servers.0.search_base_dns', { required: true })}
262269
/>
263270
</Field>
271+
<Box borderColor="strong" borderStyle="solid" padding={2} width={68}>
272+
<Stack alignItems={'center'} direction={'row'} gap={2} justifyContent={'space-between'}>
273+
<Stack alignItems={'start'} direction={'column'}>
274+
<Text element="h2">
275+
<Trans i18nKey="ldap-settings-page.advanced-settings-section.title">Advanced Settings</Trans>
276+
</Text>
277+
<Text>
278+
<Trans i18nKey="ldap-settings-page.advanced-settings-section.subtitle">
279+
Mappings, extra security measures, and more.
280+
</Trans>
281+
</Text>
282+
</Stack>
283+
<Button variant="secondary" onClick={() => setIsDrawerOpen(true)}>
284+
<Trans i18nKey="ldap-settings-page.advanced-settings-section.edit.button">Edit</Trans>
285+
</Button>
286+
</Stack>
287+
</Box>
264288
<Box display={'flex'} gap={2} marginTop={5}>
265289
<Stack alignItems={'center'} gap={2}>
266290
<Button type={'submit'}>
@@ -276,6 +300,7 @@ export const LdapSettingsPage = () => {
276300
</Box>
277301
</section>
278302
)}
303+
{isDrawerOpen && <LdapDrawerComponent onClose={() => setIsDrawerOpen(false)} />}
279304
</form>
280305
</FormProvider>
281306
</Page.Contents>

public/app/types/ldap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export interface LdapServerConfig {
9090
group_search_filter: string;
9191
group_search_filter_user_attribute: string;
9292
host: string;
93-
min_tls_version: string;
93+
min_tls_version?: string;
9494
port: number;
9595
root_ca_cert: string;
9696
search_base_dns: string[];

0 commit comments

Comments
 (0)