LDAP: Restore test user mapping functionality (#110841)

* Migrate LdapPage from connect() to React-Redux hooks

* Convert LDAP debug page into a drawer and hook it into settings

* prettier

* Use the Text component and make the input and button look like they do on the main settings page.

* Bring back isLoading and put in a LoadingPlaceholder

* i18n-extract

* rejigger

* linter fix
This commit is contained in:
John Troy 2025-09-15 09:22:26 -04:00 committed by GitHub
parent 185e2234b5
commit 585b53bc7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 178 additions and 51 deletions

View File

@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { connect, ConnectedProps } from 'react-redux';
import { NavModelItem } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
@ -11,8 +10,7 @@ import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { AccessControlAction } from 'app/types/accessControl';
import { AppNotificationSeverity } from 'app/types/appNotifications';
import { LdapConnectionInfo, LdapUser, SyncInfo, LdapError } from 'app/types/ldap';
import { StoreState } from 'app/types/store';
import { useDispatch, useSelector } from 'app/types/store';
import {
loadLdapState,
@ -26,13 +24,7 @@ import { LdapConnectionStatus } from './LdapConnectionStatus';
import { LdapSyncInfo } from './LdapSyncInfo';
import { LdapUserInfo } from './LdapUserInfo';
interface OwnProps extends GrafanaRouteComponentProps<{}, { username?: string }> {
ldapConnectionInfo: LdapConnectionInfo;
ldapUser?: LdapUser;
ldapSyncInfo?: SyncInfo;
ldapError?: LdapError;
userError?: LdapError;
}
interface Props extends GrafanaRouteComponentProps<{}, { username?: string }> {}
interface FormModel {
username: string;
@ -45,36 +37,31 @@ const pageNav: NavModelItem = {
id: 'LDAP',
};
export const LdapPage = ({
clearUserMappingInfo,
queryParams,
loadLdapState,
loadLdapSyncStatus,
loadUserMapping,
clearUserError,
ldapUser,
userError,
ldapError,
ldapSyncInfo,
ldapConnectionInfo,
}: Props) => {
export const LdapPage = ({ queryParams }: Props) => {
const dispatch = useDispatch();
const ldapConnectionInfo = useSelector((state) => state.ldap.connectionInfo);
const ldapUser = useSelector((state) => state.ldap.user);
const ldapSyncInfo = useSelector((state) => state.ldap.syncInfo);
const userError = useSelector((state) => state.ldap.userError);
const ldapError = useSelector((state) => state.ldap.ldapError);
const [isLoading, setIsLoading] = useState(true);
const { register, handleSubmit } = useForm<FormModel>();
const fetchUserMapping = useCallback(
async (username: string) => {
return loadUserMapping(username);
return dispatch(loadUserMapping(username));
},
[loadUserMapping]
[dispatch]
);
useEffect(() => {
const fetchLDAPStatus = async () => {
return Promise.all([loadLdapState(), loadLdapSyncStatus()]);
return Promise.all([dispatch(loadLdapState()), dispatch(loadLdapSyncStatus())]);
};
async function init() {
await clearUserMappingInfo();
await dispatch(clearUserMappingInfo());
await fetchLDAPStatus();
if (queryParams.username) {
@ -85,7 +72,7 @@ export const LdapPage = ({
}
init();
}, [clearUserMappingInfo, fetchUserMapping, loadLdapState, loadLdapSyncStatus, queryParams]);
}, [dispatch, fetchUserMapping, queryParams]);
const search = ({ username }: FormModel) => {
if (username) {
@ -94,7 +81,7 @@ export const LdapPage = ({
};
const onClearUserError = () => {
clearUserError();
dispatch(clearUserError());
};
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
@ -147,23 +134,4 @@ export const LdapPage = ({
);
};
const mapStateToProps = (state: StoreState) => ({
ldapConnectionInfo: state.ldap.connectionInfo,
ldapUser: state.ldap.user,
ldapSyncInfo: state.ldap.syncInfo,
userError: state.ldap.userError,
ldapError: state.ldap.ldapError,
});
const mapDispatchToProps = {
loadLdapState,
loadLdapSyncStatus,
loadUserMapping,
clearUserError,
clearUserMappingInfo,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type Props = OwnProps & ConnectedProps<typeof connector>;
export default connector(LdapPage);
export default LdapPage;

View File

@ -31,6 +31,7 @@ import { LdapPayload, MapKeyCertConfigured } from 'app/types/ldap';
import { StoreState } from 'app/types/store';
import { LdapDrawerComponent } from './LdapDrawer';
import { LdapTestDrawer } from './LdapTestDrawer';
const appEvents = getAppEvents();
@ -99,6 +100,8 @@ const emptySettings: LdapPayload = {
export const LdapSettingsPage = () => {
const [isLoading, setIsLoading] = useState(true);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isTestDrawerOpen, setIsTestDrawerOpen] = useState(false);
const [usernameParam, setUsernameParam] = useState<string | null>(null);
const [isBindPasswordConfigured, setBindPasswordConfigured] = useState(false);
const [mapKeyCertConfigured, setMapKeyCertConfigured] = useState<MapKeyCertConfigured>({
@ -122,6 +125,10 @@ export const LdapSettingsPage = () => {
useEffect(() => {
async function init() {
const urlParams = new URLSearchParams(window.location.search);
const username = urlParams.get('username');
setUsernameParam(username);
const payload = await getSettings();
let serverConfig = emptySettings.settings.config.servers[0];
if (payload.settings.config.servers?.length > 0) {
@ -135,6 +142,10 @@ export const LdapSettingsPage = () => {
reset(payload);
setIsLoading(false);
if (username) {
setIsTestDrawerOpen(true);
}
}
init();
}, [reset]); // eslint-disable-line react-hooks/exhaustive-deps
@ -416,6 +427,9 @@ export const LdapSettingsPage = () => {
<Button variant="secondary" onClick={handleSubmit(saveForm)}>
<Trans i18nKey="ldap-settings-page.buttons-section.save-button">Save</Trans>
</Button>
<Button variant="secondary" onClick={() => setIsTestDrawerOpen(true)}>
<Trans i18nKey="ldap-settings-page.buttons-section.test-button">Test</Trans>
</Button>
<LinkButton href="/admin/authentication" variant="secondary">
<Trans i18nKey="ldap-settings-page.buttons-section.discard-button">Discard</Trans>
</LinkButton>
@ -455,6 +469,9 @@ export const LdapSettingsPage = () => {
/>
)}
</form>
{isTestDrawerOpen && (
<LdapTestDrawer onClose={() => setIsTestDrawerOpen(false)} username={usernameParam || undefined} />
)}
</FormProvider>
</Page.Contents>
</Page>

View File

@ -0,0 +1,138 @@
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
import { featureEnabled } from '@grafana/runtime';
import { Alert, Button, Drawer, Field, Input, LoadingPlaceholder, Stack, Text } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types/accessControl';
import { AppNotificationSeverity } from 'app/types/appNotifications';
import { useDispatch, useSelector } from 'app/types/store';
import {
loadLdapState,
loadLdapSyncStatus,
loadUserMapping,
clearUserError,
clearUserMappingInfo,
} from '../state/actions';
import { LdapConnectionStatus } from './LdapConnectionStatus';
import { LdapSyncInfo } from './LdapSyncInfo';
import { LdapUserInfo } from './LdapUserInfo';
interface Props {
onClose: () => void;
username?: string;
}
interface FormModel {
username: string;
}
export const LdapTestDrawer = ({ onClose, username }: Props) => {
const dispatch = useDispatch();
const ldapConnectionInfo = useSelector((state) => state.ldap.connectionInfo);
const ldapUser = useSelector((state) => state.ldap.user);
const ldapSyncInfo = useSelector((state) => state.ldap.syncInfo);
const userError = useSelector((state) => state.ldap.userError);
const ldapError = useSelector((state) => state.ldap.ldapError);
const [isLoading, setIsLoading] = useState(true);
const { register, handleSubmit } = useForm<FormModel>();
const fetchUserMapping = useCallback(
async (username: string) => {
return dispatch(loadUserMapping(username));
},
[dispatch]
);
useEffect(() => {
const fetchLDAPStatus = async () => {
return Promise.all([dispatch(loadLdapState()), dispatch(loadLdapSyncStatus())]);
};
async function init() {
dispatch(clearUserMappingInfo());
await fetchLDAPStatus();
if (username) {
await fetchUserMapping(username);
}
setIsLoading(false);
}
init();
}, [dispatch, fetchUserMapping, username]);
const search = (data: FormModel, event?: React.BaseSyntheticEvent) => {
event?.preventDefault();
event?.stopPropagation();
if (data.username) {
fetchUserMapping(data.username);
}
};
const onClearUserError = () => {
dispatch(clearUserError());
};
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
return (
<Drawer
title={t('admin.ldap.debug-title', 'LDAP Diagnostics')}
subtitle={t('admin.ldap.debug-subtitle', 'Verify your LDAP and user mapping configuration.')}
onClose={onClose}
>
{isLoading ? (
<LoadingPlaceholder text={t('admin.ldap.text-loading-ldap-status', 'Loading LDAP status...')} />
) : (
<Stack direction="column" gap={4}>
{ldapError && ldapError.title && (
<Alert title={ldapError.title} severity={AppNotificationSeverity.Error}>
{ldapError.body}
</Alert>
)}
<LdapConnectionStatus ldapConnectionInfo={ldapConnectionInfo} />
{featureEnabled('ldapsync') && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />}
{canReadLDAPUser && (
<section>
<Stack direction="column" gap={2}>
<Text element="h3">
<Trans i18nKey="admin.ldap.test-mapping-heading">Test user mapping</Trans>
</Text>
<form onSubmit={handleSubmit(search)}>
<Field noMargin label={t('admin.ldap-page.label-username', 'Username')}>
<Stack>
<Input
{...register('username', { required: true })}
width={34}
id="username"
type="text"
defaultValue={username}
/>
<Button variant="secondary" type="submit">
<Trans i18nKey="admin.ldap.test-mapping-run-button">Run</Trans>
</Button>
</Stack>
</Field>
</form>
{userError && userError.title && (
<Alert title={userError.title} severity={AppNotificationSeverity.Error} onRemove={onClearUserError}>
{userError.body}
</Alert>
)}
{ldapUser && <LdapUserInfo ldapUser={ldapUser} />}
</Stack>
</section>
)}
</Stack>
)}
</Drawer>
);
};

View File

@ -156,8 +156,11 @@
"title": "Get Grafana Enterprise"
},
"ldap": {
"debug-subtitle": "Verify your LDAP and user mapping configuration.",
"debug-title": "LDAP Diagnostics",
"test-mapping-heading": "Test user mapping",
"test-mapping-run-button": "Run"
"test-mapping-run-button": "Run",
"text-loading-ldap-status": "Loading LDAP status..."
},
"ldap-connection-status": {
"columns": {
@ -9239,7 +9242,8 @@
"disable-button": "Disable",
"discard-button": "Discard",
"save-and-enable-button": "Save and enable",
"save-button": "Save"
"save-button": "Save",
"test-button": "Test"
},
"documentation": "documentation",
"host": {