SamlSecuritySettingContents.jsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /* eslint-disable react/no-danger */
  2. import React from 'react';
  3. import { pathUtils } from '@growi/core/dist/utils';
  4. import { useTranslation } from 'next-i18next';
  5. import PropTypes from 'prop-types';
  6. import { Collapse } from 'reactstrap';
  7. import urljoin from 'url-join';
  8. import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
  9. import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
  10. import { toastSuccess, toastError } from '~/client/util/toastr';
  11. import { useSiteUrl } from '~/stores-universal/context';
  12. import { withUnstatedContainers } from '../../UnstatedUtils';
  13. class SamlSecurityManagementContents extends React.Component {
  14. constructor(props) {
  15. super(props);
  16. this.state = {
  17. isHelpOpened: false,
  18. };
  19. this.onClickSubmit = this.onClickSubmit.bind(this);
  20. }
  21. async onClickSubmit() {
  22. const { t, adminSamlSecurityContainer, adminGeneralSecurityContainer } = this.props;
  23. try {
  24. await adminSamlSecurityContainer.updateSamlSetting();
  25. toastSuccess(t('security_settings.SAML.updated_saml'));
  26. }
  27. catch (err) {
  28. toastError(err);
  29. }
  30. try {
  31. await adminGeneralSecurityContainer.retrieveSetupStratedies();
  32. }
  33. catch (err) {
  34. toastError(err);
  35. }
  36. }
  37. render() {
  38. const {
  39. t, adminGeneralSecurityContainer, adminSamlSecurityContainer, siteUrl,
  40. } = this.props;
  41. const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
  42. const { isSamlEnabled } = adminGeneralSecurityContainer.state;
  43. const samlCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/saml/callback');
  44. return (
  45. <React.Fragment>
  46. <h2 className="alert-anchor border-bottom">
  47. {t('security_settings.SAML.name')}
  48. </h2>
  49. {useOnlyEnvVars && (
  50. <p
  51. className="alert alert-info"
  52. dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
  53. />
  54. )}
  55. <div className="row mt-4 mb-5">
  56. <div className="col-6 offset-3">
  57. <div className="form-check form-switch form-check-success">
  58. <input
  59. id="isSamlEnabled"
  60. className="form-check-input"
  61. type="checkbox"
  62. checked={adminGeneralSecurityContainer.state.isSamlEnabled}
  63. onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
  64. disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
  65. />
  66. <label className="form-label form-check-label" htmlFor="isSamlEnabled">
  67. {t('security_settings.SAML.enable_saml')}
  68. </label>
  69. </div>
  70. {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
  71. && <div className="badge text-bg-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
  72. </div>
  73. </div>
  74. <div className="row mb-5">
  75. <label className="text-start text-md-end col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
  76. <div className="col-md-6">
  77. <input
  78. className="form-control"
  79. type="text"
  80. defaultValue={samlCallbackUrl}
  81. readOnly
  82. />
  83. <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
  84. {(siteUrl == null || siteUrl === '') && (
  85. <div className="alert alert-danger">
  86. <span className="material-symbols-outlined">error</span>
  87. <span
  88. // eslint-disable-next-line max-len
  89. dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
  90. />
  91. </div>
  92. )}
  93. </div>
  94. </div>
  95. {isSamlEnabled && (
  96. <React.Fragment>
  97. {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
  98. <div className="alert alert-danger">
  99. {t('security_settings.missing mandatory configs')}
  100. <ul className="mb-0">
  101. {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
  102. const key = configKey.replace('security:passport-saml:', '');
  103. return <li key={configKey}>{t(`security_settings.form_item_name.${key}`)}</li>;
  104. })}
  105. </ul>
  106. </div>
  107. )}
  108. <h3 className="alert-anchor border-bottom mb-3">
  109. Basic Settings
  110. </h3>
  111. <table className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}>
  112. <colgroup>
  113. <col className="item-name" />
  114. <col className="from-db" />
  115. <col className="from-env-vars" />
  116. </colgroup>
  117. <thead>
  118. <tr><th></th><th>Database</th><th>Environment variables</th></tr>
  119. </thead>
  120. <tbody>
  121. <tr>
  122. <th>{t('security_settings.form_item_name.entryPoint')}</th>
  123. <td>
  124. <input
  125. className="form-control"
  126. type="text"
  127. name="samlEntryPoint"
  128. readOnly={useOnlyEnvVars}
  129. value={adminSamlSecurityContainer.state.samlEntryPoint}
  130. onChange={e => adminSamlSecurityContainer.changeSamlEntryPoint(e.target.value)}
  131. />
  132. </td>
  133. <td>
  134. <input
  135. className="form-control"
  136. type="text"
  137. value={adminSamlSecurityContainer.state.envEntryPoint || ''}
  138. readOnly
  139. />
  140. <p className="form-text text-muted">
  141. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
  142. </p>
  143. </td>
  144. </tr>
  145. <tr>
  146. <th>{t('security_settings.form_item_name.issuer')}</th>
  147. <td>
  148. <input
  149. className="form-control"
  150. type="text"
  151. name="samlEnvVarissuer"
  152. readOnly={useOnlyEnvVars}
  153. value={adminSamlSecurityContainer.state.samlIssuer}
  154. onChange={e => adminSamlSecurityContainer.changeSamlIssuer(e.target.value)}
  155. />
  156. </td>
  157. <td>
  158. <input
  159. className="form-control"
  160. type="text"
  161. value={adminSamlSecurityContainer.state.envIssuer || ''}
  162. readOnly
  163. />
  164. <p className="form-text text-muted">
  165. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
  166. </p>
  167. </td>
  168. </tr>
  169. <tr>
  170. <th>{t('security_settings.form_item_name.cert')}</th>
  171. <td>
  172. <textarea
  173. className="form-control form-control-sm"
  174. type="text"
  175. rows="5"
  176. name="samlCert"
  177. readOnly={useOnlyEnvVars}
  178. value={adminSamlSecurityContainer.state.samlCert}
  179. onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
  180. />
  181. <p>
  182. <small>
  183. {t('security_settings.SAML.cert_detail')}
  184. </small>
  185. </p>
  186. <div>
  187. <small>
  188. e.g.
  189. <pre className="card custom-card">{`-----BEGIN CERTIFICATE-----
  190. MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
  191. UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
  192. ...
  193. crmVwBzbloUO2l6k1ibwD2WVwpdxMKIF5z58HfKAvxZAzCHE7kMEZr1ge30WRXQA
  194. pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
  195. -----END CERTIFICATE-----
  196. `}
  197. </pre>
  198. </small>
  199. </div>
  200. </td>
  201. <td>
  202. <textarea
  203. className="form-control form-control-sm"
  204. type="text"
  205. rows="5"
  206. readOnly
  207. value={adminSamlSecurityContainer.state.envCert || ''}
  208. />
  209. <p className="form-text text-muted">
  210. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
  211. </p>
  212. </td>
  213. </tr>
  214. </tbody>
  215. </table>
  216. <h3 className="alert-anchor border-bottom mt-5 mb-3">
  217. Attribute Mapping
  218. </h3>
  219. <table className="table settings-table">
  220. <colgroup>
  221. <col className="item-name" />
  222. <col className="from-db" />
  223. <col className="from-env-vars" />
  224. </colgroup>
  225. <thead>
  226. <tr><th></th><th>Database</th><th>Environment variables</th></tr>
  227. </thead>
  228. <tbody>
  229. <tr>
  230. <th>{t('security_settings.form_item_name.attrMapId')}</th>
  231. <td>
  232. <input
  233. className="form-control"
  234. type="text"
  235. value={adminSamlSecurityContainer.state.samlAttrMapId}
  236. onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
  237. />
  238. <p className="form-text text-muted">
  239. <small>
  240. {t('security_settings.SAML.id_detail')}
  241. </small>
  242. </p>
  243. </td>
  244. <td>
  245. <input
  246. className="form-control"
  247. type="text"
  248. value={adminSamlSecurityContainer.state.envAttrMapId || ''}
  249. readOnly
  250. />
  251. <p className="form-text text-muted">
  252. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
  253. </p>
  254. </td>
  255. </tr>
  256. <tr>
  257. <th>{t('security_settings.form_item_name.attrMapUsername')}</th>
  258. <td>
  259. <input
  260. className="form-control"
  261. type="text"
  262. value={adminSamlSecurityContainer.state.samlAttrMapUsername}
  263. onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
  264. />
  265. <p className="form-text text-muted">
  266. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.username_detail') }} />
  267. </p>
  268. </td>
  269. <td>
  270. <input
  271. className="form-control"
  272. type="text"
  273. value={adminSamlSecurityContainer.state.envAttrMapUsername || ''}
  274. readOnly
  275. />
  276. <p className="form-text text-muted">
  277. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
  278. </p>
  279. </td>
  280. </tr>
  281. <tr>
  282. <th>{t('security_settings.form_item_name.attrMapMail')}</th>
  283. <td>
  284. <input
  285. className="form-control"
  286. type="text"
  287. value={adminSamlSecurityContainer.state.samlAttrMapMail}
  288. onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
  289. />
  290. <p className="form-text text-muted">
  291. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: 'Email' }) }} />
  292. </p>
  293. </td>
  294. <td>
  295. <input
  296. className="form-control"
  297. type="text"
  298. value={adminSamlSecurityContainer.state.envAttrMapMail || ''}
  299. readOnly
  300. />
  301. <p className="form-text text-muted">
  302. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
  303. </p>
  304. </td>
  305. </tr>
  306. <tr>
  307. <th>{t('security_settings.form_item_name.attrMapFirstName')}</th>
  308. <td>
  309. <input
  310. className="form-control"
  311. type="text"
  312. value={adminSamlSecurityContainer.state.samlAttrMapFirstName}
  313. onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
  314. />
  315. <p className="form-text text-muted">
  316. {/* eslint-disable-next-line max-len */}
  317. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapFirstName') }) }} />
  318. </p>
  319. </td>
  320. <td>
  321. <input
  322. className="form-control"
  323. type="text"
  324. value={adminSamlSecurityContainer.state.envAttrMapFirstName || ''}
  325. readOnly
  326. />
  327. <p className="form-text text-muted">
  328. <small>
  329. <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
  330. <br />
  331. <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'firstName' }) }} />
  332. </small>
  333. </p>
  334. </td>
  335. </tr>
  336. <tr>
  337. <th>{t('security_settings.form_item_name.attrMapLastName')}</th>
  338. <td>
  339. <input
  340. className="form-control"
  341. type="text"
  342. value={adminSamlSecurityContainer.state.samlAttrMapLastName}
  343. onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
  344. />
  345. <p className="form-text text-muted">
  346. {/* eslint-disable-next-line max-len */}
  347. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapLastName') }) }} />
  348. </p>
  349. </td>
  350. <td>
  351. <input
  352. className="form-control"
  353. type="text"
  354. value={adminSamlSecurityContainer.state.envAttrMapLastName || ''}
  355. readOnly
  356. />
  357. <p className="form-text text-muted">
  358. <small>
  359. <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
  360. <br />
  361. <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'lastName' }) }} />
  362. </small>
  363. </p>
  364. </td>
  365. </tr>
  366. </tbody>
  367. </table>
  368. <h3 className="alert-anchor border-bottom mt-5 mb-4">
  369. Attribute Mapping Options
  370. </h3>
  371. <div className="row ms-3">
  372. <div className="form-check form-check-success">
  373. <input
  374. id="bindByUserName-SAML"
  375. className="form-check-input"
  376. type="checkbox"
  377. checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
  378. onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
  379. />
  380. <label
  381. className="form-label form-check-label"
  382. htmlFor="bindByUserName-SAML"
  383. dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
  384. />
  385. </div>
  386. <p className="form-text text-muted">
  387. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
  388. </p>
  389. </div>
  390. <div className="row mb-5 ms-3">
  391. <div className="form-check form-check-success">
  392. <input
  393. id="bindByEmail-SAML"
  394. className="form-check-input"
  395. type="checkbox"
  396. checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
  397. onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
  398. />
  399. <label
  400. className="form-label form-check-label"
  401. htmlFor="bindByEmail-SAML"
  402. dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
  403. />
  404. </div>
  405. <p className="form-text text-muted">
  406. <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
  407. </p>
  408. </div>
  409. <h3 className="alert-anchor border-bottom mb-4">
  410. Attribute-based Login Control
  411. </h3>
  412. <p className="form-text text-muted">
  413. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_detail') }} />
  414. </p>
  415. <table className="table settings-table">
  416. <colgroup>
  417. <col className="item-name" />
  418. <col className="from-db" />
  419. <col className="from-env-vars" />
  420. </colgroup>
  421. <thead>
  422. <tr><th></th><th>Database</th><th>Environment variables</th></tr>
  423. </thead>
  424. <tbody>
  425. <tr>
  426. <th>
  427. { t('security_settings.form_item_name.ABLCRule') }
  428. </th>
  429. <td>
  430. <textarea
  431. className="form-control"
  432. type="text"
  433. value={adminSamlSecurityContainer.state.samlABLCRule || ''}
  434. onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
  435. />
  436. <div className="mt-2">
  437. <p>
  438. See&nbsp;
  439. <a
  440. href="https://lucene.apache.org/core/2_9_4/queryparsersyntax.html"
  441. target="_blank"
  442. rel="noreferer noreferrer"
  443. >
  444. Apache Lucene - Query Parser Syntax <span className="growi-custom-icons">external_link</span>
  445. </a>.
  446. </p>
  447. <div className="accordion" id="accordionId">
  448. <div className="accordion-item p-1">
  449. <h2 className="accordion-header">
  450. <button
  451. className="btn btn-link text-start"
  452. type="button"
  453. onClick={() => this.setState({ isHelpOpened: !this.state.isHelpOpened })}
  454. aria-expanded="true"
  455. aria-controls="ablchelp"
  456. >
  457. <span
  458. className="material-symbols-outlined me-1"
  459. small
  460. >{this.state.isHelpOpened ? 'expand_more' : 'chevron_right'}
  461. </span> Show more...
  462. </button>
  463. </h2>
  464. <Collapse isOpen={this.state.isHelpOpened}>
  465. <div className="accordion-body">
  466. <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_help') }} />
  467. <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example1') }} />
  468. <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example2') }} />
  469. </div>
  470. </Collapse>
  471. </div>
  472. </div>
  473. </div>
  474. </td>
  475. <td>
  476. <textarea
  477. className="form-control"
  478. type="text"
  479. value={adminSamlSecurityContainer.state.envABLCRule || ''}
  480. readOnly
  481. />
  482. <p className="form-text text-muted">
  483. <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
  484. </p>
  485. </td>
  486. </tr>
  487. </tbody>
  488. </table>
  489. <div className="row my-3">
  490. <div className="offset-3 col-5">
  491. <button
  492. type="button"
  493. className="btn btn-primary"
  494. disabled={adminSamlSecurityContainer.state.retrieveError != null}
  495. onClick={this.onClickSubmit}
  496. >
  497. {t('Update')}
  498. </button>
  499. </div>
  500. </div>
  501. </React.Fragment>
  502. )}
  503. </React.Fragment>
  504. );
  505. }
  506. }
  507. SamlSecurityManagementContents.propTypes = {
  508. t: PropTypes.func.isRequired, // i18next
  509. adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
  510. adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
  511. siteUrl: PropTypes.string,
  512. };
  513. const SamlSecurityManagementContentsWrapperFC = (props) => {
  514. const { t } = useTranslation('admin');
  515. const { data: siteUrl } = useSiteUrl();
  516. return <SamlSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
  517. };
  518. const SamlSecurityManagementContentsWrapper = withUnstatedContainers(SamlSecurityManagementContentsWrapperFC, [
  519. AdminGeneralSecurityContainer,
  520. AdminSamlSecurityContainer,
  521. ]);
  522. export default SamlSecurityManagementContentsWrapper;