diff --git a/app/api/server/v1/ldap.ts b/app/api/server/v1/ldap.ts index c8cc10701e4..3b47a64b28a 100644 --- a/app/api/server/v1/ldap.ts +++ b/app/api/server/v1/ldap.ts @@ -1,10 +1,11 @@ +import { Match, check } from 'meteor/check'; + import { hasRole } from '../../../authorization/server'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { LDAP } from '../../../../server/sdk'; - API.v1.addRoute('ldap.testConnection', { authRequired: true }, { post() { if (!this.userId) { @@ -31,3 +32,29 @@ API.v1.addRoute('ldap.testConnection', { authRequired: true }, { }); }, }); + +API.v1.addRoute('ldap.testSearch', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + username: String, + })); + + if (!this.userId) { + throw new Error('error-invalid-user'); + } + + if (!hasRole(this.userId, 'admin')) { + throw new Error('error-not-authorized'); + } + + if (settings.get('LDAP_Enable') !== true) { + throw new Error('LDAP_disabled'); + } + + Promise.await(LDAP.testSearch(this.bodyParams.username)); + + return API.v1.success({ + message: 'LDAP_User_Found', + }); + }, +}); diff --git a/client/contexts/ServerContext/endpoints/v1/ldap.ts b/client/contexts/ServerContext/endpoints/v1/ldap.ts index 068b797107e..09b19d5637a 100644 --- a/client/contexts/ServerContext/endpoints/v1/ldap.ts +++ b/client/contexts/ServerContext/endpoints/v1/ldap.ts @@ -6,6 +6,11 @@ export type LDAPEndpoints = { message: TranslationKey; }; }; + 'ldap.testSearch': { + POST: (params: { username: string }) => { + message: TranslationKey; + }; + }; 'ldap.syncNow': { POST: () => { message: TranslationKey; diff --git a/client/views/admin/settings/groups/LDAPGroupPage.tsx b/client/views/admin/settings/groups/LDAPGroupPage.tsx index c32d42e8b11..0d2a30bf27d 100644 --- a/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -1,9 +1,11 @@ -import { Button } from '@rocket.chat/fuselage'; -import React, { memo, useMemo } from 'react'; +import { Button, Box, TextInput, Field } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import React, { FormEvent, memo, useMemo } from 'react'; import type { ISetting } from '../../../../../definition/ISetting'; +import GenericModal from '../../../../components/GenericModal'; import { useEditableSettings } from '../../../../contexts/EditableSettingsContext'; -import { useModal } from '../../../../contexts/ModalContext'; +import { useSetModal } from '../../../../contexts/ModalContext'; import { useEndpoint } from '../../../../contexts/ServerContext'; import { useSetting } from '../../../../contexts/SettingsContext'; import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; @@ -15,9 +17,11 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const dispatchToastMessage = useToastMessageDispatch(); const testConnection = useEndpoint('POST', 'ldap.testConnection'); const syncNow = useEndpoint('POST', 'ldap.syncNow'); + const testSearch = useEndpoint('POST', 'ldap.testSearch'); const ldapEnabled = useSetting('LDAP_Enable'); const ldapSyncEnabled = useSetting('LDAP_Background_Sync') && ldapEnabled; - const modal = useModal(); + const setModal = useSetModal(); + const closeModal = useMutableCallback(() => setModal()); const editableSettings = useEditableSettings( useMemo( @@ -45,28 +49,68 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleSyncNowButtonClick = async (): Promise => { try { await testConnection(undefined); - // #ToDo: Switch to modal.setModal - modal.open( - { - title: t('Execute_Synchronization_Now'), - text: t('LDAP_Sync_Now_Description'), - confirmButtonText: t('Sync'), - showCancelButton: true, - closeOnConfirm: true, - closeOnCancel: true, - }, - async (isConfirm: boolean): Promise => { - if (!isConfirm) { - return; - } + const confirmSync = async (): Promise => { + try { + const { message } = await syncNow(undefined); + dispatchToastMessage({ type: 'success', message: t(message) }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; - try { - const { message } = await syncNow(undefined); - dispatchToastMessage({ type: 'success', message: t(message) }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }, + setModal( + + {t('LDAP_Sync_Now_Description')} + , + ); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + const handleSearchTestButtonClick = async (): Promise => { + try { + await testConnection(undefined); + let username = ''; + const handleChangeUsername = (event: FormEvent): void => { + username = event.currentTarget.value; + }; + + const confirmSearch = async (): Promise => { + try { + const { message } = await testSearch({ username }); + dispatchToastMessage({ type: 'success', message: t(message) }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + setModal( + + + + {t('LDAP_Username_To_Search')} + + + + + + + , ); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); @@ -84,6 +128,11 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { disabled={!ldapEnabled || changed} onClick={handleTestConnectionButtonClick} /> + diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index d2cfa6eec09..693711f932c 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2555,6 +2555,7 @@ "LDAP_Timeout_Description": "How many mileseconds wait for a search result before return an error", "LDAP_Unique_Identifier_Field": "Unique Identifier Field", "LDAP_Unique_Identifier_Field_Description": "Which field will be used to link the LDAP user and the Rocket.Chat user. You can inform multiple values separated by comma to try to get the value from LDAP record.
Default value is `objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber`", + "LDAP_User_Found": "LDAP User Found", "LDAP_User_Search_AttributesToQuery": "Attributes to Query", "LDAP_User_Search_AttributesToQuery_Description": "Specify which attributes should be returned on LDAP queries, separating them with commas. Defaults to everything. `*` represents all regular attributes and `+` represents all operational attributes. Make sure to include every attribute that is used by every Rocket.Chat sync option.", "LDAP_User_Search_Field": "Search Field", @@ -2564,6 +2565,7 @@ "LDAP_User_Search_Scope": "Scope", "LDAP_Username_Field": "Username Field", "LDAP_Username_Field_Description": "Which field will be used as *username* for new users. Leave empty to use the username informed on login page.
You can use template tags too, like `#{givenName}.#{sn}`.
Default value is `sAMAccountName`.", + "LDAP_Username_To_Search": "Username to search", "LDAP_Validate_Teams_For_Each_Login": "Validate mapping for each login", "LDAP_Validate_Teams_For_Each_Login_Description": "Determine if users' teams should be updated every time they login to Rocket.Chat. If this is turned off the team will be loaded only on their first login.", "Lead_capture_email_regex": "Lead capture email regex", @@ -4075,6 +4077,7 @@ "Terms": "Terms", "Test_Connection": "Test Connection", "Test_Desktop_Notifications": "Test Desktop Notifications", + "Test_LDAP_Search": "Test LDAP Search", "Texts": "Texts", "Thank_you_exclamation_mark": "Thank you!", "Thank_you_for_your_feedback": "Thank you for your feedback", diff --git a/server/lib/ldap/Connection.ts b/server/lib/ldap/Connection.ts index 532022081d4..37f319dd2d9 100644 --- a/server/lib/ldap/Connection.ts +++ b/server/lib/ldap/Connection.ts @@ -4,7 +4,6 @@ import { settings } from '../../../app/settings/server'; import type { ILDAPConnectionOptions, LDAPEncryptionType, LDAPSearchScope } from '../../../definition/ldap/ILDAPOptions'; import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry'; import type { ILDAPCallback, ILDAPPageCallback } from '../../../definition/ldap/ILDAPCallback'; -import { callbacks } from '../../../app/callbacks/server'; import { logger, connLogger, searchLogger, authLogger, bindLogger, mapLogger } from './Logger'; import { getLDAPConditionalSetting } from './getLDAPConditionalSetting'; @@ -550,8 +549,8 @@ export class LDAPConnection { this.usingAuthentication = true; } - protected async runBeforeSearch(searchOptions: ldapjs.SearchOptions): Promise { - callbacks.run('beforeLDAPSearch', searchOptions, this); + protected async runBeforeSearch(_searchOptions: ldapjs.SearchOptions): Promise { + this.maybeBindDN(); } /* diff --git a/server/lib/ldap/Manager.ts b/server/lib/ldap/Manager.ts index d7cc6cd5d4d..c9283e99a83 100644 --- a/server/lib/ldap/Manager.ts +++ b/server/lib/ldap/Manager.ts @@ -67,6 +67,24 @@ export class LDAPManager { } } + public static async testSearch(username: string): Promise { + const escapedUsername = ldapEscape.filter`${ username }`; + const ldap = new LDAPConnection(); + + try { + await ldap.connect(); + + const users = await ldap.searchByUsername(escapedUsername); + if (users.length !== 1) { + logger.debug(`Search returned ${ users.length } records for ${ escapedUsername }`); + throw new Error('User not found'); + } + } catch (error) { + logger.error(error); + throw error; + } + } + public static syncUserAvatar(user: IUser, ldapUser: ILDAPEntry): void { if (!user?._id || settings.get('LDAP_Sync_User_Avatar') !== true) { return; diff --git a/server/sdk/types/ILDAPService.ts b/server/sdk/types/ILDAPService.ts index e321db4c056..8f6d0578d2b 100644 --- a/server/sdk/types/ILDAPService.ts +++ b/server/sdk/types/ILDAPService.ts @@ -3,4 +3,5 @@ import type { LDAPLoginResult } from '../../../definition/ldap/ILDAPLoginResult' export interface ILDAPService { loginRequest(username: string, password: string): Promise; testConnection(): Promise; + testSearch(username: string): Promise; } diff --git a/server/services/ldap/service.ts b/server/services/ldap/service.ts index 087713e2838..f08c3a4188e 100644 --- a/server/services/ldap/service.ts +++ b/server/services/ldap/service.ts @@ -13,4 +13,8 @@ export class LDAPService extends ServiceClass implements ILDAPService { async testConnection(): Promise { return LDAPManager.testConnection(); } + + async testSearch(username: string): Promise { + return LDAPManager.testSearch(username); + } }