The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/admin/ldap/LdapDrawer.tsx

462 lines
18 KiB

import { css } from '@emotion/css';
import { Dispatch, SetStateAction, useEffect, useId, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import {
useStyles2,
Button,
CollapsableSection,
Divider,
Drawer,
Field,
Icon,
Input,
Label,
MultiSelect,
Select,
Stack,
Switch,
TextLink,
Tooltip,
RadioButtonGroup,
SecretInput,
} from '@grafana/ui';
import { LdapPayload, MapKeyCertConfigured } from 'app/types';
import { GroupMappingComponent } from './LdapGroupMapping';
interface Props {
onClose: () => void;
mapKeyCertConfigured: MapKeyCertConfigured;
setMapKeyCertConfigured: Dispatch<SetStateAction<MapKeyCertConfigured>>;
}
const serverConfig = 'settings.config.servers.0';
const tlsOptions: Array<SelectableValue<string>> = ['TLS1.2', 'TLS1.3'].map((v) => ({ label: v, value: v }));
enum EncryptionProvider {
Base64 = 'base64',
FilePath = 'path',
}
export const LdapDrawerComponent = ({
onClose,
mapKeyCertConfigured: mapCertConfigured,
setMapKeyCertConfigured: setMapCertConfigured,
}: Props) => {
const [encryptionProvider, setEncryptionProvider] = useState(EncryptionProvider.Base64);
const styles = useStyles2(getStyles);
const { control, getValues, register, setValue, watch } = useFormContext<LdapPayload>();
const nameId = useId();
const surnameId = useId();
const usernameId = useId();
const memberOfId = useId();
const emailId = useId();
useEffect(() => {
const { client_cert, client_key, root_ca_cert } = getValues(serverConfig);
setEncryptionProvider(
!client_cert?.length && !client_key?.length && !root_ca_cert?.length
? EncryptionProvider.Base64
: EncryptionProvider.FilePath
);
}, [getValues]);
const { t } = useTranslate();
const renderMultiSelectLabel = (value: string) => {
if (value.length >= 5) {
return `${value.slice(0, 2)}...${value.slice(-2)}`;
}
return value;
};
const attributesLabel = (
<Label
className={styles.sectionLabel}
description={t(
'ldap-drawer.attributes-section.description',
"Specify the LDAP attributes that map to the user's given name, surname, and email address, ensuring the application correctly retrieves and displays user information."
)}
>
<Trans i18nKey="ldap-drawer.attributes-section.label">Attributes</Trans>
</Label>
);
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:
</Trans>{' '}
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
<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>
</>
);
const onAddGroupMapping = () => {
const groupMappings = getValues(`${serverConfig}.group_mappings`) || [];
setValue(`${serverConfig}.group_mappings`, [
...groupMappings,
{
group_dn: '',
org_id: 1,
org_role: 'Viewer',
grafana_admin: false,
},
]);
};
const onRemoveGroupMapping = (index: number) => {
const groupMappings = getValues(`${serverConfig}.group_mappings`);
setValue(`${serverConfig}.group_mappings`, [...groupMappings.slice(0, index), ...groupMappings.slice(index + 1)]);
};
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"
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="389"
type="number"
{...register(`${serverConfig}.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"
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="10"
type="number"
{...register(`${serverConfig}.timeout`, { valueAsNumber: true })}
/>
</Field>
</CollapsableSection>
<CollapsableSection label={attributesLabel} isOpen={true}>
<Field label={t('ldap-drawer.attributes-section.name-label', 'Name')}>
<Input id={nameId} {...register(`${serverConfig}.attributes.name`)} />
</Field>
<Field label={t('ldap-drawer.attributes-section.surname-label', 'Surname')}>
<Input id={surnameId} {...register(`${serverConfig}.attributes.surname`)} />
</Field>
<Field label={t('ldap-drawer.attributes-section.username-label', 'Username')}>
<Input id={usernameId} {...register(`${serverConfig}.attributes.username`)} />
</Field>
<Field label={t('ldap-drawer.attributes-section.member-of-label', 'Member Of')}>
<Input id={memberOfId} {...register(`${serverConfig}.attributes.member_of`)} />
</Field>
<Field label={t('ldap-drawer.attributes-section.email-label', 'Email')}>
<Input id={emailId} {...register(`${serverConfig}.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(`${serverConfig}.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(`${serverConfig}.group_search_filter`)} />
</Field>
<Field label={t('ldap-drawer.group-mapping-section.group-search-base-dns-label', 'Group search base DNS')}>
<Controller
name={`${serverConfig}.group_search_base_dns`}
control={control}
render={({ field: { onChange, ref, value, ...field } }) => (
<MultiSelect
{...field}
allowCustomValue
className={styles.multiSelect}
noOptionsMessage=""
placeholder={t(
'ldap-drawer.group-mapping-section.group-search-base-dns-placeholder',
'example: ou=groups,dc=example,dc=com'
)}
onChange={(v) => onChange(v.map(({ value }) => String(value)))}
value={value?.map((v) => ({ label: v, value: v }))}
/>
)}
/>
</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(`${serverConfig}.group_search_filter_user_attribute`)}
/>
</Field>
{watch('settings.config.servers.0.group_mappings')?.map((_, i) => {
return <GroupMappingComponent key={i} groupMappingIndex={i} onRemove={() => onRemoveGroupMapping(i)} />;
})}
<Divider />
<Button className={styles.button} variant="secondary" icon="plus" onClick={() => onAddGroupMapping()}>
<Trans i18nKey="ldap-drawer.group-mapping-section.add.button">Add group mapping</Trans>
</Button>
</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(`${serverConfig}.use_ssl`)} />
<Tooltip content={useTlsDescription} interactive>
<Icon name="info-circle" />
</Tooltip>
</Stack>
</Field>
{watch(`${serverConfig}.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(`${serverConfig}.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(`${serverConfig}.min_tls_version`)}
onChange={({ value }) => setValue(`${serverConfig}.min_tls_version`, value)}
/>
</Field>
<Field label={t('ldap-drawer.extra-security-section.tls-ciphers-label', 'TLS ciphers')}>
<Controller
name={`${serverConfig}.tls_ciphers`}
control={control}
render={({ field: { onChange, ref, value, ...field } }) => (
<MultiSelect
{...field}
allowCustomValue
className={styles.multiSelect}
noOptionsMessage=""
placeholder={t(
'ldap-drawer.extra-security-section.tls-ciphers-placeholder',
'example: TLS_AES_256_GCM_SHA384'
)}
onChange={(v) => onChange(v.map(({ value }) => String(value)))}
value={value?.map((v) => ({ label: v, value: v }))}
/>
)}
/>
</Field>
<Field
label={t(
'ldap-drawer.extra-security-section.encryption-provider-label',
'Encryption key and certificate provision specification.'
)}
description={t(
'ldap-drawer.extra-security-section.encryption-provider-description',
'X.509 certificate provides the public part, while the private key issued in a PKCS#8 format provides the private part of the asymmetric encryption.'
)}
>
<RadioButtonGroup
id="encryption-provider"
options={[
{
label: t(
'ldap-drawer.extra-security-section.encryption-provider-base-64',
'Base64-encoded content'
),
value: EncryptionProvider.Base64,
},
{
label: t('ldap-drawer.extra-security-section.encryption-provider-file-path', 'Path to files'),
value: EncryptionProvider.FilePath,
},
]}
value={encryptionProvider}
onChange={setEncryptionProvider}
/>
</Field>
{encryptionProvider === EncryptionProvider.Base64 && (
<>
<Field
label={t(
'ldap-drawer.extra-security-section.root-ca-cert-value-label',
'Root CA certificate content'
)}
>
<Controller
name={`${serverConfig}.root_ca_cert_value`}
control={control}
render={({ field: { onChange, ref, value, ...field } }) => (
<MultiSelect
{...field}
allowCustomValue
className={styles.multiSelect}
noOptionsMessage=""
placeholder={t(
'ldap-drawer.extra-security-section.root-ca-cert-value-placeholder',
'example: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t'
)}
onChange={(v) => onChange(v.map(({ value }) => String(value)))}
value={value?.map((v) => ({ label: renderMultiSelectLabel(v), value: v }))}
/>
)}
/>
</Field>
<Field
label={t('ldap-drawer.extra-security-section.client-cert-value-label', 'Client certificate content')}
>
<Input
id="client-cert"
placeholder={t(
'ldap-drawer.extra-security-section.client-cert-value-placeholder',
'Client certificate content in base64'
)}
type="text"
{...register(`${serverConfig}.client_cert_value`)}
/>
</Field>
<Field label={t('ldap-drawer.extra-security-section.client-key-value-label', 'Client key content')}>
<SecretInput
id="client-key"
placeholder={t(
'ldap-drawer.extra-security-section.client-key-value-placeholder',
'Client key content in base64'
)}
isConfigured={mapCertConfigured.clientKeyCertValue}
onReset={() => {
setValue(`${serverConfig}.client_key_value`, '');
setMapCertConfigured({ ...mapCertConfigured, clientKeyCertValue: false });
}}
/>
</Field>
</>
)}
{encryptionProvider === EncryptionProvider.FilePath && (
<>
<Field label={t('ldap-drawer.extra-security-section.root-ca-cert-label', 'Root CA certificate path')}>
<Input
id="root-ca-cert"
placeholder={t(
'ldap-drawer.extra-security-section.root-ca-cert-placeholder',
'/path/to/root_ca_cert.pem'
)}
type="text"
{...register(`${serverConfig}.root_ca_cert`)}
/>
</Field>
<Field label={t('ldap-drawer.extra-security-section.client-cert-label', 'Client certificate path')}>
<Input
id="client-cert"
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="/path/to/client_cert.pem"
type="text"
{...register(`${serverConfig}.client_cert`)}
/>
</Field>
<Field label={t('ldap-drawer.extra-security-section.client-key-label', 'Client key path')}>
<SecretInput
id="client-key"
placeholder={t(
'ldap-drawer.extra-security-section.client-key-placeholder',
'/path/to/client_key.pem'
)}
isConfigured={mapCertConfigured.clientKeyCertPath}
onReset={() => {
setValue(`${serverConfig}.client_key`, '');
setMapCertConfigured({ ...mapCertConfigured, clientKeyCertPath: false });
}}
value={watch(`${serverConfig}.client_key`)}
onChange={({ currentTarget: { value } }) => setValue(`${serverConfig}.client_key`, value)}
/>
</Field>
</>
)}
</>
)}
</CollapsableSection>
</Drawer>
);
};
function getStyles(theme: GrafanaTheme2) {
return {
sectionLabel: css({
fontSize: theme.typography.size.lg,
}),
button: css({
marginBottom: theme.spacing(4),
}),
multiSelect: css({
'div:last-of-type > svg': {
display: 'none',
},
}),
};
}