LdapSecuritySettingContents.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. import React, { useCallback, useEffect, useState } from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import { useForm } from 'react-hook-form';
  4. import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
  5. import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
  6. import { toastError, toastSuccess } from '~/client/util/toastr';
  7. import { withUnstatedContainers } from '../../UnstatedUtils';
  8. import LdapAuthTestModal from './LdapAuthTestModal';
  9. type Props = {
  10. adminGeneralSecurityContainer: AdminGeneralSecurityContainer;
  11. adminLdapSecurityContainer: AdminLdapSecurityContainer;
  12. };
  13. const LdapSecuritySettingContents = (props: Props) => {
  14. const { adminGeneralSecurityContainer, adminLdapSecurityContainer } = props;
  15. const { t } = useTranslation('admin');
  16. const { isLdapEnabled } = adminGeneralSecurityContainer.state;
  17. const {
  18. serverUrl,
  19. ldapBindDN,
  20. ldapBindDNPassword,
  21. ldapSearchFilter,
  22. ldapAttrMapUsername,
  23. ldapAttrMapMail,
  24. ldapAttrMapName,
  25. ldapGroupSearchBase,
  26. ldapGroupSearchFilter,
  27. ldapGroupDnProperty,
  28. } = adminLdapSecurityContainer.state;
  29. const [isLdapAuthTestModalShown, setIsLdapAuthTestModalShown] =
  30. useState(false);
  31. const { register, handleSubmit, reset } = useForm();
  32. useEffect(() => {
  33. reset({
  34. serverUrl,
  35. ldapBindDN,
  36. ldapBindDNPassword,
  37. ldapSearchFilter,
  38. ldapAttrMapUsername,
  39. ldapAttrMapMail,
  40. ldapAttrMapName,
  41. ldapGroupSearchBase,
  42. ldapGroupSearchFilter,
  43. ldapGroupDnProperty,
  44. });
  45. }, [
  46. reset,
  47. serverUrl,
  48. ldapBindDN,
  49. ldapBindDNPassword,
  50. ldapSearchFilter,
  51. ldapAttrMapUsername,
  52. ldapAttrMapMail,
  53. ldapAttrMapName,
  54. ldapGroupSearchBase,
  55. ldapGroupSearchFilter,
  56. ldapGroupDnProperty,
  57. ]);
  58. const onSubmit = useCallback(
  59. async (data) => {
  60. try {
  61. await adminLdapSecurityContainer.updateLdapSetting({
  62. serverUrl: data.serverUrl,
  63. isUserBind: adminLdapSecurityContainer.state.isUserBind,
  64. ldapBindDN: data.ldapBindDN,
  65. ldapBindDNPassword: data.ldapBindDNPassword,
  66. ldapSearchFilter: data.ldapSearchFilter,
  67. ldapAttrMapUsername: data.ldapAttrMapUsername,
  68. isSameUsernameTreatedAsIdenticalUser:
  69. adminLdapSecurityContainer.state
  70. .isSameUsernameTreatedAsIdenticalUser,
  71. ldapAttrMapMail: data.ldapAttrMapMail,
  72. ldapAttrMapName: data.ldapAttrMapName,
  73. ldapGroupSearchBase: data.ldapGroupSearchBase,
  74. ldapGroupSearchFilter: data.ldapGroupSearchFilter,
  75. ldapGroupDnProperty: data.ldapGroupDnProperty,
  76. });
  77. await adminGeneralSecurityContainer.retrieveSetupStratedies();
  78. toastSuccess(t('security_settings.ldap.updated_ldap'));
  79. } catch (err) {
  80. toastError(err);
  81. }
  82. },
  83. [t, adminLdapSecurityContainer, adminGeneralSecurityContainer],
  84. );
  85. const openLdapAuthTestModal = useCallback(() => {
  86. setIsLdapAuthTestModalShown(true);
  87. }, []);
  88. const closeLdapAuthTestModal = useCallback(() => {
  89. setIsLdapAuthTestModalShown(false);
  90. }, []);
  91. return (
  92. <React.Fragment>
  93. <h2 className="alert-anchor border-bottom mb-4">LDAP</h2>
  94. <div className="row my-4">
  95. <div className="col-6 offset-3">
  96. <div className="form-check form-switch form-check-success">
  97. <input
  98. id="isLdapEnabled"
  99. className="form-check-input"
  100. type="checkbox"
  101. checked={isLdapEnabled}
  102. onChange={() => {
  103. adminGeneralSecurityContainer.switchIsLdapEnabled();
  104. }}
  105. />
  106. <label
  107. className="form-label form-check-label"
  108. htmlFor="isLdapEnabled"
  109. >
  110. {t('security_settings.ldap.enable_ldap')}
  111. </label>
  112. </div>
  113. {!adminGeneralSecurityContainer.state.setupStrategies.includes(
  114. 'ldap',
  115. ) &&
  116. isLdapEnabled && (
  117. <div className="badge text-bg-warning">
  118. {t('security_settings.setup_is_not_yet_complete')}
  119. </div>
  120. )}
  121. </div>
  122. </div>
  123. {isLdapEnabled && (
  124. <form onSubmit={handleSubmit(onSubmit)}>
  125. <h3 className="border-bottom mb-4">
  126. {t('security_settings.configuration')}
  127. </h3>
  128. <div className="row my-3">
  129. <label
  130. htmlFor="serverUrl"
  131. className="text-start text-md-end col-md-3 col-form-label"
  132. >
  133. Server URL
  134. </label>
  135. <div className="col-md-9">
  136. <input
  137. className="form-control"
  138. type="text"
  139. {...register('serverUrl')}
  140. />
  141. <small>
  142. <p
  143. className="form-text text-muted"
  144. // eslint-disable-next-line react/no-danger
  145. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  146. dangerouslySetInnerHTML={{
  147. __html: t('security_settings.ldap.server_url_detail'),
  148. }}
  149. />
  150. {t('security_settings.example')}:{' '}
  151. <code>
  152. ldaps://ldap.company.com/ou=people,dc=company,dc=com
  153. </code>
  154. </small>
  155. </div>
  156. </div>
  157. <div className="row my-3">
  158. <span className="form-label text-start text-md-end col-md-3 col-form-label">
  159. <strong>{t('security_settings.ldap.bind_mode')}</strong>
  160. </span>
  161. <div className="col-md-9">
  162. <div className="dropdown">
  163. <button
  164. className="btn btn-outline-secondary dropdown-toggle"
  165. type="button"
  166. id="dropdownMenuButton"
  167. data-bs-toggle="dropdown"
  168. aria-haspopup="true"
  169. aria-expanded="true"
  170. >
  171. {adminLdapSecurityContainer.state.isUserBind ? (
  172. <span className="pull-left">
  173. {t('security_settings.ldap.bind_user')}
  174. </span>
  175. ) : (
  176. <span className="pull-left">
  177. {t('security_settings.ldap.bind_manager')}
  178. </span>
  179. )}
  180. </button>
  181. <div className="dropdown-menu">
  182. <button
  183. className="dropdown-item"
  184. type="button"
  185. onClick={() => {
  186. adminLdapSecurityContainer.changeLdapBindMode(true);
  187. }}
  188. >
  189. {t('security_settings.ldap.bind_user')}
  190. </button>
  191. <button
  192. className="dropdown-item"
  193. type="button"
  194. onClick={() => {
  195. adminLdapSecurityContainer.changeLdapBindMode(false);
  196. }}
  197. >
  198. {t('security_settings.ldap.bind_manager')}
  199. </button>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. <div className="row my-3">
  205. <label
  206. className="form-label text-start text-md-end col-md-3 col-form-label"
  207. htmlFor="ldapBindDN"
  208. >
  209. <strong>Bind DN</strong>
  210. </label>
  211. <div className="col-md-9">
  212. <input
  213. id="ldapBindDN"
  214. className="form-control"
  215. type="text"
  216. {...register('ldapBindDN')}
  217. />
  218. {adminLdapSecurityContainer.state.isUserBind === true ? (
  219. <p className="form-text text-muted">
  220. <small>
  221. {t('security_settings.ldap.bind_DN_user_detail1')}
  222. <br />
  223. {/* eslint-disable-next-line react/no-danger */}
  224. <span
  225. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  226. dangerouslySetInnerHTML={{
  227. __html: t(
  228. 'security_settings.ldap.bind_DN_user_detail2',
  229. ),
  230. }}
  231. />
  232. <br />
  233. {t('security_settings.example')}1:{' '}
  234. <code>uid={'{{ username }}'},dc=domain,dc=com</code>
  235. <br />
  236. {t('security_settings.example')}2:{' '}
  237. <code>{'{{ username }}'}@domain.com</code>
  238. </small>
  239. </p>
  240. ) : (
  241. <p className="form-text text-muted">
  242. <small>
  243. {t('security_settings.ldap.bind_DN_manager_detail')}
  244. <br />
  245. {t('security_settings.example')}1:{' '}
  246. <code>uid=admin,dc=domain,dc=com</code>
  247. <br />
  248. {t('security_settings.example')}2:{' '}
  249. <code>admin@domain.com</code>
  250. </small>
  251. </p>
  252. )}
  253. </div>
  254. </div>
  255. <div className="row my-3">
  256. <label
  257. className="text-start text-md-end col-md-3 col-form-label"
  258. htmlFor="bindDNPassword"
  259. >
  260. <strong>{t('security_settings.ldap.bind_DN_password')}</strong>
  261. </label>
  262. <div className="col-md-9">
  263. {adminLdapSecurityContainer.state.isUserBind ? (
  264. <p className="card custom-card">
  265. <small>
  266. {t('security_settings.ldap.bind_DN_password_user_detail')}
  267. </small>
  268. </p>
  269. ) : (
  270. <>
  271. <input
  272. className="form-control"
  273. type="password"
  274. {...register('ldapBindDNPassword')}
  275. />
  276. <p className="form-text text-muted">
  277. <small>
  278. {t(
  279. 'security_settings.ldap.bind_DN_password_manager_detail',
  280. )}
  281. </small>
  282. </p>
  283. </>
  284. )}
  285. </div>
  286. </div>
  287. <div className="row my-3">
  288. <label
  289. className="form-label text-start text-md-end col-md-3 col-form-label"
  290. htmlFor="ldapSearchFilter"
  291. >
  292. <strong>{t('security_settings.ldap.search_filter')}</strong>
  293. </label>
  294. <div className="col-md-9">
  295. <input
  296. id="ldapSearchFilter"
  297. className="form-control"
  298. type="text"
  299. {...register('ldapSearchFilter')}
  300. />
  301. <p className="form-text text-muted">
  302. <small>
  303. {t('security_settings.ldap.search_filter_detail1')}
  304. <br />
  305. {/* eslint-disable-next-line react/no-danger */}
  306. <span
  307. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  308. dangerouslySetInnerHTML={{
  309. __html: t('security_settings.ldap.search_filter_detail2'),
  310. }}
  311. />
  312. <br />
  313. {/* eslint-disable-next-line react/no-danger */}
  314. <span
  315. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  316. dangerouslySetInnerHTML={{
  317. __html: t('security_settings.ldap.search_filter_detail3'),
  318. }}
  319. />
  320. </small>
  321. </p>
  322. <p className="form-text text-muted">
  323. <small>
  324. {t('security_settings.example')}1 -{' '}
  325. {t('security_settings.ldap.search_filter_example1')}:
  326. <code>
  327. (|(uid={'{{username}}'})(mail={'{{username}}'}))
  328. </code>
  329. <br />
  330. {t('security_settings.example')}2 -{' '}
  331. {t('security_settings.ldap.search_filter_example2')}:
  332. <code>(sAMAccountName={'{{username}}'})</code>
  333. </small>
  334. </p>
  335. </div>
  336. </div>
  337. <h3 className="alert-anchor border-bottom mb-4">
  338. Attribute Mapping ({t('optional')})
  339. </h3>
  340. <div className="row my-3">
  341. <label
  342. className="form-label text-start text-md-end col-md-3 col-form-label"
  343. htmlFor="attrMapUsername"
  344. >
  345. <strong>{t('username')}</strong>
  346. </label>
  347. <div className="col-md-9">
  348. <input
  349. className="form-control"
  350. type="text"
  351. placeholder="Default: uid"
  352. {...register('ldapAttrMapUsername')}
  353. />
  354. <p className="form-text text-muted">
  355. {/* eslint-disable-next-line react/no-danger */}
  356. <small
  357. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  358. dangerouslySetInnerHTML={{
  359. __html: t('security_settings.ldap.username_detail'),
  360. }}
  361. />
  362. </p>
  363. </div>
  364. </div>
  365. <div className="row my-3">
  366. <div className="offset-md-3 col-md-9">
  367. <div className="form-check form-check-success">
  368. <input
  369. type="checkbox"
  370. className="form-check-input"
  371. id="isSameUsernameTreatedAsIdenticalUser"
  372. checked={
  373. adminLdapSecurityContainer.state
  374. .isSameUsernameTreatedAsIdenticalUser
  375. }
  376. onChange={() => {
  377. adminLdapSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser();
  378. }}
  379. />
  380. <label
  381. className="form-check-label"
  382. htmlFor="isSameUsernameTreatedAsIdenticalUser"
  383. >
  384. <span
  385. // eslint-disable-next-line react/no-danger
  386. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  387. dangerouslySetInnerHTML={{
  388. __html: t(
  389. 'security_settings.Treat username matching as identical',
  390. ),
  391. }}
  392. />
  393. </label>
  394. </div>
  395. <p className="form-text text-muted">
  396. {/* eslint-disable-next-line react/no-danger */}
  397. <small
  398. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  399. dangerouslySetInnerHTML={{
  400. __html: t(
  401. 'security_settings.Treat username matching as identical_warn',
  402. ),
  403. }}
  404. />
  405. </p>
  406. </div>
  407. </div>
  408. <div className="row my-3">
  409. <label
  410. className="form-label text-start text-md-end col-md-3 col-form-label"
  411. htmlFor="attrMapMail"
  412. >
  413. <strong>{t('Email')}</strong>
  414. </label>
  415. <div className="col-md-9">
  416. <input
  417. className="form-control"
  418. type="text"
  419. placeholder="Default: mail"
  420. {...register('ldapAttrMapMail')}
  421. />
  422. <p className="form-text text-muted">
  423. <small>{t('security_settings.ldap.mail_detail')}</small>
  424. </p>
  425. </div>
  426. </div>
  427. <div className="row my-3">
  428. <label
  429. className="form-label text-start text-md-end col-md-3 col-form-label"
  430. htmlFor="attrMapName"
  431. >
  432. <strong>{t('Name')}</strong>
  433. </label>
  434. <div className="col-md-9">
  435. <input
  436. className="form-control"
  437. type="text"
  438. {...register('ldapAttrMapName')}
  439. />
  440. <p className="form-text text-muted">
  441. <small>{t('security_settings.ldap.name_detail')}</small>
  442. </p>
  443. </div>
  444. </div>
  445. <h3 className="alert-anchor border-bottom mb-4">
  446. {t('security_settings.ldap.group_search_filter')} ({t('optional')})
  447. </h3>
  448. <div className="row my-3">
  449. <label
  450. className="form-label text-start text-md-end col-md-3 col-form-label"
  451. htmlFor="groupSearchBase"
  452. >
  453. <strong>
  454. {t('security_settings.ldap.group_search_base_DN')}
  455. </strong>
  456. </label>
  457. <div className="col-md-9">
  458. <input
  459. className="form-control"
  460. type="text"
  461. {...register('ldapGroupSearchBase')}
  462. />
  463. <p className="form-text text-muted">
  464. <small>
  465. {/* eslint-disable-next-line react/no-danger */}
  466. <span
  467. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  468. dangerouslySetInnerHTML={{
  469. __html: t(
  470. 'security_settings.ldap.group_search_base_DN_detail',
  471. ),
  472. }}
  473. />
  474. <br />
  475. {t('security_settings.example')}:{' '}
  476. <code>ou=groups,dc=domain,dc=com</code>
  477. </small>
  478. </p>
  479. </div>
  480. </div>
  481. <div className="row my-3">
  482. <label
  483. className="form-label text-start text-md-end col-md-3 col-form-label"
  484. htmlFor="groupSearchFilter"
  485. >
  486. <strong>{t('security_settings.ldap.group_search_filter')}</strong>
  487. </label>
  488. <div className="col-md-9">
  489. <input
  490. className="form-control"
  491. type="text"
  492. {...register('ldapGroupSearchFilter')}
  493. />
  494. <p className="form-text text-muted">
  495. <small>
  496. {/* eslint-disable react/no-danger */}
  497. <span
  498. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  499. dangerouslySetInnerHTML={{
  500. __html: t(
  501. 'security_settings.ldap.group_search_filter_detail1',
  502. ),
  503. }}
  504. />
  505. <br />
  506. <span
  507. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  508. dangerouslySetInnerHTML={{
  509. __html: t(
  510. 'security_settings.ldap.group_search_filter_detail2',
  511. ),
  512. }}
  513. />
  514. <br />
  515. <span
  516. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  517. dangerouslySetInnerHTML={{
  518. __html: t(
  519. 'security_settings.ldap.group_search_filter_detail3',
  520. ),
  521. }}
  522. />
  523. {/* eslint-enable react/no-danger */}
  524. </small>
  525. </p>
  526. <p className="form-text text-muted">
  527. <small>
  528. {t('security_settings.example')}:
  529. {/* eslint-disable-next-line react/no-danger */}
  530. <span
  531. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  532. dangerouslySetInnerHTML={{
  533. __html: t(
  534. 'security_settings.ldap.group_search_filter_detail4',
  535. ),
  536. }}
  537. />
  538. </small>
  539. </p>
  540. </div>
  541. </div>
  542. <div className="row my-3">
  543. <label
  544. className="form-label text-start text-md-end col-md-3 col-form-label"
  545. htmlFor="groupDnProperty"
  546. >
  547. <strong>
  548. {t('security_settings.ldap.group_search_user_DN_property')}
  549. </strong>
  550. </label>
  551. <div className="col-md-9">
  552. <input
  553. className="form-control"
  554. type="text"
  555. placeholder="Default: uid"
  556. {...register('ldapGroupDnProperty')}
  557. />
  558. <p className="form-text text-muted">
  559. {/* eslint-disable-next-line react/no-danger */}
  560. <small
  561. // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted translation markup
  562. dangerouslySetInnerHTML={{
  563. __html: t(
  564. 'security_settings.ldap.group_search_user_DN_property_detail',
  565. ),
  566. }}
  567. />
  568. </p>
  569. </div>
  570. </div>
  571. <div className="row my-3">
  572. <div className="offset-3 col-5">
  573. <button
  574. type="submit"
  575. className="btn btn-primary"
  576. disabled={
  577. adminLdapSecurityContainer.state.retrieveError != null
  578. }
  579. >
  580. {t('Update')}
  581. </button>
  582. <button
  583. type="button"
  584. className="btn btn-outline-secondary ms-2"
  585. onClick={openLdapAuthTestModal}
  586. >
  587. {t('security_settings.ldap.test_config')}
  588. </button>
  589. </div>
  590. </div>
  591. </form>
  592. )}
  593. <LdapAuthTestModal
  594. isOpen={isLdapAuthTestModalShown}
  595. onClose={closeLdapAuthTestModal}
  596. />
  597. </React.Fragment>
  598. );
  599. };
  600. const LdapSecuritySettingContentsWrapper = withUnstatedContainers(
  601. LdapSecuritySettingContents,
  602. [AdminGeneralSecurityContainer, AdminLdapSecurityContainer],
  603. );
  604. export default LdapSecuritySettingContentsWrapper;