SSO LDAP: ironing details (#93584)

* Rework description tooltip

* Null protect empty

* Add missing ref properties for form controllers

* Rework LDAP provider label

* Add missing `*` for required input

* Hide multiselect visual queues

* Add modal

* Adjust translated text hierarchies

* Add redirect confirmation modal
This commit is contained in:
linoman 2024-09-25 15:59:57 +02:00 committed by GitHub
parent 267f417924
commit b6906cc866
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 116 additions and 56 deletions

View File

@ -60,7 +60,7 @@ export const LdapDrawerComponent = ({
useEffect(() => {
const { client_cert, client_key, root_ca_cert } = getValues(serverConfig);
setEncryptionProvider(
!client_cert.length && !client_key.length && !root_ca_cert?.length
!client_cert?.length && !client_key?.length && !root_ca_cert?.length
? EncryptionProvider.Base64
: EncryptionProvider.FilePath
);
@ -83,19 +83,21 @@ export const LdapDrawerComponent = ({
);
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>
<>
<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/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`, [
...getValues(`${serverConfig}.group_mappings`),
...groupMappings,
{
group_dn: '',
org_id: 1,
@ -338,10 +340,12 @@ export const LdapDrawerComponent = ({
<Controller
name={`${serverConfig}.root_ca_cert_value`}
control={control}
render={({ field: { onChange, value, ...field } }) => (
render={({ field: { onChange, ref, value, ...field } }) => (
<MultiSelect
{...field}
allowCustomValue
className={styles.multiSelect}
noOptionsMessage={''}
onChange={(v) => onChange(v.map(({ value }) => String(value)))}
value={value?.map((v) => ({ label: renderMultiSelectLabel(v), value: v }))}
/>
@ -447,5 +451,10 @@ function getStyles(theme: GrafanaTheme2) {
button: css({
marginBottom: theme.spacing(4),
}),
multiSelect: css({
svg: {
display: 'none',
},
}),
};
}

View File

@ -4,7 +4,7 @@ import { Controller, 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 { getBackendSrv, getAppEvents, reportInteraction } from '@grafana/runtime';
import {
useStyles2,
Alert,
@ -15,12 +15,14 @@ import {
Input,
LinkButton,
Menu,
Modal,
Stack,
Text,
TextLink,
Dropdown,
MultiSelect,
} from '@grafana/ui';
import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
import { Page } from 'app/core/components/Page/Page';
import config from 'app/core/config';
import { t, Trans } from 'app/core/internationalization';
@ -94,6 +96,7 @@ const emptySettings: LdapPayload = {
export const LdapSettingsPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [mapKeyCertConfigured, setMapKeyCertConfigured] = useState<MapKeyCertConfigured>({
// values
@ -107,14 +110,25 @@ export const LdapSettingsPage = () => {
});
const methods = useForm<LdapPayload>({ defaultValues: emptySettings });
const { control, getValues, handleSubmit, register, reset, watch } = methods;
const {
control,
formState: { isDirty },
getValues,
handleSubmit,
register,
reset,
watch,
} = methods;
const styles = useStyles2(getStyles);
useEffect(() => {
async function init() {
const payload = await getSettings();
const serverConfig = payload.settings.config.servers[0];
let serverConfig = emptySettings.settings.config.servers[0];
if (payload.settings.config.servers?.length > 0) {
serverConfig = payload.settings.config.servers[0];
}
setMapKeyCertConfigured({
rootCaCertValue: serverConfig.root_ca_cert_value?.length > 0,
clientCertValue: serverConfig.client_cert_value !== '',
@ -203,12 +217,14 @@ export const LdapSettingsPage = () => {
/**
* Button's Actions
*/
const submitAndEnableLdapSettings = (payload: LdapPayload) => {
const submitAndEnableLdapSettings = async (payload: LdapPayload) => {
payload.settings.enabled = true;
putPayload(payload);
await putPayload(payload);
reportInteraction('authentication_ldap_enabled');
};
const saveForm = () => {
putPayload(getValues());
const saveForm = async () => {
await putPayload(getValues());
reportInteraction('authentication_ldap_saved');
};
const deleteLDAPConfig = async () => {
try {
@ -220,6 +236,7 @@ export const LdapSettingsPage = () => {
payload: [t('ldap-settings-page.alert.discard-success', 'LDAP settings discarded')],
});
reset(payload);
reportInteraction('authentication_ldap_deleted');
} catch (error) {
appEvents.publish({
type: AppEvents.alertError.name,
@ -230,6 +247,10 @@ export const LdapSettingsPage = () => {
}
};
const onDiscard = () => {
reportInteraction('authentication_ldap_abandoned');
};
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
@ -259,6 +280,7 @@ export const LdapSettingsPage = () => {
{config.disableLoginForm && disabledFormAlert}
<FormProvider {...methods}>
<form onSubmit={handleSubmit(submitAndEnableLdapSettings, onErrors)}>
<FormPrompt confirmRedirect={isDirty} onDiscard={onDiscard} />
{isLoading && <Loader />}
{!isLoading && (
<section className={styles.form}>
@ -266,7 +288,7 @@ export const LdapSettingsPage = () => {
<Trans i18nKey="ldap-settings-page.title">Basic Settings</Trans>
</h3>
<Field
label={t('ldap-settings-page.host.label', 'Server host')}
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.'
@ -301,7 +323,7 @@ export const LdapSettingsPage = () => {
/>
</Field>
<Field
label={t('ldap-settings-page.search_filter.label', 'Search filter*')}
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.'
@ -324,10 +346,12 @@ export const LdapSettingsPage = () => {
<Controller
name={`${serverConfig}.search_base_dns`}
control={control}
render={({ field: { onChange, ...field } }) => (
render={({ field: { onChange, ref, ...field } }) => (
<MultiSelect
{...field}
allowCustomValue
className={styles.multiSelect}
noOptionsMessage={''}
onChange={(v) => onChange(v.map(({ value }) => String(value)))}
/>
)}
@ -346,20 +370,20 @@ export const LdapSettingsPage = () => {
</Text>
</Stack>
<Button variant="secondary" onClick={() => setIsDrawerOpen(true)}>
<Trans i18nKey="ldap-settings-page.advanced-settings-section.edit.button">Edit</Trans>
<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">
<Trans i18nKey="ldap-settings-page.buttons-section.save-and-enable.button">Save and enable</Trans>
<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>
<Trans i18nKey="ldap-settings-page.buttons-section.save-button">Save</Trans>
</Button>
<LinkButton href="/admin/authentication" variant="secondary">
<Trans i18nKey="ldap-settings-page.buttons-section.discard.button">Discard</Trans>
<Trans i18nKey="ldap-settings-page.buttons-section.discard-button">Discard</Trans>
</LinkButton>
<Dropdown
overlay={
@ -392,6 +416,26 @@ export const LdapSettingsPage = () => {
</form>
</FormProvider>
</Page.Contents>
<Modal
title={t('ldap-settings-page.discard-modal.title', 'Leave LDAP configuration?')}
isOpen={isModalOpen}
onDismiss={() => setIsModalOpen(false)}
>
<Stack direction={'column'} gap={2}>
<Trans i18nKey="ldap-settings-page.discard-modal.description">
Are you sure you want to abandon the changes you&lsquo;ve made to the LDAP configuration? All changes will
be lost.
</Trans>
<Stack direction="row" gap={2} justifyContent="flex-end">
<Button variant="secondary">
<Trans i18nKey="ldap-settings-page.discard-modal.cancel-button">Back to editing</Trans>
</Button>
<Button variant="destructive">
<Trans i18nKey="ldap-settings-page.discard-modal.discard-button">Abandon LDAP</Trans>
</Button>
</Stack>
</Stack>
</Modal>
</Page>
);
};
@ -401,6 +445,11 @@ function getStyles(theme: GrafanaTheme2) {
form: css({
width: theme.spacing(68),
}),
multiSelect: css({
svg: {
display: 'none',
},
}),
};
}

View File

@ -57,7 +57,13 @@ export const AuthConfigPageUnconnected = ({
providers = providers.map((p) => {
if (p.provider === 'ldap') {
p.settings.type = p.provider;
return {
...p,
settings: {
...p.settings,
type: 'LDAP',
},
};
}
return p;
});

View File

@ -1091,7 +1091,7 @@
"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)",
"use-ssl-label": "Use SSL",
"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>}"
"use-ssl-tooltip": "For a complete list of supported ciphers and TLS versions, refer to:"
},
"group-mapping": {
"grafana-admin": {
@ -1141,9 +1141,7 @@
},
"ldap-settings-page": {
"advanced-settings-section": {
"edit": {
"button": "Edit"
},
"edit-button": "Edit",
"subtitle": "Mappings, extra security measures, and more.",
"title": "Advanced Settings"
},
@ -1164,20 +1162,20 @@
"label": "Bind password"
},
"buttons-section": {
"discard": {
"button": "Discard"
},
"save": {
"button": "Save"
},
"save-and-enable": {
"button": "Save and enable"
}
"discard-button": "Discard",
"save-and-enable-button": "Save and enable",
"save-button": "Save"
},
"discard-modal": {
"cancel-button": "Back to editing",
"description": "Are you sure you want to abandon the changes you&lsquo;ve made to the LDAP configuration? All changes will be lost.",
"discard-button": "Abandon LDAP",
"title": "Leave LDAP configuration?"
},
"documentation": "documentation",
"host": {
"description": "Hostname or IP address of the LDAP server you wish to connect to.",
"label": "Server host",
"label": "Server host *",
"placeholder": "example: 127.0.0.1"
},
"login-form-alert": {
@ -1186,7 +1184,7 @@
},
"search_filter": {
"description": "LDAP search filter used to locate specific entries within the directory.",
"label": "Search filter*",
"label": "Search filter *",
"placeholder": "example: cn=%s"
},
"search-base-dns": {

View File

@ -1091,7 +1091,7 @@
"tls-ciphers-placeholder": "ę.ģ. [\"ŦĿŜ_ÅĒŜ_256_ĞCM_ŜĦÅ384\"]",
"use-ssl-description": "Ŝęŧ ŧő ŧřūę įƒ ĿĐÅP şęřvęř şĥőūľđ ūşę ŦĿŜ čőʼnʼnęčŧįőʼn (ęįŧĥęř ŵįŧĥ ŜŦÅŖŦŦĿŜ őř ĿĐÅPŜ)",
"use-ssl-label": "Ůşę ŜŜĿ",
"use-ssl-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ĸ>}"
"use-ssl-tooltip": "Főř ä čőmpľęŧę ľįşŧ őƒ şūppőřŧęđ čįpĥęřş äʼnđ ŦĿŜ vęřşįőʼnş, řęƒęř ŧő:"
},
"group-mapping": {
"grafana-admin": {
@ -1141,9 +1141,7 @@
},
"ldap-settings-page": {
"advanced-settings-section": {
"edit": {
"button": "Ēđįŧ"
},
"edit-button": "Ēđįŧ",
"subtitle": "Mäppįʼnģş, ęχŧřä şęčūřįŧy męäşūřęş, äʼnđ mőřę.",
"title": "Åđväʼnčęđ Ŝęŧŧįʼnģş"
},
@ -1164,20 +1162,20 @@
"label": "ßįʼnđ päşşŵőřđ"
},
"buttons-section": {
"discard": {
"button": "Đįşčäřđ"
},
"save": {
"button": "Ŝävę"
},
"save-and-enable": {
"button": "Ŝävę äʼnđ ęʼnäþľę"
}
"discard-button": "Đįşčäřđ",
"save-and-enable-button": "Ŝävę äʼnđ ęʼnäþľę",
"save-button": "Ŝävę"
},
"discard-modal": {
"cancel-button": "ßäčĸ ŧő ęđįŧįʼnģ",
"description": "Åřę yőū şūřę yőū ŵäʼnŧ ŧő äþäʼnđőʼn ŧĥę čĥäʼnģęş yőū&ľşqūő;vę mäđę ŧő ŧĥę ĿĐÅP čőʼnƒįģūřäŧįőʼn? Åľľ čĥäʼnģęş ŵįľľ þę ľőşŧ.",
"discard-button": "Åþäʼnđőʼn ĿĐÅP",
"title": "Ŀęävę ĿĐÅP čőʼnƒįģūřäŧįőʼn?"
},
"documentation": "đőčūmęʼnŧäŧįőʼn",
"host": {
"description": "Ħőşŧʼnämę őř ĨP äđđřęşş őƒ ŧĥę ĿĐÅP şęřvęř yőū ŵįşĥ ŧő čőʼnʼnęčŧ ŧő.",
"label": "Ŝęřvęř ĥőşŧ",
"label": "Ŝęřvęř ĥőşŧ *",
"placeholder": "ęχämpľę: 127.0.0.1"
},
"login-form-alert": {
@ -1186,7 +1184,7 @@
},
"search_filter": {
"description": "ĿĐÅP şęäřčĥ ƒįľŧęř ūşęđ ŧő ľőčäŧę şpęčįƒįč ęʼnŧřįęş ŵįŧĥįʼn ŧĥę đįřęčŧőřy.",
"label": "Ŝęäřčĥ ƒįľŧęř*",
"label": "Ŝęäřčĥ ƒįľŧęř *",
"placeholder": "ęχämpľę: čʼn=%ş"
},
"search-base-dns": {