mirror of https://github.com/grafana/grafana.git
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:
parent
185e2234b5
commit
585b53bc7d
|
@ -1,6 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
|
||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
@ -11,8 +10,7 @@ import { contextSrv } from 'app/core/core';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { AccessControlAction } from 'app/types/accessControl';
|
import { AccessControlAction } from 'app/types/accessControl';
|
||||||
import { AppNotificationSeverity } from 'app/types/appNotifications';
|
import { AppNotificationSeverity } from 'app/types/appNotifications';
|
||||||
import { LdapConnectionInfo, LdapUser, SyncInfo, LdapError } from 'app/types/ldap';
|
import { useDispatch, useSelector } from 'app/types/store';
|
||||||
import { StoreState } from 'app/types/store';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
loadLdapState,
|
loadLdapState,
|
||||||
|
@ -26,13 +24,7 @@ import { LdapConnectionStatus } from './LdapConnectionStatus';
|
||||||
import { LdapSyncInfo } from './LdapSyncInfo';
|
import { LdapSyncInfo } from './LdapSyncInfo';
|
||||||
import { LdapUserInfo } from './LdapUserInfo';
|
import { LdapUserInfo } from './LdapUserInfo';
|
||||||
|
|
||||||
interface OwnProps extends GrafanaRouteComponentProps<{}, { username?: string }> {
|
interface Props extends GrafanaRouteComponentProps<{}, { username?: string }> {}
|
||||||
ldapConnectionInfo: LdapConnectionInfo;
|
|
||||||
ldapUser?: LdapUser;
|
|
||||||
ldapSyncInfo?: SyncInfo;
|
|
||||||
ldapError?: LdapError;
|
|
||||||
userError?: LdapError;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormModel {
|
interface FormModel {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -45,36 +37,31 @@ const pageNav: NavModelItem = {
|
||||||
id: 'LDAP',
|
id: 'LDAP',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LdapPage = ({
|
export const LdapPage = ({ queryParams }: Props) => {
|
||||||
clearUserMappingInfo,
|
const dispatch = useDispatch();
|
||||||
queryParams,
|
|
||||||
loadLdapState,
|
const ldapConnectionInfo = useSelector((state) => state.ldap.connectionInfo);
|
||||||
loadLdapSyncStatus,
|
const ldapUser = useSelector((state) => state.ldap.user);
|
||||||
loadUserMapping,
|
const ldapSyncInfo = useSelector((state) => state.ldap.syncInfo);
|
||||||
clearUserError,
|
const userError = useSelector((state) => state.ldap.userError);
|
||||||
ldapUser,
|
const ldapError = useSelector((state) => state.ldap.ldapError);
|
||||||
userError,
|
|
||||||
ldapError,
|
|
||||||
ldapSyncInfo,
|
|
||||||
ldapConnectionInfo,
|
|
||||||
}: Props) => {
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const { register, handleSubmit } = useForm<FormModel>();
|
const { register, handleSubmit } = useForm<FormModel>();
|
||||||
|
|
||||||
const fetchUserMapping = useCallback(
|
const fetchUserMapping = useCallback(
|
||||||
async (username: string) => {
|
async (username: string) => {
|
||||||
return loadUserMapping(username);
|
return dispatch(loadUserMapping(username));
|
||||||
},
|
},
|
||||||
[loadUserMapping]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLDAPStatus = async () => {
|
const fetchLDAPStatus = async () => {
|
||||||
return Promise.all([loadLdapState(), loadLdapSyncStatus()]);
|
return Promise.all([dispatch(loadLdapState()), dispatch(loadLdapSyncStatus())]);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await clearUserMappingInfo();
|
await dispatch(clearUserMappingInfo());
|
||||||
await fetchLDAPStatus();
|
await fetchLDAPStatus();
|
||||||
|
|
||||||
if (queryParams.username) {
|
if (queryParams.username) {
|
||||||
|
@ -85,7 +72,7 @@ export const LdapPage = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, [clearUserMappingInfo, fetchUserMapping, loadLdapState, loadLdapSyncStatus, queryParams]);
|
}, [dispatch, fetchUserMapping, queryParams]);
|
||||||
|
|
||||||
const search = ({ username }: FormModel) => {
|
const search = ({ username }: FormModel) => {
|
||||||
if (username) {
|
if (username) {
|
||||||
|
@ -94,7 +81,7 @@ export const LdapPage = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClearUserError = () => {
|
const onClearUserError = () => {
|
||||||
clearUserError();
|
dispatch(clearUserError());
|
||||||
};
|
};
|
||||||
|
|
||||||
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
|
const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead);
|
||||||
|
@ -147,23 +134,4 @@ export const LdapPage = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
export default LdapPage;
|
||||||
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);
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { LdapPayload, MapKeyCertConfigured } from 'app/types/ldap';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
|
|
||||||
import { LdapDrawerComponent } from './LdapDrawer';
|
import { LdapDrawerComponent } from './LdapDrawer';
|
||||||
|
import { LdapTestDrawer } from './LdapTestDrawer';
|
||||||
|
|
||||||
const appEvents = getAppEvents();
|
const appEvents = getAppEvents();
|
||||||
|
|
||||||
|
@ -99,6 +100,8 @@ const emptySettings: LdapPayload = {
|
||||||
export const LdapSettingsPage = () => {
|
export const LdapSettingsPage = () => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [isTestDrawerOpen, setIsTestDrawerOpen] = useState(false);
|
||||||
|
const [usernameParam, setUsernameParam] = useState<string | null>(null);
|
||||||
|
|
||||||
const [isBindPasswordConfigured, setBindPasswordConfigured] = useState(false);
|
const [isBindPasswordConfigured, setBindPasswordConfigured] = useState(false);
|
||||||
const [mapKeyCertConfigured, setMapKeyCertConfigured] = useState<MapKeyCertConfigured>({
|
const [mapKeyCertConfigured, setMapKeyCertConfigured] = useState<MapKeyCertConfigured>({
|
||||||
|
@ -122,6 +125,10 @@ export const LdapSettingsPage = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const username = urlParams.get('username');
|
||||||
|
setUsernameParam(username);
|
||||||
|
|
||||||
const payload = await getSettings();
|
const payload = await getSettings();
|
||||||
let serverConfig = emptySettings.settings.config.servers[0];
|
let serverConfig = emptySettings.settings.config.servers[0];
|
||||||
if (payload.settings.config.servers?.length > 0) {
|
if (payload.settings.config.servers?.length > 0) {
|
||||||
|
@ -135,6 +142,10 @@ export const LdapSettingsPage = () => {
|
||||||
|
|
||||||
reset(payload);
|
reset(payload);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
setIsTestDrawerOpen(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
init();
|
init();
|
||||||
}, [reset]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [reset]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
@ -416,6 +427,9 @@ export const LdapSettingsPage = () => {
|
||||||
<Button variant="secondary" onClick={handleSubmit(saveForm)}>
|
<Button variant="secondary" onClick={handleSubmit(saveForm)}>
|
||||||
<Trans i18nKey="ldap-settings-page.buttons-section.save-button">Save</Trans>
|
<Trans i18nKey="ldap-settings-page.buttons-section.save-button">Save</Trans>
|
||||||
</Button>
|
</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">
|
<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>
|
</LinkButton>
|
||||||
|
@ -455,6 +469,9 @@ export const LdapSettingsPage = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
{isTestDrawerOpen && (
|
||||||
|
<LdapTestDrawer onClose={() => setIsTestDrawerOpen(false)} username={usernameParam || undefined} />
|
||||||
|
)}
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -156,8 +156,11 @@
|
||||||
"title": "Get Grafana Enterprise"
|
"title": "Get Grafana Enterprise"
|
||||||
},
|
},
|
||||||
"ldap": {
|
"ldap": {
|
||||||
|
"debug-subtitle": "Verify your LDAP and user mapping configuration.",
|
||||||
|
"debug-title": "LDAP Diagnostics",
|
||||||
"test-mapping-heading": "Test user mapping",
|
"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": {
|
"ldap-connection-status": {
|
||||||
"columns": {
|
"columns": {
|
||||||
|
@ -9239,7 +9242,8 @@
|
||||||
"disable-button": "Disable",
|
"disable-button": "Disable",
|
||||||
"discard-button": "Discard",
|
"discard-button": "Discard",
|
||||||
"save-and-enable-button": "Save and enable",
|
"save-and-enable-button": "Save and enable",
|
||||||
"save-button": "Save"
|
"save-button": "Save",
|
||||||
|
"test-button": "Test"
|
||||||
},
|
},
|
||||||
"documentation": "documentation",
|
"documentation": "documentation",
|
||||||
"host": {
|
"host": {
|
||||||
|
|
Loading…
Reference in New Issue