Преглед изворни кода

Merge pull request #2522 from weseek/master

release v4.0.9
Yuki Takei пре 5 година
родитељ
комит
4f93723811

+ 13 - 1
CHANGES.md

@@ -1,6 +1,18 @@
 # CHANGES
 
-## v4.0.8-RC
+## v4.0.9-RC
+
+* Feature: Detailed configurations for OpenID Connect
+    * Authorization Endpoint
+    * Token Endpoint
+    * Revocation Endpoint
+    * Introspection Endpoint
+    * UserInfo Endpoint
+    * Registration Endpoint
+    * JSON Web Key Set URI
+
+
+## v4.0.8
 
 * Improvement: Navigations
     * New floating subnavigation

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.0.8-RC",
+  "version": "4.0.9-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 1
resource/locales/en_US/sandbox-math.md

@@ -4,7 +4,7 @@ See [MathJax](https://www.mathjax.org/).
 
 ## Inline Formula
 
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
   $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
 
 ## The Lorenz Equations

+ 10 - 1
resource/locales/en_US/translation.json

@@ -450,6 +450,14 @@
     "issuerHost": "Issuer Host",
     "scope": "Scope",
     "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
+    "authorization_endpoint": "Authorization Endpoint",
+    "token_endpoint": "Token Endpoint",
+    "revocation_endpoint": "Revocation Endpoint",
+    "introspection_endpoint": "Introspection Endpoint",
+    "userinfo_endpoint": "UserInfo Endpoint",
+    "end_session_endpoint": "EndSessioin Endpoint",
+    "registration_endpoint": "Registration Endpoint",
+    "jwks_uri": "JSON Web Key Set URL",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
     "updated_general_security_setting": "Succeeded to update security setting",
@@ -575,7 +583,8 @@
         "register_1": "Contant to OIDC IdP Administrator",
         "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
         "register_3": "Copy and paste your ClientID and Client Secret above",
-        "updated_oidc": "Succeeded to update OpenID Connect"
+        "updated_oidc": "Succeeded to update OpenID Connect",
+        "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
       },
       "how_to": {
         "google": "How to configure Google OAuth?",

+ 1 - 1
resource/locales/ja_JP/sandbox-math.md

@@ -4,7 +4,7 @@ See [MathJax](https://www.mathjax.org/).
 
 ## Inline Formula
 
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
   $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
 
 ## The Lorenz Equations

+ 10 - 1
resource/locales/ja_JP/translation.json

@@ -447,6 +447,14 @@
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
+    "authorization_endpoint": "認可エンドポイント",
+    "token_endpoint": "トークンエンドポイント",
+    "revocation_endpoint": "失効エンドポイント",
+    "introspection_endpoint": "検証エンドポイント",
+    "userinfo_endpoint": "ユーザ情報エンドポイント",
+    "end_session_endpoint": "セッション終了エンドポイント",
+    "registration_endpoint": "登録エンドポイント",
+    "jwks_uri": "JSON Web Key Set URL",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
     "updated_general_security_setting": "セキュリティ設定を更新しました。",
@@ -568,7 +576,8 @@
         "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
-        "updated_oidc": "OpenID Connect を更新しました"
+        "updated_oidc": "OpenID Connect を更新しました",
+        "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"
       },
       "how_to": {
         "google": "Google OAuth の設定方法",

+ 1 - 1
resource/locales/zh_CN/sandbox-math.md

@@ -4,7 +4,7 @@ See [MathJax](https://www.mathjax.org/).
 
 ## Inline Formula
 
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
   $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
 
 ## The Lorenz Equations

+ 10 - 1
resource/locales/zh_CN/translation.json

@@ -440,6 +440,14 @@
 		"issuerHost": "发行者主机",
 		"scope": "Scope",
 		"desc_of_callback_URL": "在{{AuthName}}身份提供程序的设置中使用它",
+    "authorization_endpoint": "Authorization Endpoint",
+    "token_endpoint": "Token Endpoint",
+    "revocation_endpoint": "Revocation Endpoint",
+    "introspection_endpoint": "Introspection Endpoint",
+    "userinfo_endpoint": "UserInfo Endpoint",
+    "end_session_endpoint": "EndSessioin Endpoint",
+    "registration_endpoint": "Registration Endpoint",
+    "jwks_uri": "JSON Web Key Set URL",
 		"clientID": "Client ID",
 		"client_secret": "客户机密",
 		"updated_general_security_setting": "更新安全设置成功",
@@ -565,7 +573,8 @@
 				"register_1": "Contant to OIDC IdP Administrator",
 				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
-				"updated_oidc": "Succeeded to update OpenID Connect"
+				"updated_oidc": "Succeeded to update OpenID Connect",
+        "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
 			},
 			"how_to": {
 				"google": "How to configure Google OAuth?",

+ 140 - 0
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -170,6 +170,146 @@ class OidcSecurityManagement extends React.Component {
               </div>
             </div>
 
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcAuthorizationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.authorization_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcAuthorizationEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcAuthorizationEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcAuthorizationEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcTokenEndpoint" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.token_endpoint')}</label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcTokenEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcTokenEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcTokenEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcRevocationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.revocation_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcRevocationEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcRevocationEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcRevocationEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcIntrospectionEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.introspection_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcIntrospectionEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcIntrospectionEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcIntrospectionEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcUserInfoEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.userinfo_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcUserInfoEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcUserInfoEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcUserInfoEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcEndSessionEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.end_session_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcEndSessionEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcEndSessionEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcEndSessionEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcRegistrationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.registration_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcRegistrationEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcRegistrationEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcRegistrationEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcJWKSUri" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.jwks_uri')}</label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcJWKSUri"
+                  defaultValue={adminOidcSecurityContainer.state.oidcJWKSUri || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcJWKSUri(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
             <h3 className="alert-anchor border-bottom">
               Attribute Mapping ({t('security_setting.optional')})
             </h3>

+ 1 - 1
src/client/js/components/Navbar/DrawerToggler.jsx

@@ -12,7 +12,7 @@ const DrawerToggler = (props) => {
 
   const clickHandler = useCallback(() => {
     navigationContainer.toggleDrawer();
-  }, []);
+  }, [navigationContainer]);
 
   const iconClass = props.iconClass || 'icon-menu';
 

+ 1 - 1
src/client/js/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -46,7 +46,7 @@ const GrowiSubNavigationSwitcher = (props) => {
     return () => {
       window.removeEventListener('resize', resizeHandler);
     };
-  }, []);
+  }, [resetWidth]);
 
   const stickyChangeHandler = useCallback((event) => {
     logger.debug('StickyEvents.CHANGE detected');

+ 106 - 2
src/client/js/services/AdminOidcSecurityContainer.js

@@ -23,6 +23,14 @@ export default class AdminOidcSecurityContainer extends Container {
       callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/oidc/callback'),
       oidcProviderName: '',
       oidcIssuerHost: '',
+      oidcAuthorizationEndpoint: '',
+      oidcTokenEndpoint: '',
+      oidcRevocationEndpoint: '',
+      oidcIntrospectionEndpoint: '',
+      oidcUserInfoEndpoint: '',
+      oidcEndSessionEndpoint: '',
+      oidcRegistrationEndpoint: '',
+      oidcJWKSUri: '',
       oidcClientId: '',
       oidcClientSecret: '',
       oidcAttrMapId: '',
@@ -45,6 +53,14 @@ export default class AdminOidcSecurityContainer extends Container {
       this.setState({
         oidcProviderName: oidcAuth.oidcProviderName,
         oidcIssuerHost: oidcAuth.oidcIssuerHost,
+        oidcAuthorizationEndpoint: oidcAuth.oidcAuthorizationEndpoint,
+        oidcTokenEndpoint: oidcAuth.oidcTokenEndpoint,
+        oidcRevocationEndpoint: oidcAuth.oidcRevocationEndpoint,
+        oidcIntrospectionEndpoint: oidcAuth.oidcIntrospectionEndpoint,
+        oidcUserInfoEndpoint: oidcAuth.oidcUserInfoEndpoint,
+        oidcEndSessionEndpoint: oidcAuth.oidcEndSessionEndpoint,
+        oidcRegistrationEndpoint: oidcAuth.oidcRegistrationEndpoint,
+        oidcJWKSUri: oidcAuth.oidcJWKSUri,
         oidcClientId: oidcAuth.oidcClientId,
         oidcClientSecret: oidcAuth.oidcClientSecret,
         oidcAttrMapId: oidcAuth.oidcAttrMapId,
@@ -83,6 +99,62 @@ export default class AdminOidcSecurityContainer extends Container {
     this.setState({ oidcIssuerHost: inputValue });
   }
 
+  /**
+   * Change oidcAuthorizationEndpoint
+   */
+  changeOidcAuthorizationEndpoint(inputValue) {
+    this.setState({ oidcAuthorizationEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcTokenEndpoint
+   */
+  changeOidcTokenEndpoint(inputValue) {
+    this.setState({ oidcTokenEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcRevocationEndpoint
+   */
+  changeOidcRevocationEndpoint(inputValue) {
+    this.setState({ oidcRevocationEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcIntrospectionEndpoint
+   */
+  changeOidcIntrospectionEndpoint(inputValue) {
+    this.setState({ oidcIntrospectionEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcUserInfoEndpoint
+   */
+  changeOidcUserInfoEndpoint(inputValue) {
+    this.setState({ oidcUserInfoEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcEndSessionEndpoint
+   */
+  changeOidcEndSessionEndpoint(inputValue) {
+    this.setState({ oidcEndSessionEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcRegistrationEndpoint
+   */
+  changeOidcRegistrationEndpoint(inputValue) {
+    this.setState({ oidcRegistrationEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcJWKSUri
+   */
+  changeOidcJWKSUri(inputValue) {
+    this.setState({ oidcJWKSUri: inputValue });
+  }
+
   /**
    * Change oidcClientId
    */
@@ -144,13 +216,37 @@ export default class AdminOidcSecurityContainer extends Container {
    */
   async updateOidcSetting() {
     const {
-      oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret, oidcAttrMapId, oidcAttrMapUserName,
-      oidcAttrMapName, oidcAttrMapEmail, isSameUsernameTreatedAsIdenticalUser, isSameEmailTreatedAsIdenticalUser,
+      oidcProviderName,
+      oidcIssuerHost,
+      oidcAuthorizationEndpoint,
+      oidcTokenEndpoint,
+      oidcRevocationEndpoint,
+      oidcIntrospectionEndpoint,
+      oidcUserInfoEndpoint,
+      oidcEndSessionEndpoint,
+      oidcRegistrationEndpoint,
+      oidcJWKSUri,
+      oidcClientId,
+      oidcClientSecret,
+      oidcAttrMapId,
+      oidcAttrMapUserName,
+      oidcAttrMapName,
+      oidcAttrMapEmail,
+      isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser,
     } = this.state;
 
     let requestParams = {
       oidcProviderName,
       oidcIssuerHost,
+      oidcAuthorizationEndpoint,
+      oidcTokenEndpoint,
+      oidcRevocationEndpoint,
+      oidcIntrospectionEndpoint,
+      oidcUserInfoEndpoint,
+      oidcEndSessionEndpoint,
+      oidcRegistrationEndpoint,
+      oidcJWKSUri,
       oidcClientId,
       oidcClientSecret,
       oidcAttrMapId,
@@ -168,6 +264,14 @@ export default class AdminOidcSecurityContainer extends Container {
     this.setState({
       oidcProviderName: securitySettingParams.oidcProviderName,
       oidcIssuerHost: securitySettingParams.oidcIssuerHost,
+      oidcAuthorizationEndpoint: securitySettingParams.oidcAuthorizationEndpoint,
+      oidcTokenEndpoint: securitySettingParams.oidcTokenEndpoint,
+      oidcRevocationEndpoint: securitySettingParams.oidcRevocationEndpoint,
+      oidcIntrospectionEndpoint: securitySettingParams.oidcIntrospectionEndpoint,
+      oidcUserInfoEndpoint: securitySettingParams.oidcUserInfoEndpoint,
+      oidcEndSessionEndpoint: securitySettingParams.oidcEndSessionEndpoint,
+      oidcRegistrationEndpoint: securitySettingParams.oidcRegistrationEndpoint,
+      oidcJWKSUri: securitySettingParams.oidcJWKSUri,
       oidcClientId: securitySettingParams.oidcClientId,
       oidcClientSecret: securitySettingParams.oidcClientSecret,
       oidcAttrMapId: securitySettingParams.oidcAttrMapId,

+ 11 - 8
src/client/js/services/EditorContainer.js

@@ -57,16 +57,19 @@ export default class EditorContainer extends Container {
    * initialize state for page permission
    */
   initStateGrant() {
-    const elem = document.getElementById('save-page-controls');
+    const mainContent = document.getElementById('content-main');
 
-    if (elem) {
-      this.state.grant = +elem.dataset.grant;
+    if (mainContent == null) {
+      logger.debug('#content-main element is not exists');
+      return;
+    }
 
-      const grantGroupId = elem.dataset.grantGroup;
-      if (grantGroupId != null && grantGroupId.length > 0) {
-        this.state.grantGroupId = grantGroupId;
-        this.state.grantGroupName = elem.dataset.grantGroupName;
-      }
+    this.state.grant = +mainContent.getAttribute('data-page-grant');
+
+    const grantGroupId = mainContent.getAttribute('data-page-grant-group');
+    if (grantGroupId != null && grantGroupId.length > 0) {
+      this.state.grantGroupId = grantGroupId;
+      this.state.grantGroupName = mainContent.getAttribute('data-page-grant-group-name');
     }
   }
 

+ 56 - 0
src/server/routes/apiv3/security-setting.js

@@ -65,6 +65,14 @@ const validator = {
   oidcAuth: [
     body('oidcProviderName').if(value => value != null).isString(),
     body('oidcIssuerHost').if(value => value != null).isString(),
+    body('oidcAuthorizationEndpoint').if(value => value != null).isString(),
+    body('oidcTokenEndpoint').if(value => value != null).isString(),
+    body('oidcRevocationEndpoint').if(value => value != null).isString(),
+    body('oidcIntrospectionEndpoint').if(value => value != null).isString(),
+    body('oidcUserInfoEndpoint').if(value => value != null).isString(),
+    body('oidcEndSessionEndpoint').if(value => value != null).isString(),
+    body('oidcRegistrationEndpoint').if(value => value != null).isString(),
+    body('oidcJWKSUri').if(value => value != null).isString(),
     body('oidcClientId').if(value => value != null).isString(),
     body('oidcClientSecret').if(value => value != null).isString(),
     body('oidcAttrMapId').if(value => value != null).isString(),
@@ -219,6 +227,30 @@ const validator = {
  *          oidcIssuerHost:
  *            type: string
  *            description: issuer host for oidc
+ *          oidcAuthorizationEndpoint:
+ *            type: string
+ *            description: authorization endpoint for oidc
+ *          oidcTokenEndpoint:
+ *            type: string
+ *            description: token endpoint for oidc
+ *          oidcRevocationEndpoint:
+ *            type: string
+ *            description: revocation endpoint for oidc
+ *          oidcIntrospectionEndpoint:
+ *            type: string
+ *            description: introspection endpoint for oidc
+ *          oidcUserInfoEndpoint:
+ *            type: string
+ *            description: userinfo endpoint for oidc
+ *          oidcEndSessionEndpoint:
+ *            type: string
+ *            description: end session endpoint for oidc
+ *          oidcRegistrationEndpoint:
+ *            type: string
+ *            description: registration endpoint for oidc
+ *          oidcJWKSUri:
+ *            type: string
+ *            description: JSON Web Key Set URI for oidc
  *          oidcClientId:
  *            type: string
  *            description: client id for oidc
@@ -376,6 +408,14 @@ module.exports = (crowi) => {
       oidcAuth: {
         oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
         oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        oidcAuthorizationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
+        oidcTokenEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
+        oidcRevocationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
+        oidcIntrospectionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
+        oidcUserInfoEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
+        oidcEndSessionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
+        oidcRegistrationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
+        oidcJWKSUri: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
         oidcClientId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
         oidcClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
         oidcAttrMapId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),
@@ -767,6 +807,14 @@ module.exports = (crowi) => {
     const requestParams = {
       'security:passport-oidc:providerName': req.body.oidcProviderName,
       'security:passport-oidc:issuerHost': req.body.oidcIssuerHost,
+      'security:passport-oidc:authorizationEndpoint': req.body.oidcAuthorizationEndpoint,
+      'security:passport-oidc:tokenEndpoint': req.body.oidcTokenEndpoint,
+      'security:passport-oidc:revocationEndpoint': req.body.oidcRevocationEndpoint,
+      'security:passport-oidc:introspectionEndpoint': req.body.oidcIntrospectionEndpoint,
+      'security:passport-oidc:userInfoEndpoint': req.body.oidcUserInfoEndpoint,
+      'security:passport-oidc:endSessionEndpoint': req.body.oidcEndSessionEndpoint,
+      'security:passport-oidc:registrationEndpoint': req.body.oidcRegistrationEndpoint,
+      'security:passport-oidc:jwksUri': req.body.oidcJWKSUri,
       'security:passport-oidc:clientId': req.body.oidcClientId,
       'security:passport-oidc:clientSecret': req.body.oidcClientSecret,
       'security:passport-oidc:attrMapId': req.body.oidcAttrMapId,
@@ -783,6 +831,14 @@ module.exports = (crowi) => {
       const securitySettingParams = {
         oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
         oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        oidcAuthorizationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
+        oidcTokenEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
+        oidcRevocationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
+        oidcIntrospectionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
+        oidcUserInfoEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
+        oidcEndSessionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
+        oidcRegistrationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
+        oidcJWKSUri: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
         oidcClientId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
         oidcClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
         oidcAttrMapId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),

+ 4 - 0
src/server/routes/attachment.js

@@ -193,6 +193,10 @@ module.exports = function(crowi, app) {
       return res.sendStatus(304);
     }
 
+    if (fileUploader.canRespond()) {
+      return fileUploader.respond(res, attachment);
+    }
+
     let fileStream;
     try {
       fileStream = await fileUploader.findDeliveryFile(attachment);

+ 12 - 0
src/server/service/config-loader.js

@@ -137,6 +137,18 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.BOOLEAN,
     default: false,
   },
+  FILE_UPLOAD_LOCAL_USE_INTERNAL_REDIRECT: {
+    ns:      'crowi',
+    key:     'fileUpload:local:useInternalRedirect',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
+  FILE_UPLOAD_LOCAL_INTERNAL_REDIRECT_PATH: {
+    ns:      'crowi',
+    key:     'fileUpload:local:internalRedirectPath',
+    type:    TYPES.STRING,
+    default: '/growi-internal/',
+  },
   ELASTICSEARCH_URI: {
     ns:      'crowi',
     key:     'app:elasticsearchUri',

+ 25 - 0
src/server/service/file-uploader/local.js

@@ -4,6 +4,7 @@ const fs = require('fs');
 const path = require('path');
 const mkdir = require('mkdirp');
 const streamToPromise = require('stream-to-promise');
+const urljoin = require('url-join');
 
 module.exports = function(crowi) {
   const Uploader = require('./uploader');
@@ -92,5 +93,29 @@ module.exports = function(crowi) {
     return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
   };
 
+  /**
+   * Checks if Uploader can respond to the HTTP request.
+   */
+  lib.canRespond = () => {
+    // Check whether to use internal redirect of nginx or Apache.
+    return process.env.FILE_UPLOAD === 'local' && lib.configManager.getConfig('crowi', 'fileUpload:local:useInternalRedirect');
+  };
+
+  /**
+   * Respond to the HTTP request.
+   * @param {Response} res
+   * @param {Response} attachment
+   */
+  lib.respond = (res, attachment) => {
+    // Responce using internal redirect of nginx or Apache.
+    const storagePath = getFilePathOnStorage(attachment);
+    const relativePath = path.relative(crowi.publicDir, storagePath);
+    const internalPathRoot = lib.configManager.getConfig('crowi', 'fileUpload:local:internalRedirectPath');
+    const internalPath = urljoin(internalPathRoot, relativePath);
+    res.set('X-Accel-Redirect', internalPath);
+    res.set('X-Sendfile', storagePath);
+    return res.end();
+  };
+
   return lib;
 };

+ 16 - 0
src/server/service/file-uploader/uploader.js

@@ -54,6 +54,22 @@ class Uploader {
 
   }
 
+  /**
+   * Checks if Uploader can respond to the HTTP request.
+   */
+  canRespond() {
+    return false;
+  }
+
+  /**
+   * Respond to the HTTP request.
+   * @param {Response} res
+   * @param {Response} attachment
+   */
+  respond(res, attachment) {
+    throw new Error('Implement this');
+  }
+
 }
 
 module.exports = Uploader;

+ 34 - 0
src/server/service/passport.js

@@ -570,6 +570,40 @@ class PassportService {
     const oidcIssuer = await OIDCIssuer.discover(issuerHost);
     debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
 
+    const authorizationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint');
+    if (authorizationEndpoint) {
+      oidcIssuer.metadata.authorization_endpoint = authorizationEndpoint;
+    }
+    const tokenEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint');
+    if (tokenEndpoint) {
+      oidcIssuer.metadata.token_endpoint = tokenEndpoint;
+    }
+    const revocationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint');
+    if (revocationEndpoint) {
+      oidcIssuer.metadata.revocation_endpoint = revocationEndpoint;
+    }
+    const introspectionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint');
+    if (introspectionEndpoint) {
+      oidcIssuer.metadata.introspection_endpoint = introspectionEndpoint;
+    }
+    const userInfoEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint');
+    if (userInfoEndpoint) {
+      oidcIssuer.metadata.userinfo_endpoint = userInfoEndpoint;
+    }
+    const endSessionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint');
+    if (endSessionEndpoint) {
+      oidcIssuer.metadata.end_session_endpoint = endSessionEndpoint;
+    }
+    const registrationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint');
+    if (registrationEndpoint) {
+      oidcIssuer.metadata.registration_endpoint = registrationEndpoint;
+    }
+    const jwksUri = configManager.getConfig('crowi', 'security:passport-oidc:jwksUri');
+    if (jwksUri) {
+      oidcIssuer.metadata.jwks_uri = jwksUri;
+    }
+    debug('Configured issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
+
     const client = new oidcIssuer.Client({
       client_id: clientId,
       client_secret: clientSecret,

+ 0 - 15
src/server/views/_form.html

@@ -10,20 +10,5 @@
 {% endif %}
 
 <div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
-{#
-<div class="page-editor-footer d-flex flex-row align-items-center justify-content-between">
-
-  <div>
-    <div id="page-editor-options-selector" class="d-none d-md-block"></div>
-  </div>
-
-  <div id="save-page-controls"
-    data-grant="{{ grant }}"
-    data-grant-group="{{ grantedGroupId }}"
-    data-grant-group-name="{{ grantedGroupName }}">
-  </div>
-
-</div>
-#}
 
 <div class="file-module hidden"></div>

+ 3 - 0
src/server/views/widget/page_content.html

@@ -9,6 +9,9 @@
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
+  data-page-grant="{{ grant }}"
+  data-page-grant-group="{{ grantedGroupId }}"
+  data-page-grant-group-name="{{ grantedGroupName }}"
   data-page-is-liked="{% if user %}{{ page.isLiked(user) }}{% else %}false{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-forbidden="{% if forbidden %}true{% else %}false{% endif %}"