SSO LDAP: Add LDAP settings drawer (#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
This commit is contained in:
linoman 2024-08-22 17:16:18 +02:00 committed by GitHub
parent 922babb157
commit 9fbfaf08a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 454 additions and 2 deletions

View File

@ -0,0 +1,251 @@
import { css } from '@emotion/css';
import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import {
useStyles2,
CollapsableSection,
Divider,
Drawer,
Field,
Icon,
Input,
Label,
Select,
Stack,
Switch,
Text,
TextLink,
Tooltip,
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { LdapPayload } from 'app/types';
interface Props {
onClose: () => void;
}
const tlsOptions: Array<SelectableValue<string>> = ['TLS1.2', 'TLS1.3'].map((v) => ({ label: v, value: v }));
export const LdapDrawerComponent = ({ onClose }: Props) => {
const styles = useStyles2(getStyles);
const { register, setValue, watch } = useFormContext<LdapPayload>();
const groupMappingsLabel = (
<Label
className={styles.sectionLabel}
description={t('ldap-drawer.group-mapping-section.description', 'Map LDAP groups to Grafana org roles')}
>
<Trans i18nKey="ldap-drawer.group-mapping-section.label">Group mapping</Trans>
</Label>
);
const useTlsDescription = (
<Trans i18nKey="ldap-drawer.extra-security-section.use-ssl.tooltip">
For a complete list of supported ciphers and TLS versions, refer to:{' '}
{
<TextLink style={{ fontSize: 'inherit' }} href="https://go.dev/src/crypto/tls/cipher_suites.go" external>
https://go.dev/src/crypto/tls/cipher_suites.go
</TextLink>
}
</Trans>
);
return (
<Drawer title={t('ldap-drawer.title', 'Advanced Settings')} onClose={onClose}>
<CollapsableSection label={t('ldap-drawer.misc-section.label', 'Misc')} isOpen={true}>
<Field
label={t('ldap-drawer.misc-section.allow-sign-up.label', 'Allow sign up')}
description={t(
'ldap-drawer.misc-section.allow-sign-up.descrition',
'If not enabled, only existing Grafana users can log in using LDAP'
)}
>
<Switch id="allow-sign-up" {...register('settings.allowSignUp')} />
</Field>
<Field
label={t('ldap-drawer.misc-section.port.label', 'Port')}
description={t(
'ldap-drawer.misc-section.port.description',
'Default port is 389 without SSL or 636 with SSL'
)}
>
<Input
id="port"
placeholder={t('ldap-drawer.misc-section.port.placeholder', '389')}
type="number"
{...register('settings.config.servers.0.port', { valueAsNumber: true })}
/>
</Field>
<Field
label={t('ldap-drawer.misc-section.timeout.label', 'Timeout')}
description={t(
'ldap-drawer.misc-section.timeout.description',
'Timeout in seconds for the connection to the LDAP server'
)}
>
<Input
id="timeout"
placeholder={t('ldap-drawer.misc-section.timeout.placeholder', '389')}
type="number"
{...register('settings.config.servers.0.timeout', { valueAsNumber: true })}
/>
</Field>
</CollapsableSection>
<CollapsableSection label={t('ldap-drawer.attributes-section.label', 'Attributes')} isOpen={true}>
<Text color="secondary">
<Trans i18nKey="ldap-drawer.attributes-section.description">
Specify the LDAP attributes that map to the user&lsquo;s given name, surname, and email address, ensuring
the application correctly retrieves and displays user information.
</Trans>
</Text>
<Field htmlFor="name" label={t('ldap-drawer.attributes-section.name.label', 'Name')}>
<Input id="name" {...register('settings.config.servers.0.attributes.name')} />
</Field>
<Field htmlFor="surname" label={t('ldap-drawer.attributes-section.surname.label', 'Surname')}>
<Input id="surname" {...register('settings.config.servers.0.attributes.surname')} />
</Field>
<Field htmlFor="username" label={t('ldap-drawer.attributes-section.username.label', 'Username')}>
<Input id="username" {...register('settings.config.servers.0.attributes.username')} />
</Field>
<Field htmlFor="member-of" label={t('ldap-drawer.attributes-section.member-of.label', 'Member Of')}>
<Input id="member-of" {...register('settings.config.servers.0.attributes.member_of')} />
</Field>
<Field htmlFor="email" label={t('ldap-drawer.attributes-section.email.label', 'Email')}>
<Input id="email" {...register('settings.config.servers.0.attributes.email')} />
</Field>
</CollapsableSection>
<CollapsableSection label={groupMappingsLabel} isOpen={true}>
<Field
htmlFor="skip-org-role-sync"
label={t('ldap-drawer.group-mapping-section.skip-org-role-sync.label', 'Skip organization role sync')}
description={t(
'ldap-drawer.group-mapping-section.skip-org-role-sync.description',
'Prevent synchronizing users organization roles from your IdP'
)}
>
<Switch id="skip-org-role-sync" {...register('settings.config.servers.0.skip_org_role_sync')} />
</Field>
<Field
htmlFor="group-search-filter"
label={t('ldap-drawer.group-mapping-section.group-search-filter.label', 'Group search filter')}
description={t(
'ldap-drawer.group-mapping-section.group-search-filter.description',
'Used to filter and identify group entries within the directory'
)}
>
<Input id="group-search-filter" {...register('settings.config.servers.0.group_search_filter')} />
</Field>
<Field
htmlFor="group-search-base-dns"
label={t('ldap-drawer.group-mapping-section.group-search-base-dns.label', 'Group search base DNS')}
description={t(
'ldap-drawer.group-mapping-section.group-search-base-dns.description',
'Separate by commas or spaces'
)}
>
<Input
id="group-search-base-dns"
onChange={({ currentTarget: { value } }) =>
setValue('settings.config.servers.0.group_search_base_dns', [value])
}
/>
</Field>
<Field
htmlFor="group-search-filter-user-attribute"
label={t(
'ldap-drawer.group-mapping-section.group-search-filter-user-attribute.label',
'Group name attribute'
)}
description={t(
'ldap-drawer.group-mapping-section.group-search-filter-user-attribute.description',
'Identifies users within group entries for filtering purposes'
)}
>
<Input
id="group-search-filter-user-attribute"
{...register('settings.config.servers.0.group_search_filter_user_attribute')}
/>
</Field>
<Divider />
</CollapsableSection>
<CollapsableSection
label={t('ldap-drawer.extra-security-section.label', 'Extra security measures')}
isOpen={true}
>
<Field
label={t('ldap-drawer.extra-security-section.use-ssl.label', 'Use SSL')}
description={t(
'ldap-drawer.extra-security-section.use-ssl.description',
'Set to true if LDAP server should use TLS connection (either with STARTTLS or LDAPS)'
)}
>
<Stack>
<Switch id="use-ssl" {...register('settings.config.servers.0.use_ssl')} />
<Tooltip content={useTlsDescription} interactive>
<Icon name="info-circle" />
</Tooltip>
</Stack>
</Field>
{watch('settings.config.servers.0.use_ssl') && (
<>
<Field
label={t('ldap-drawer.extra-security-section.start-tls.label', 'Start TLS')}
description={t(
'ldap-drawer.extra-security-section.start-tls.description',
'If set to true, use LDAP with STARTTLS instead of LDAPS'
)}
>
<Switch id="start-tls" {...register('settings.config.servers.0.start_tls')} />
</Field>
<Field
htmlFor="min-tls-version"
label={t('ldap-drawer.extra-security-section.min-tls-version.label', 'Min TLS version')}
description={t(
'ldap-drawer.extra-security-section.min-tls-version.description',
'This is the minimum TLS version allowed. Accepted values are: TLS1.2, TLS1.3.'
)}
>
<Select
id="min-tls-version"
options={tlsOptions}
value={watch('settings.config.servers.0.min_tls_version')}
onChange={({ value }) => setValue('settings.config.servers.0.min_tls_version', value)}
/>
</Field>
<Field
label={t('ldap-drawer.extra-security-section.tls-ciphers.label', 'TLS ciphers')}
description={t(
'ldap-drawer.extra-security-section.tls-ciphers.description',
'List of comma- or space-separated ciphers'
)}
>
<Input
id="tls-ciphers"
placeholder={t(
'ldap-drawer.extra-security-section.tls-ciphers.placeholder',
'e.g. ["TLS_AES_256_GCM_SHA384"]'
)}
value={watch('settings.config.servers.0.tls_ciphers')}
onChange={({ currentTarget: { value } }) =>
setValue(
'settings.config.servers.0.tls_ciphers',
value?.split(/,|\s/).map((v: string) => v.trim())
)
}
/>
</Field>
</>
)}
</CollapsableSection>
</Drawer>
);
};
function getStyles(theme: GrafanaTheme2) {
return {
sectionLabel: css({
fontSize: theme.typography.size.lg,
}),
};
}

View File

@ -5,13 +5,15 @@ 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 { useStyles2, Alert, Box, Button, Field, Input, Stack, Text, 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';
import { LdapDrawerComponent } from './LdapDrawer';
const appEvents = getAppEvents();
const mapStateToProps = (state: StoreState) => ({
@ -71,6 +73,7 @@ const emptySettings: LdapPayload = {
export const LdapSettingsPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const methods = useForm<LdapPayload>({ defaultValues: emptySettings });
const { getValues, handleSubmit, register, reset } = methods;
@ -161,6 +164,10 @@ export const LdapSettingsPage = () => {
});
return;
}
appEvents.publish({
type: AppEvents.alertSuccess.name,
payload: [t('ldap-settings-page.alert.discard-success', 'LDAP settings discarded')],
});
reset(payload);
} catch (error) {
appEvents.publish({
@ -261,6 +268,23 @@ export const LdapSettingsPage = () => {
{...register('settings.config.servers.0.search_base_dns', { required: true })}
/>
</Field>
<Box borderColor="strong" borderStyle="solid" padding={2} width={68}>
<Stack alignItems={'center'} direction={'row'} gap={2} justifyContent={'space-between'}>
<Stack alignItems={'start'} direction={'column'}>
<Text element="h2">
<Trans i18nKey="ldap-settings-page.advanced-settings-section.title">Advanced Settings</Trans>
</Text>
<Text>
<Trans i18nKey="ldap-settings-page.advanced-settings-section.subtitle">
Mappings, extra security measures, and more.
</Trans>
</Text>
</Stack>
<Button variant="secondary" onClick={() => setIsDrawerOpen(true)}>
<Trans i18nKey="ldap-settings-page.advanced-settings-section.edit.button">Edit</Trans>
</Button>
</Stack>
</Box>
<Box display={'flex'} gap={2} marginTop={5}>
<Stack alignItems={'center'} gap={2}>
<Button type={'submit'}>
@ -276,6 +300,7 @@ export const LdapSettingsPage = () => {
</Box>
</section>
)}
{isDrawerOpen && <LdapDrawerComponent onClose={() => setIsDrawerOpen(false)} />}
</form>
</FormProvider>
</Page.Contents>

View File

@ -90,7 +90,7 @@ export interface LdapServerConfig {
group_search_filter: string;
group_search_filter_user_attribute: string;
host: string;
min_tls_version: string;
min_tls_version?: string;
port: number;
root_ca_cert: string;
search_base_dns: string[];

View File

@ -984,8 +984,96 @@
"refresh": "Refresh"
}
},
"ldap-drawer": {
"attributes-section": {
"description": "Specify the LDAP attributes that map to the user&lsquo;s given name, surname, and email address, ensuring the application correctly retrieves and displays user information.",
"email": {
"label": "Email"
},
"label": "Attributes",
"member-of": {
"label": "Member Of"
},
"name": {
"label": "Name"
},
"surname": {
"label": "Surname"
},
"username": {
"label": "Username"
}
},
"extra-security-section": {
"label": "Extra security measures",
"min-tls-version": {
"description": "This is the minimum TLS version allowed. Accepted values are: TLS1.2, TLS1.3.",
"label": "Min TLS version"
},
"start-tls": {
"description": "If set to true, use LDAP with STARTTLS instead of LDAPS",
"label": "Start TLS"
},
"tls-ciphers": {
"description": "List of comma- or space-separated ciphers",
"label": "TLS ciphers",
"placeholder": "e.g. [\"TLS_AES_256_GCM_SHA384\"]"
},
"use-ssl": {
"description": "Set to true if LDAP server should use TLS connection (either with STARTTLS or LDAPS)",
"label": "Use SSL",
"tooltip": "For a complete list of supported ciphers and TLS versions, refer to: {\n <TextLink style={{ fontSize: 'inherit' }} href=\"https://go.dev/src/crypto/tls/cipher_suites.go\" external>\n https://go.dev/src/crypto/tls/cipher_suites.go\n </TextLink>}"
}
},
"group-mapping-section": {
"description": "Map LDAP groups to Grafana org roles",
"group-search-base-dns": {
"description": "Separate by commas or spaces",
"label": "Group search base DNS"
},
"group-search-filter": {
"description": "Used to filter and identify group entries within the directory",
"label": "Group search filter"
},
"group-search-filter-user-attribute": {
"description": "Identifies users within group entries for filtering purposes",
"label": "Group name attribute"
},
"label": "Group mapping",
"skip-org-role-sync": {
"description": "Prevent synchronizing users organization roles from your IdP",
"label": "Skip organization role sync"
}
},
"misc-section": {
"allow-sign-up": {
"descrition": "If not enabled, only existing Grafana users can log in using LDAP",
"label": "Allow sign up"
},
"label": "Misc",
"port": {
"description": "Default port is 389 without SSL or 636 with SSL",
"label": "Port",
"placeholder": "389"
},
"timeout": {
"description": "Timeout in seconds for the connection to the LDAP server",
"label": "Timeout",
"placeholder": "389"
}
},
"title": "Advanced Settings"
},
"ldap-settings-page": {
"advanced-settings-section": {
"edit": {
"button": "Edit"
},
"subtitle": "Mappings, extra security measures, and more.",
"title": "Advanced Settings"
},
"alert": {
"discard-success": "LDAP settings discarded",
"error-fetching": "Error fetching LDAP settings",
"error-saving": "Error saving LDAP settings",
"error-update": "Error updating LDAP settings",

View File

@ -984,8 +984,96 @@
"refresh": "Ŗęƒřęşĥ"
}
},
"ldap-drawer": {
"attributes-section": {
"description": "Ŝpęčįƒy ŧĥę ĿĐÅP äŧŧřįþūŧęş ŧĥäŧ mäp ŧő ŧĥę ūşęř&ľşqūő;ş ģįvęʼn ʼnämę, şūřʼnämę, äʼnđ ęmäįľ äđđřęşş, ęʼnşūřįʼnģ ŧĥę äppľįčäŧįőʼn čőřřęčŧľy řęŧřįęvęş äʼnđ đįşpľäyş ūşęř įʼnƒőřmäŧįőʼn.",
"email": {
"label": "Ēmäįľ"
},
"label": "Åŧŧřįþūŧęş",
"member-of": {
"label": "Męmþęř ؃"
},
"name": {
"label": "Ńämę"
},
"surname": {
"label": "Ŝūřʼnämę"
},
"username": {
"label": "Ůşęřʼnämę"
}
},
"extra-security-section": {
"label": "Ēχŧřä şęčūřįŧy męäşūřęş",
"min-tls-version": {
"description": "Ŧĥįş įş ŧĥę mįʼnįmūm ŦĿŜ vęřşįőʼn äľľőŵęđ. Åččępŧęđ väľūęş äřę: ŦĿŜ1.2, ŦĿŜ1.3.",
"label": "Mįʼn ŦĿŜ vęřşįőʼn"
},
"start-tls": {
"description": "Ĩƒ şęŧ ŧő ŧřūę, ūşę ĿĐÅP ŵįŧĥ ŜŦÅŖŦŦĿŜ įʼnşŧęäđ őƒ ĿĐÅPŜ",
"label": "Ŝŧäřŧ ŦĿŜ"
},
"tls-ciphers": {
"description": "Ŀįşŧ őƒ čőmmä- őř şpäčę-şępäřäŧęđ čįpĥęřş",
"label": "ŦĿŜ čįpĥęřş",
"placeholder": "ę.ģ. [\"ŦĿŜ_ÅĒŜ_256_ĞCM_ŜĦÅ384\"]"
},
"use-ssl": {
"description": "Ŝęŧ ŧő ŧřūę įƒ ĿĐÅP şęřvęř şĥőūľđ ūşę ŦĿŜ čőʼnʼnęčŧįőʼn (ęįŧĥęř ŵįŧĥ ŜŦÅŖŦŦĿŜ őř ĿĐÅPŜ)",
"label": "Ůşę ŜŜĿ",
"tooltip": "Főř ä čőmpľęŧę ľįşŧ őƒ şūppőřŧęđ čįpĥęřş äʼnđ ŦĿŜ vęřşįőʼnş, řęƒęř ŧő: {\n <ŦęχŧĿįʼnĸ şŧyľę={{ fontSize: 'inherit' }} ĥřęƒ=\"ĥŧŧpş://ģő.đęv/şřč/čřypŧő/ŧľş/čįpĥęř_şūįŧęş.ģő\" ęχŧęřʼnäľ>\n ĥŧŧpş://ģő.đęv/şřč/čřypŧő/ŧľş/čįpĥęř_şūįŧęş.ģő\n </ŦęχŧĿįʼnĸ>}"
}
},
"group-mapping-section": {
"description": "Mäp ĿĐÅP ģřőūpş ŧő Ğřäƒäʼnä őřģ řőľęş",
"group-search-base-dns": {
"description": "Ŝępäřäŧę þy čőmmäş őř şpäčęş",
"label": "Ğřőūp şęäřčĥ þäşę ĐŃŜ"
},
"group-search-filter": {
"description": "Ůşęđ ŧő ƒįľŧęř äʼnđ įđęʼnŧįƒy ģřőūp ęʼnŧřįęş ŵįŧĥįʼn ŧĥę đįřęčŧőřy",
"label": "Ğřőūp şęäřčĥ ƒįľŧęř"
},
"group-search-filter-user-attribute": {
"description": "Ĩđęʼnŧįƒįęş ūşęřş ŵįŧĥįʼn ģřőūp ęʼnŧřįęş ƒőř ƒįľŧęřįʼnģ pūřpőşęş",
"label": "Ğřőūp ʼnämę äŧŧřįþūŧę"
},
"label": "Ğřőūp mäppįʼnģ",
"skip-org-role-sync": {
"description": "Přęvęʼnŧ şyʼnčĥřőʼnįžįʼnģ ūşęřş’ őřģäʼnįžäŧįőʼn řőľęş ƒřőm yőūř ĨđP",
"label": "Ŝĸįp őřģäʼnįžäŧįőʼn řőľę şyʼnč"
}
},
"misc-section": {
"allow-sign-up": {
"descrition": "Ĩƒ ʼnőŧ ęʼnäþľęđ, őʼnľy ęχįşŧįʼnģ Ğřäƒäʼnä ūşęřş čäʼn ľőģ įʼn ūşįʼnģ ĿĐÅP",
"label": "Åľľőŵ şįģʼn ūp"
},
"label": "Mįşč",
"port": {
"description": "Đęƒäūľŧ pőřŧ įş 389 ŵįŧĥőūŧ ŜŜĿ őř 636 ŵįŧĥ ŜŜĿ",
"label": "Pőřŧ",
"placeholder": "389"
},
"timeout": {
"description": "Ŧįmęőūŧ įʼn şęčőʼnđş ƒőř ŧĥę čőʼnʼnęčŧįőʼn ŧő ŧĥę ĿĐÅP şęřvęř",
"label": "Ŧįmęőūŧ",
"placeholder": "389"
}
},
"title": "Åđväʼnčęđ Ŝęŧŧįʼnģş"
},
"ldap-settings-page": {
"advanced-settings-section": {
"edit": {
"button": "Ēđįŧ"
},
"subtitle": "Mäppįʼnģş, ęχŧřä şęčūřįŧy męäşūřęş, äʼnđ mőřę.",
"title": "Åđväʼnčęđ Ŝęŧŧįʼnģş"
},
"alert": {
"discard-success": "ĿĐÅP şęŧŧįʼnģş đįşčäřđęđ",
"error-fetching": "Ēřřőř ƒęŧčĥįʼnģ ĿĐÅP şęŧŧįʼnģş",
"error-saving": "Ēřřőř şävįʼnģ ĿĐÅP şęŧŧįʼnģş",
"error-update": "Ēřřőř ūpđäŧįʼnģ ĿĐÅP şęŧŧįʼnģş",