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

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

WESEEK Kaito 6 лет назад
Родитель
Сommit
7787a5e89f

+ 8 - 0
CHANGES.md

@@ -14,10 +14,18 @@ Upgrading Guide: https://docs.growi.org/en/admin-guide/upgrading/36x.html
 ### Updates
 
 * Improvement: Drop unnecessary MongoDB collection indexes
+* Fix: Appending tag is failed by wrong index of PageTagRelation
+    * Introduced by 3.5.20
 * Support: Support Node.js v12
 * Support: Upgrade libs
     * growi-commons
 
+
+
+## 3.5.23
+
+* Fix: Healthcheck API with `?connectToMiddlewares` returns error
+
 ## 3.5.22
 
 * Improvement: Add `FILE_UPLOAD_DISABLED` env var

+ 1 - 1
README.md

@@ -130,7 +130,7 @@ Environment Variables
         * `mongodb` : MongoDB GridFS (Setting-less)
         * `local` : Server's Local file system (Setting-less)
         * `none` : Disable file uploading
-    * FILE_UPLOAD_DISABLED: If `true`, disabled file upload. However can view files. default: `false`
+    * FILE_UPLOAD_DISABLED: If `true`, file uploading will be disabled. However, the files can be still viewed. Default: `false`
     * MAX_FILE_SIZE: The maximum file size limit for uploads (bytes). default: `Infinity`
     * FILE_UPLOAD_TOTAL_LIMIT: Total capacity limit for uploads (bytes). default: `Infinity`
     * GCS_API_KEY_JSON_PATH: Path of the JSON file that contains [service account key to authenticate to GCP API](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)

+ 13 - 12
resource/locales/en-US/translation.json

@@ -465,14 +465,14 @@
     "anyone": "Anyone",
 
 		"Authentication mechanism settings": "Authentication Mechanism Settings",
-    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
+    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting_link":"Go to Markdown settings",
     "callback_URL": "Callback URL",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
     "scope": "Scope",
-    "desc_of_callback_URL": "Use it in the setting of the %s provider",
+    "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
     "guest_mode": {
@@ -531,12 +531,13 @@
     },
     "SAML": {
       "name": "SAML",
+      "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",
       "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>%s</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>%s</code> ."
+      "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> ."
     },
     "Basic": {
       "name": "Basic Authentication",
@@ -589,14 +590,14 @@
       }
     },
     "form_item_name": {
-      "security:passport-saml:entryPoint": "Entry point",
-      "security:passport-saml:issuer": "Issuer",
-      "security:passport-saml:cert": "Certificate",
-      "security:passport-saml:attrMapId": "ID",
-      "security:passport-saml:attrMapUsername": "Username",
-      "security:passport-saml:attrMapMail": "Mail Address",
-      "security:passport-saml:attrMapFirstName": "First Name",
-      "security:passport-saml:attrMapLastName": "Last Name"
+      "entryPoint": "Entry point",
+      "issuer": "Issuer",
+      "cert": "Certificate",
+      "attrMapId": "ID",
+      "attrMapUsername": "Username",
+      "attrMapMail": "Mail Address",
+      "attrMapFirstName": "First Name",
+      "attrMapLastName": "Last Name"
     }
 	},
 

+ 13 - 12
resource/locales/ja/translation.json

@@ -463,11 +463,11 @@
     "anyone": "誰でも可能",
 
     "Authentication mechanism settings":"認証機構設定",
-    "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。%s から設定してください。",
+    "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link":"マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
-    "desc_of_callback_URL": "%s プロバイダ側の設定で利用してください。",
+    "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
     "guest_mode": {
@@ -526,12 +526,13 @@
     },
     "SAML": {
       "name": "SAML",
+      "enable_saml": "SAML を有効にする",
       "id_detail": "SAML Identity プロバイダ内で一意に識別可能な値を格納している属性",
       "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
       "mapping_detail": "新規ユーザーの%sに関連付ける属性",
       "cert_detail": "IdP からのレスポンスの validation を行うためのPEMエンコードされた X.509 証明書",
-      "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>%s</code> の値を利用します",
-      "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>%s</code> の値をfalseに変更もしくは削除してください"
+      "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します",
+      "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
     },
     "Basic": {
       "name": "Basic 認証",
@@ -573,14 +574,14 @@
       }
     },
     "form_item_name": {
-      "security:passport-saml:entryPoint": "エントリーポイント",
-      "security:passport-saml:issuer": "発行者",
-      "security:passport-saml:cert": "証明書",
-      "security:passport-saml:attrMapId": "ID",
-      "security:passport-saml:attrMapUsername": "ユーザー名",
-      "security:passport-saml:attrMapMail": "メールアドレス",
-      "security:passport-saml:attrMapFirstName": "姓",
-      "security:passport-saml:attrMapLastName": "名"
+      "entryPoint": "エントリーポイント",
+      "issuer": "発行者",
+      "cert": "証明書",
+      "attrMapId": "ID",
+      "attrMapUsername": "ユーザー名",
+      "attrMapMail": "メールアドレス",
+      "attrMapFirstName": "姓",
+      "attrMapLastName": "名"
     }
   },
 

+ 3 - 1
src/client/js/app.jsx

@@ -60,6 +60,7 @@ import AdminLdapSecurityContainer from './services/AdminLdapSecurityContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import MarkDownSettingContainer from './services/MarkDownSettingContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
+import AdminSamlSecurityContainer from './services/AdminSamlSecurityContainer';
 
 const logger = loggerFactory('growi:app');
 
@@ -217,7 +218,8 @@ const adminSecuritySettingElem = document.getElementById('admin-security-setting
 if (adminSecuritySettingElem != null) {
   const adminGeneralSecurityContainer = new AdminGeneralSecurityContainer(appContainer);
   const adminLdapSecurityContainer = new AdminLdapSecurityContainer(appContainer);
-  const adminSecurityContainers = [adminGeneralSecurityContainer, adminLdapSecurityContainer];
+  const adminSamlSecurityContainer = new AdminSamlSecurityContainer(appContainer);
+  const adminSecurityContainers = [adminGeneralSecurityContainer, adminLdapSecurityContainer, adminSamlSecurityContainer];
   ReactDOM.render(
     <Provider inject={[injectableContainers, adminSecurityContainers]}>
       <I18nextProvider i18n={i18n}>

+ 6 - 4
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -1,3 +1,4 @@
+/* eslint-disable react/no-danger */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
@@ -20,9 +21,11 @@ class LocalSecuritySetting extends React.Component {
         </h2>
 
         {adminGeneralSecurityContainer.state.useOnlyEnvVarsForSomeOptions && (
-        <p className="alert alert-info">
-          { t('security_setting.Local.note for the only env option', 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS') }
-        </p>
+        <p
+          className="alert alert-info"
+          // eslint-disable-next-line max-len
+          dangerouslySetInnerHTML={{ __html: t('security_setting.Local.note for the only env option', { env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+        />
         )}
 
         <div className="row mb-5">
@@ -90,7 +93,6 @@ class LocalSecuritySetting extends React.Component {
               </div>
             </div>
             <div className="row mb-5">
-              {/* eslint-disable-next-line react/no-danger */}
               <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
               <div className="col-xs-6">
                 <div>

+ 217 - 0
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -0,0 +1,217 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+import AdminSamlSecurityContainer from '../../../services/AdminSamlSecurityContainer';
+
+
+class SamlSecurityManagement extends React.Component {
+
+  render() {
+    const { t, adminGeneralSecurityContainer, adminSamlSecurityContainer } = this.props;
+    const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
+
+    return (
+      <React.Fragment>
+
+        {useOnlyEnvVars && (
+        <p
+          className="alert alert-info"
+          dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+        />
+        )}
+
+        <div className="row mb-5">
+          <strong className="col-xs-3 text-right">{ t('security_setting.SAML.name') }</strong>
+          <div className="col-xs-6 text-left">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isSamlEnabled"
+                type="checkbox"
+                checked={adminGeneralSecurityContainer.state.isSamlEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
+              />
+              <label htmlFor="isSamlEnabled">
+                { t('security_setting.SAML.enable_saml') }
+              </label>
+            </div>
+          </div>
+        </div>
+
+        <div className="row mb-5">
+          <label className="col-xs-3 text-right">{ t('security_setting.callback_URL') }</label>
+          <div className="col-xs-6">
+            <input
+              className="form-control"
+              type="text"
+              value={adminSamlSecurityContainer.state.callbackUrl}
+              readOnly
+            />
+            <p className="help-block small">{ t('security_setting.desc_of_callback_URL', { AuthName: 'SAML Identity' }) }</p>
+            {!adminSamlSecurityContainer.state.appSiteUrl && (
+            <div className="alert alert-danger">
+              <i
+                className="icon-exclamation"
+                // eslint-disable-next-line max-len
+                dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+              />
+            </div>
+            )}
+          </div>
+        </div>
+
+        {adminGeneralSecurityContainer.state.isSamlEnabled && (
+          <React.Fragment>
+
+            {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
+            <div className="alert alert-danger">
+              { t('security_setting.missing mandatory configs') }
+              <ul>
+                {/* TODO GW-583 show li after fetch data */}
+                {/* <li>{ t('security_setting.form_item_name.key') }</li> */}
+              </ul>
+            </div>
+          )}
+
+
+            <h3 className="alert-anchor border-bottom">
+              Basic Settings
+            </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.entryPoint') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      name="samlDbEntryPoint"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbEntryPoint}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbEntryPoint(e.target.value)}
+                    />
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarEntryPoint}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.issuer') }</th>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      name="samlEnvVarissuer"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbIssuer}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbIssuer(e.target.value)}
+                    />
+                  </td>
+                  <td>
+                    <input
+                      className="form-control"
+                      type="text"
+                      value={adminSamlSecurityContainer.state.samlEnvVarIssuer}
+                      readOnly
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
+                    </p>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{ t('security_setting.form_item_name.cert') }</th>
+                  <td>
+                    <textarea
+                      className="form-control input-sm"
+                      type="text"
+                      rows="5"
+                      name="samlDbCert"
+                      readOnly={useOnlyEnvVars}
+                      value={adminSamlSecurityContainer.state.samlDbcert}
+                      onChange={e => adminSamlSecurityContainer.changeSamlDbCert(e.target.value)}
+                    />
+                    <p className="help-block">
+                      <small>
+                        { t('security_setting.SAML.cert_detail') }
+                      </small>
+                    </p>
+                    <div>
+                      <small>
+                      e.g.
+                        <pre>{`-----BEGIN CERTIFICATE-----
+MIICBzCCAXACCQD4US7+0A/b/zANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJK
+UDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAoMDFdFU0VFSywgSW5jLjESMBAGA1UE
+...
+crmVwBzbloUO2l6k1ibwD2WVwpdxMKIF5z58HfKAvxZAzCHE7kMEZr1ge30WRXQA
+pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
+-----END CERTIFICATE-----
+                        `}
+                        </pre>
+                      </small>
+                    </div>
+                  </td>
+                  <td>
+                    <textarea
+                      className="form-control input-sm"
+                      type="text"
+                      rows="5"
+                      readOnly
+                      value={adminSamlSecurityContainer.state.samlEnvVarCert}
+                    />
+                    <p className="help-block">
+                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            {/* TODO GW-635 Attribute Mapping */}
+
+          </React.Fragment>
+
+        )}
+
+
+      </React.Fragment>
+    );
+
+  }
+
+}
+
+SamlSecurityManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+  adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
+};
+
+const SamlSecurityManagementWrapper = (props) => {
+  return createSubscribedElement(SamlSecurityManagement, props, [AppContainer, AdminGeneralSecurityContainer, AdminSamlSecurityContainer]);
+};
+
+export default withTranslation()(SamlSecurityManagementWrapper);

+ 2 - 1
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -7,6 +7,7 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LocalSecuritySetting from './LocalSecuritySetting';
+import SamlSecuritySetting from './SamlSecuritySetting';
 
 class SecurityManagement extends React.Component {
 
@@ -147,7 +148,7 @@ class SecurityManagement extends React.Component {
                 <LdapSecuritySetting />
               </div>
               <div id="passport-saml" className="tab-pane" role="tabpanel">
-                {/* TODO GW-544 reactify saml.html */}
+                <SamlSecuritySetting />
               </div>
               <div id="passport-oidc" className="tab-pane" role="tabpanel">
                 {/* TODO GW-545 reactify oidc.html */}

+ 9 - 3
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -23,6 +23,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       registrationMode: 'open',
       registrationWhiteList: '',
       isLdapEnabled: true,
+      isSamlEnabled: true,
     };
 
     this.init();
@@ -57,13 +58,18 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ registrationMode: value });
   }
 
-  // LDAP function
-
   /**
-   * Switch local enabled
+   * Switch LDAP enabled
    */
   switchIsLdapEnabled() {
     this.setState({ isLdapEnabled: !this.state.isLdapEnabled });
   }
 
+  /**
+   * Switch SAML enabled
+   */
+  switchIsSamlEnabled() {
+    this.setState({ isSamlEnabled: !this.state.isSamlEnabled });
+  }
+
 }

+ 70 - 0
src/client/js/services/AdminSamlSecurityContainer.js

@@ -0,0 +1,70 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:security:AdminSamlSecurityContainer');
+
+/**
+ * Service container for admin security page (SecuritySamlSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminSamlSecurityContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      // TODO GW-583 set value
+      useOnlyEnvVars: false,
+      appSiteUrl: false,
+      callbackUrl: 'hoge.com',
+      missingMandatoryConfigKeys: [],
+      samlDbEntryPoint: '',
+      samlEnvVarEntryPoint: '',
+      samlDbIssuer: '',
+      samlEnvVarIssuer: '',
+      samlDbCert: '',
+      samlEnvVarCert: '',
+    };
+
+    this.init();
+
+  }
+
+  init() {
+    // TODO GW-583 fetch config value with api
+  }
+
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminSamlSecurityContainer';
+  }
+
+  /**
+   * Change saml db entry point
+   */
+  changeSamlDbEntryPoint(inputValue) {
+    this.setState({ samlDbEntryPoint: inputValue });
+  }
+
+  /**
+   * Change saml db issuer
+   */
+  changeSamlDbIssuer(inputValue) {
+    this.setState({ samlDbIssuer: inputValue });
+  }
+
+  /**
+   * Change saml db Cert
+   */
+  changeSamlDbCert(inputValue) {
+    this.setState({ samlDbCert: inputValue });
+  }
+
+}

+ 33 - 0
src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js

@@ -0,0 +1,33 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:drop-wrong-index-of-page-tag-relation');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+async function dropIndexIfExists(db, collectionName, indexName) {
+  // check existence of the collection
+  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  if (items.length === 0) {
+    return;
+  }
+
+  const collection = await db.collection(collectionName);
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 1 - 1
src/server/models/page-tag-relation.js

@@ -27,7 +27,7 @@ const schema = new mongoose.Schema({
   },
 });
 // define unique compound index
-schema.index({ page: 1, user: 1 }, { unique: true });
+schema.index({ relatedPage: 1, relatedTag: 1 }, { unique: true });
 schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);
 

+ 5 - 6
src/server/routes/apiv3/healthcheck.js

@@ -25,7 +25,7 @@ module.exports = (crowi) => {
    *      parameters:
    *        - name: connectToMiddlewares
    *          in: query
-   *          description: Check also MongoDB and Elasticsearch
+   *          description: Check also MongoDB and SearchService
    *          schema:
    *            type: boolean
    *      responses:
@@ -38,14 +38,13 @@ module.exports = (crowi) => {
    *                  mongo:
    *                    type: string
    *                    description: 'OK'
-   *                  esInfo:
+   *                  searchInfo:
    *                    type: object
-   *                    description: A result of `client.info()` of Elasticsearch Info API
    */
   router.get('/', helmet.noCache(), async(req, res) => {
     const connectToMiddlewares = req.query.connectToMiddlewares;
 
-    // return 200 w/o connecting to MongoDB and Elasticsearch
+    // return 200 w/o connecting to MongoDB and SearchService
     if (connectToMiddlewares == null) {
       res.status(200).send({ status: 'OK' });
       return;
@@ -57,9 +56,9 @@ module.exports = (crowi) => {
       await Config.findOne({});
       // connect to Elasticsearch
       const search = crowi.getSearcher();
-      const esInfo = await search.getInfo();
+      const searchInfo = await search.getInfo();
 
-      res.status(200).send({ mongo: 'OK', esInfo });
+      res.status(200).send({ mongo: 'OK', searchInfo });
     }
     catch (err) {
       res.status(503).send({ err });

+ 1 - 0
src/server/service/global-notification/global-notification-mail.js

@@ -11,6 +11,7 @@ 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');
   }
 
   /**

+ 24 - 13
src/server/service/search-delegator/elasticsearch.js

@@ -21,12 +21,8 @@ class ElasticsearchDelegator {
     this.configManager = configManager;
     this.searchEvent = searchEvent;
 
-    this.esNodeName = '-';
-    this.esNodeNames = [];
     this.esVersion = 'unknown';
-    this.esVersions = [];
-    this.esPlugin = [];
-    this.esPlugins = [];
+    this.esNodeInfos = {};
 
     this.client = null;
 
@@ -72,6 +68,13 @@ class ElasticsearchDelegator {
     this.indexName = indexName;
   }
 
+  getInfo() {
+    return {
+      esVersion: this.esVersion,
+      esNodeInfos: this.esNodeInfos,
+    };
+  }
+
   /**
    * return information object to connect to ES
    * @return {object} { host, httpAuth, indexName}
@@ -158,18 +161,26 @@ class ElasticsearchDelegator {
    */
   async checkESVersion() {
     try {
-      const nodes = await this.client.nodes.info();
-      if (!nodes._nodes || !nodes.nodes) {
+      const info = await this.client.nodes.info();
+      if (!info._nodes || !info.nodes) {
         throw new Error('no nodes info');
       }
 
-      for (const [nodeName, nodeInfo] of Object.entries(nodes.nodes)) {
-        this.esNodeName = nodeName;
-        this.esNodeNames.push(nodeName);
+      for (const [nodeName, nodeInfo] of Object.entries(info.nodes)) {
         this.esVersion = nodeInfo.version;
-        this.esVersions.push(nodeInfo.version);
-        this.esPlugin = nodeInfo.plugins;
-        this.esPlugins.push(nodeInfo.plugins);
+
+        const filteredInfo = {
+          name: nodeInfo.name,
+          version: nodeInfo.version,
+          plugins: nodeInfo.plugins.map((pluginInfo) => {
+            return {
+              name: pluginInfo.name,
+              version: pluginInfo.version,
+            };
+          }),
+        };
+
+        this.esNodeInfos[nodeName] = filteredInfo;
       }
     }
     catch (error) {

+ 1 - 1
src/server/service/search.js

@@ -65,7 +65,7 @@ class SearchService {
   }
 
   getInfo() {
-    return this.delegator.client.info({});
+    return this.delegator.getInfo();
   }
 
   async buildIndex() {