diff --git a/cypress/integration/user_fed_ldap_test.spec.ts b/cypress/integration/user_fed_ldap_test.spec.ts index 8d66220110..b520987728 100644 --- a/cypress/integration/user_fed_ldap_test.spec.ts +++ b/cypress/integration/user_fed_ldap_test.spec.ts @@ -32,8 +32,6 @@ const secondLdapName = `${firstLdapName}-2`; const secondLdapVendor = "Other"; const secondBindType = "none"; -const secondBindDn = "user-2"; -const secondBindCreds = "password2"; const secondUsersDn = "user-dn-2"; const secondUserLdapAtt = "cn"; @@ -165,8 +163,6 @@ describe("User Fed LDAP tests", () => { providersPage.fillLdapRequiredConnectionData( connectionUrl, secondBindType, - secondBindDn, - secondBindCreds ); providersPage.fillLdapRequiredSearchingData( secondUsersDn, diff --git a/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts b/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts index 659cfb7296..37d02925ce 100644 --- a/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts +++ b/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts @@ -129,8 +129,8 @@ export default class ProviderPage { fillLdapRequiredConnectionData( connectionUrl: string, bindType: string, - bindDn: string, - bindCreds: string + bindDn?: string, + bindCreds?: string ) { if (connectionUrl) { cy.get(`[${this.ldapConnectionUrlInput}]`).type(connectionUrl); diff --git a/src/user-federation/UserFederationLdapSettings.tsx b/src/user-federation/UserFederationLdapSettings.tsx index 56dd2540cf..0c1211e34f 100644 --- a/src/user-federation/UserFederationLdapSettings.tsx +++ b/src/user-federation/UserFederationLdapSettings.tsx @@ -26,7 +26,7 @@ import ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresenta import { Controller, useForm } from "react-hook-form"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; -import { useAdminClient } from "../context/auth/AdminClient"; +import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient"; import { useAlerts } from "../components/alert/Alerts"; import { useTranslation } from "react-i18next"; import { ViewHeader } from "../components/view-header/ViewHeader"; @@ -35,6 +35,14 @@ import { ScrollForm } from "../components/scroll-form/ScrollForm"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { LdapMapperList } from "./ldap/mappers/LdapMapperList"; +import { useErrorHandler } from "react-error-boundary"; + +type ldapComponentRepresentation = ComponentRepresentation & { + config?: { + periodicChangedUsersSync?: boolean; + periodicFullSync?: boolean; + }; +}; type LdapSettingsHeaderProps = { onChange: (value: string) => void; @@ -167,33 +175,45 @@ const LdapSettingsHeader = ({ export const UserFederationLdapSettings = () => { const { t } = useTranslation("user-federation"); - const form = useForm(); + const form = useForm({ mode: "onChange" }); const history = useHistory(); const adminClient = useAdminClient(); const { realm } = useRealm(); + const errorHandler = useErrorHandler(); const { id } = useParams<{ id: string }>(); const { addAlert } = useAlerts(); useEffect(() => { - (async () => { - if (id) { - const fetchedComponent = await adminClient.components.findOne({ id }); - if (fetchedComponent) { - setupForm(fetchedComponent); - } - } - })(); + if (id) { + return asyncStateFetch( + () => adminClient.components.findOne({ id }), + (fetchedComponent) => { + if (fetchedComponent) { + setupForm(fetchedComponent); + } + }, + errorHandler + ); + } }, []); const setupForm = (component: ComponentRepresentation) => { - form.reset(); Object.entries(component).map((entry) => { if (entry[0] === "config") { + form.setValue( + "config.periodicChangedUsersSync", + entry[1].changedSyncPeriod[0] !== "-1" + ); + + form.setValue( + "config.periodicFullSync", + entry[1].fullSyncPeriod[0] !== "-1" + ); + convertToFormValues(entry[1], "config", form.setValue); - } else { - form.setValue(entry[0], entry[1]); } + form.setValue(entry[0], entry[1]); }); }; @@ -208,7 +228,19 @@ export const UserFederationLdapSettings = () => { } }; - const save = async (component: ComponentRepresentation) => { + const save = async (component: ldapComponentRepresentation) => { + if (component?.config?.periodicChangedUsersSync !== null) { + if (component?.config?.periodicChangedUsersSync === false) { + component.config.changedSyncPeriod = ["-1"]; + } + delete component?.config?.periodicChangedUsersSync; + } + if (component?.config?.periodicFullSync !== null) { + if (component?.config?.periodicFullSync === false) { + component.config.fullSyncPeriod = ["-1"]; + } + delete component?.config?.periodicFullSync; + } try { if (!id) { await adminClient.components.create(component); @@ -216,11 +248,10 @@ export const UserFederationLdapSettings = () => { } else { await adminClient.components.update({ id }, component); } - setupForm(component as ComponentRepresentation); addAlert(t(id ? "saveSuccess" : "createSuccess"), AlertVariant.success); } catch (error) { addAlert( - `${t(id ? "saveError" : "createError")} '${error}'`, + t(id ? "saveError" : "createError", { error }), AlertVariant.danger ); } diff --git a/src/user-federation/ldap/LdapSettingsConnection.tsx b/src/user-federation/ldap/LdapSettingsConnection.tsx index b1f55bc966..c2a0cc7518 100644 --- a/src/user-federation/ldap/LdapSettingsConnection.tsx +++ b/src/user-federation/ldap/LdapSettingsConnection.tsx @@ -1,4 +1,5 @@ import { + AlertVariant, Button, FormGroup, Select, @@ -6,14 +7,21 @@ import { SelectVariant, Switch, TextInput, + ValidatedOptions, } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; import React, { useState } from "react"; +import _ from "lodash"; + +import TestLdapConnectionRepresentation from "keycloak-admin/lib/defs/testLdapConnection"; import { HelpItem } from "../../components/help-enabler/HelpItem"; -import { Controller, UseFormMethods } from "react-hook-form"; +import { Controller, UseFormMethods, useWatch } from "react-hook-form"; import { FormAccess } from "../../components/form-access/FormAccess"; import { WizardSectionHeader } from "../../components/wizard-section-header/WizardSectionHeader"; import { PasswordInput } from "../../components/password-input/PasswordInput"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useAlerts } from "../../components/alert/Alerts"; export type LdapSettingsConnectionProps = { form: UseFormMethods; @@ -21,13 +29,45 @@ export type LdapSettingsConnectionProps = { showSectionDescription?: boolean; }; +const testLdapProperties: Array = [ + "connectionUrl", + "bindDn", + "bindCredential", + "useTruststoreSpi", + "connectionTimeout", + "startTls", + "authType", +]; + export const LdapSettingsConnection = ({ form, showSectionHeading = false, showSectionDescription = false, }: LdapSettingsConnectionProps) => { const { t } = useTranslation("user-federation"); - const helpText = useTranslation("user-federation-help").t; + const { t: helpText } = useTranslation("user-federation-help"); + const adminClient = useAdminClient(); + const { realm } = useRealm(); + const { addAlert } = useAlerts(); + + const testLdap = async () => { + try { + const settings: TestLdapConnectionRepresentation = {}; + + testLdapProperties.forEach((key) => { + const value = _.get(form.getValues(), `config.${key}`); + settings[key] = _.isArray(value) ? value[0] : ""; + }); + await adminClient.realms.testLDAPConnection( + { realm }, + { ...settings, action: "testConnection" } + ); + addAlert(t("testSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("testError"), AlertVariant.danger); + console.error(error.response?.data?.errorMessage); + } + }; const [ isTruststoreSpiDropdownOpen, @@ -36,6 +76,11 @@ export const LdapSettingsConnection = ({ const [isBindTypeDropdownOpen, setIsBindTypeDropdownOpen] = useState(false); + const ldapBindType = useWatch({ + control: form.control, + name: "config.authType", + }); + return ( <> {showSectionHeading && ( @@ -213,7 +258,7 @@ export const LdapSettingsConnection = ({ > ( )} > - - } - fieldId="kc-console-bind-dn" - isRequired - > - - {form.errors.config && - form.errors.config.bindDn && - form.errors.config.bindDn[0] && ( -
- {form.errors.config.bindDn[0].message} -
- )} -
- - } - fieldId="kc-console-bind-credentials" - isRequired - > - - {form.errors.config && - form.errors.config.bindCredential && - form.errors.config.bindCredential[0] && ( -
- {form.errors.config.bindCredential[0].message} -
- )} -
- - {" "} - {/* TODO: whatever this button is supposed to do */} - - + + {_.isEqual(ldapBindType, ["simple"]) && ( + <> + + } + fieldId="kc-console-bind-dn" + helperTextInvalid={t("validateBindDn")} + validated={ + form.errors.config?.bindDn + ? ValidatedOptions.error + : ValidatedOptions.default + } + isRequired + > + + + + } + fieldId="kc-console-bind-credentials" + helperTextInvalid={t("validateBindCredentials")} + validated={ + form.errors.config?.bindCredential + ? ValidatedOptions.error + : ValidatedOptions.default + } + isRequired + > + + + + + + + )} ); diff --git a/src/user-federation/ldap/LdapSettingsSynchronization.tsx b/src/user-federation/ldap/LdapSettingsSynchronization.tsx index b6dbccfb79..fea3fcd837 100644 --- a/src/user-federation/ldap/LdapSettingsSynchronization.tsx +++ b/src/user-federation/ldap/LdapSettingsSynchronization.tsx @@ -20,6 +20,9 @@ export const LdapSettingsSynchronization = ({ const { t } = useTranslation("user-federation"); const helpText = useTranslation("user-federation-help").t; + const watchPeriodicSync = form.watch("config.periodicFullSync", false); + const watchChangedSync = form.watch("config.periodicChangedUsersSync", false); + return ( <> {showSectionHeading && ( @@ -78,50 +81,110 @@ export const LdapSettingsSynchronization = ({ ref={form.register} /> - - {/* Enter -1 to switch off, otherwise enter value */} } - fieldId="kc-full-sync-period" - > - - - - {/* Enter -1 to switch off, otherwise enter value */} - - } - fieldId="kc-changed-users-sync-period" + fieldId="kc-periodic-full-sync" hasNoPaddingTop > - + ( + onChange(value)} + isChecked={value === true} + label={t("common:on")} + labelOff={t("common:off")} + ref={form.register} + /> + )} + > + {watchPeriodicSync && ( + + } + fieldId="kc-full-sync-period" + > + + + )} + + } + fieldId="kc-periodic-changed-users-sync" + hasNoPaddingTop + > + ( + onChange(value)} + isChecked={value === true} + label={t("common:on")} + labelOff={t("common:off")} + ref={form.register} + /> + )} + > + + {watchChangedSync && ( + + } + fieldId="kc-changed-users-sync-period" + hasNoPaddingTop + > + + + )} ); diff --git a/src/user-federation/messages.json b/src/user-federation/messages.json index 2440958b80..1c44193987 100644 --- a/src/user-federation/messages.json +++ b/src/user-federation/messages.json @@ -81,9 +81,11 @@ "subtree": "Subtree", "saveSuccess": "User federation provider successfully saved", - "saveError": "User federation provider could not be saved: {error}", + "saveError": "User federation provider could not be saved: {{error}}", "createSuccess": "User federation provider successfully created", - "createError": "User federation provider could not be created: {error}", + "createError": "User federation provider could not be created: {{error}}", + "testSuccess": "Successfully connected to LDAP", + "testError": "Error when trying to connect to LDAP. See server.log for details.", "learnMore": "Learn more", "addNewProvider": "Add new provider",