Просмотр исходного кода

Merge branch 'reactify-admin/security' into reactify-admin/oidc-security-setting

# Conflicts:
#	resource/locales/en-US/translation.json
#	resource/locales/ja/translation.json
itizawa 6 лет назад
Родитель
Сommit
55b681502c

+ 4 - 2
CHANGES.md

@@ -20,11 +20,13 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 * Support: Upgrade libs
     * growi-commons
 
-
-
 ## 3.5.23
 
+* Fix: Global Notification failed to send e-mail
+* Fix: Pagination is not working for trash list
 * Fix: Healthcheck API with `?connectToMiddlewares` returns error
+* Support: Upgrade libs
+    * growi-commons
 
 ## 3.5.22
 

+ 3 - 3
resource/locales/en-US/translation.json

@@ -490,8 +490,8 @@
     "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
     "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>%s</code> match",
     "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>%s</code>.",
-    "Use env var if empty": "Use env var <code>{{env}}</code> if empty",
-    "Use default if both are empty": "If both ​​are empty, the default value <code>%s</code> is used.",
+    "Use env var if empty": "Use env var <code>%s</code> if empty",
+    "Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
     "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
     "Local": {
       "name": "ID/Password",
@@ -534,7 +534,7 @@
       "enable_saml":"enable SAML",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
-      "mapping_detail": "Specification of mappings for %s when creating new users",
+      "mapping_detail": "Specification of mappings for {{target}} when creating new users",
       "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
       "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
       "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."

+ 5 - 5
resource/locales/ja/translation.json

@@ -483,10 +483,10 @@
     "optional": "オプション",
     "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
     "Treat username matching as identical_warn": "警告: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
-    "Treat email matching as identical": "新規ログイン時、<code>%s</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
-    "Treat email matching as identical_warn": "警告: <code>%s</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
-    "Use env var if empty": "空の場合、環境変数 <code>{{env}}</code> を利用します",
-    "Use default if both are empty": "どちらの値も空の場合、デフォルト値 <code>%s</code> を利用します",
+    "Treat email matching as identical": "新規ログイン時、<code>email</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
+    "Treat email matching as identical_warn": "警告: <code>email</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
+    "Use env var if empty": "空の場合、環境変数 <code>%s</code> を利用します",
+    "Use default if both are empty": "どちらの値も空の場合、デフォルト値 <code>{{target}}</code> を利用します",
     "missing mandatory configs": "以下の必須項目の値がデータベースと環境変数のどちらにも設定されていません",
     "Local": {
       "name": "ID/Password",
@@ -529,7 +529,7 @@
       "enable_saml": "SAML を有効にする",
       "id_detail": "SAML Identity プロバイダ内で一意に識別可能な値を格納している属性",
       "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
-      "mapping_detail": "新規ユーザーの%sに関連付ける属性",
+      "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
       "cert_detail": "IdP からのレスポンスの validation を行うためのPEMエンコードされた X.509 証明書",
       "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します",
       "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"

+ 202 - 1
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -193,7 +193,208 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               </tbody>
             </table>
 
-            {/* TODO GW-635 Attribute Mapping */}
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping
+            </h3>
+
+            <table className={`table settings-table ${adminSamlSecurityContainer.state.useOnlyEnvVars && 'use-only-env-vars'}`}>
+              <colgroup>
+                <col className="item-name" />
+                <col className="from-db" />
+                <col className="from-env-vars" />
+              </colgroup>
+              <thead>
+                <tr><th></th><th>Database</th><th>Environment variables</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.attrMapId') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbAttrMapId}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbAttrMapId(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small>
+                        { t('security_setting.SAML.id_detail') }
+                      </small>
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarAttrMapId}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.attrMapUsername') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbAttrMapUserName}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbAttrMapUserName(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.username_detail') }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarAttrMapUserName}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.attrMapMail') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbAttrMapMail}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbAttrMapMail(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: 'Email' }) }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarAttrMapMail}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.attrMapFirstName') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbAttrMapFirstName}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbAttrMapFirstName(e.target.value)}
+                    />
+                    <p className="help-block">
+                      {/* eslint-disable-next-line max-len */}
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapFirstName') }) }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarAttrMapFirstName}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small>
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
+                        <br />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.Use default if both are empty', { target: 'firstName' }) }} />
+                      </small>
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.attrMapLastName') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbAttrMapLastName}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbAttrMapLastName(e.target.value)}
+                    />
+                    <p className="help-block">
+                      {/* eslint-disable-next-line max-len */}
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapLastName') }) }} />
+                    </p>
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarAttrMapLastName}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small>
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
+                        <br />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.Use default if both are empty', { target: 'lastName' }) }} />
+                      </small>
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            <h3 className="alert-anchor border-bottom">
+              Attribute Mapping Options
+            </h3>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByUserName-SAML"
+                    type="checkbox"
+                    checked={adminSamlSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser}
+                    onChange={() => { adminSamlSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByUserName-SAML"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="bindByEmail-SAML"
+                    type="checkbox"
+                    checked={adminSamlSecurityContainer.state.isSameEmailTreatedAsIdenticalUser}
+                    onChange={() => { adminSamlSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
+                  />
+                  <label
+                    htmlFor="bindByEmail-SAML"
+                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                  />
+                </div>
+                <p className="help-block">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                </p>
+              </div>
+            </div>
 
           </React.Fragment>
 

+ 64 - 3
src/client/js/services/AdminSamlSecurityContainer.js

@@ -28,6 +28,18 @@ export default class AdminSamlSecurityContainer extends Container {
       samlEnvVarIssuer: '',
       samlDbCert: '',
       samlEnvVarCert: '',
+      samlDbAttrMapId: '',
+      samlEnvVarAttrMapId: '',
+      samlDbAttrMapUserName: '',
+      samlEnvVarAttrMapUserName: '',
+      samlDbAttrMapMail: '',
+      samlEnvVarAttrMapMail: '',
+      samlDbAttrMapFirstName: '',
+      samlEnvVarAttrMapFirstName: '',
+      samlDbAttrMapLastName: '',
+      samlEnvVarAttrMapLastName: '',
+      isSameUsernameTreatedAsIdenticalUser: false,
+      isSameEmailTreatedAsIdenticalUser: false,
     };
 
     this.init();
@@ -47,24 +59,73 @@ export default class AdminSamlSecurityContainer extends Container {
   }
 
   /**
-   * Change saml db entry point
+   * Change samlDbEntryPoint
    */
   changeSamlDbEntryPoint(inputValue) {
     this.setState({ samlDbEntryPoint: inputValue });
   }
 
   /**
-   * Change saml db issuer
+   * Change samlDbIssuer
    */
   changeSamlDbIssuer(inputValue) {
     this.setState({ samlDbIssuer: inputValue });
   }
 
   /**
-   * Change saml db Cert
+   * Change samlDbCert
    */
   changeSamlDbCert(inputValue) {
     this.setState({ samlDbCert: inputValue });
   }
 
+  /**
+   * Change samlDbAttrMapId
+   */
+  changeSamlDbAttrMapId(inputValue) {
+    this.setState({ samlDbAttrMapId: inputValue });
+  }
+
+  /**
+   * Change samlDbAttrMapUserName
+   */
+  changeSamlDbAttrMapUserName(inputValue) {
+    this.setState({ samlDbAttrMapUserName: inputValue });
+  }
+
+  /**
+   * Change samlDbAttrMapMail
+   */
+  changeSamlDbAttrMapMail(inputValue) {
+    this.setState({ samlDbAttrMapMail: inputValue });
+  }
+
+  /**
+   * Change samlDbAttrMapFirstName
+   */
+  changeSamlDbAttrMapFirstName(inputValue) {
+    this.setState({ samlDbAttrMapFirstName: inputValue });
+  }
+
+  /**
+   * Change samlDbAttrMapLastName
+   */
+  changeSamlDbAttrMapLastName(inputValue) {
+    this.setState({ samlDbAttrMapLastName: inputValue });
+  }
+
+  /**
+   * Switch isSameUsernameTreatedAsIdenticalUser
+   */
+  switchIsSameUsernameTreatedAsIdenticalUser() {
+    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  }
+
+  /**
+   * Switch isSameEmailTreatedAsIdenticalUser
+   */
+  switchIsSameEmailTreatedAsIdenticalUser() {
+    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
+  }
+
 }

+ 3 - 3
src/server/routes/apiv3/markdown-setting.js

@@ -114,7 +114,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  properties:
    *                    status:
-   *                      $ref: '#/components/schemas/lineBreakParams'
+   *                      $ref: '#/components/schemas/LineBreakParams'
    */
   router.put('/lineBreak', loginRequiredStrictly, adminRequired, csrf, validator.lineBreak, ApiV3FormValidator, async(req, res) => {
 
@@ -167,7 +167,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  properties:
    *                    status:
-   *                      $ref: '#/components/schemas/presentationParams'
+   *                      $ref: '#/components/schemas/PresentationParams'
    */
   router.put('/presentation', loginRequiredStrictly, adminRequired, csrf, validator.presentationSetting, ApiV3FormValidator, async(req, res) => {
     if (req.body.pageBreakSeparator === 3 && req.body.pageBreakCustomSeparator === '') {
@@ -235,7 +235,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  properties:
    *                    status:
-   *                      $ref: '#/components/schemas/xssParams'
+   *                      $ref: '#/components/schemas/XssParams'
    */
   router.put('/xss', loginRequiredStrictly, adminRequired, csrf, validator.xssSetting, ApiV3FormValidator, async(req, res) => {
     if (req.body.isEnabledXss && req.body.xssOption == null) {

+ 7 - 10
src/server/routes/page.js

@@ -72,9 +72,6 @@ module.exports = function(crowi, app) {
   }
 
   function generatePager(offset, limit, totalCount) {
-    let next = null;
-
-
     let prev = null;
 
     if (offset > 0) {
@@ -84,12 +81,10 @@ module.exports = function(crowi, app) {
       }
     }
 
-    if (totalCount < limit) {
+    let next = offset + limit;
+    if (totalCount < next) {
       next = null;
     }
-    else {
-      next = offset + limit;
-    }
 
     return {
       prev,
@@ -164,7 +159,7 @@ module.exports = function(crowi, app) {
 
     const queryOptions = {
       offset,
-      limit: limit + 1,
+      limit,
       includeTrashed: path.startsWith('/trash/'),
       isRegExpEscapedFromPath,
     };
@@ -469,13 +464,15 @@ module.exports = function(crowi, app) {
   };
 
   actions.deletedPageListShow = async function(req, res) {
-    const path = `/trash${getPathFromRequest(req)}`;
+    // normalizePath makes '/trash/' -> '/trash'
+    const path = pathUtils.normalizePath(`/trash${getPathFromRequest(req)}`);
+
     const limit = 50;
     const offset = parseInt(req.query.offset) || 0;
 
     const queryOptions = {
       offset,
-      limit: limit + 1,
+      limit,
       includeTrashed: true,
     };
 

+ 2 - 2
src/server/service/global-notification/global-notification-mail.js

@@ -11,7 +11,6 @@ class GlobalNotificationMailService {
     this.mailer = crowi.getMailer();
     this.type = crowi.model('GlobalNotificationSetting').TYPE.MAIL;
     this.event = crowi.model('GlobalNotificationSetting').EVENT;
-    this.defaultLang = crowi.configManager.getConfig('crowi', 'app:globalLang');
   }
 
   /**
@@ -48,12 +47,13 @@ class GlobalNotificationMailService {
    * @return  {{ subject: string, template: string, vars: object }}
    */
   generateOption(event, path, triggeredBy, { comment, oldPath }) {
+    const defaultLang = this.crowi.configManager.getConfig('crowi', 'app:globalLang');
     // validate for all events
     if (event == null || path == null || triggeredBy == null) {
       throw new Error(`invalid vars supplied to GlobalNotificationMailService.generateOption for event ${event}`);
     }
 
-    const template = nodePath.join(this.crowi.localeDir, `${this.defaultLang}/notifications/${event}.txt`);
+    const template = nodePath.join(this.crowi.localeDir, `${defaultLang}/notifications/${event}.txt`);
     let subject;
     let vars = {
       appTitle: this.crowi.appService.getAppTitle(),

+ 1 - 1
src/server/util/swigFunctions.js

@@ -156,7 +156,7 @@ module.exports = function(crowi, req, locals) {
 
   locals.isTrashPage = function() {
     const path = req.path || '';
-    if (path.match(/^\/trash\/.*/)) {
+    if (path.match(/^\/trash(\/.*)?$/)) {
       return true;
     }