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

Merge pull request #4010 from weseek/feat/admin-disable-link-sharing

Feat/admin disable link sharing
Yuki Takei 4 лет назад
Родитель
Сommit
7011a45fcb

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

@@ -151,6 +151,7 @@
   "original_path":"Original path",
   "new_path":"New path",
   "duplicated_path":"duplicated_path",
+  "Link sharing is disabled": "Link sharing is disabled",
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
@@ -170,7 +171,8 @@
     "page_not_exist_alert": "This page does not exist. Please create a new page."
   },
   "custom_navigation": {
-    "no_page_list": "There are no pages under this page."
+    "no_page_list": "There are no pages under this page.",
+    "link_sharing_is_disabled": "Link sharing is disabled."
   },
   "installer": {
     "setup": "Setup",
@@ -444,6 +446,7 @@
     "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
     "issue_share_link": "Succeeded to issue new share link",
     "remove_share_link": "Succeeded to remove {{count}} share links",
+    "switch_disable_link_sharing_success": "Succeeded to update share link setting",
     "failed_to_reset_password":"Failed to reset password"
   },
   "template": {
@@ -587,6 +590,9 @@
       "restricted": "Restricted (Requires approval by administrators)",
       "closed": "Closed (Invitation Only)"
     },
+    "share_link_rights": "Share link rights",
+    "enable_link_sharing": "Enable link sharing",
+    "all_share_links": "All share links",
     "configuration": " Configuration",
     "optional": "Optional",
     "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",

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

@@ -154,6 +154,7 @@
   "original_path":"元のパス",
   "new_path":"新しいパス",
   "duplicated_path":"重複したパス",
+  "Link sharing is disabled": "リンクのシェアは無効化されています",
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -173,7 +174,8 @@
     "page_not_exist_alert": "このページは存在しません。新たに作成する必要があります。"
   },
   "custom_navigation": {
-    "no_page_list": "このページの配下にはページが存在しません。"
+    "no_page_list": "このページの配下にはページが存在しません。",
+    "link_sharing_is_disabled": "リンクのシェアは無効化されています"
   },
   "installer": {
     "setup": "セットアップ",
@@ -446,6 +448,7 @@
     "remove_share_link_success": "{{shareLinkId}}を削除しました",
     "issue_share_link": "共有リンクを作成しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
+    "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
     "failed_to_reset_password":"パスワードのリセットに失敗しました"
   },
   "template": {
@@ -586,6 +589,9 @@
       "restricted": "制限 (登録完了には管理者の承認が必要)",
       "closed": "非公開 (登録には管理者による招待が必要)"
     },
+    "share_link_rights": "シェアリンクの権限",
+    "enable_link_sharing": "リンクのシェアを許可",
+    "all_share_links": "全てのシェアリンク",
     "configuration": "設定",
     "optional": "オプション",
     "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",

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

@@ -160,6 +160,7 @@
   "original_path":"Original path",
   "new_path":"New path",
   "duplicated_path":"duplicated_path",
+  "Link sharing is disabled": "你不允许分享该链接",
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
@@ -171,7 +172,8 @@
     "page_not_exist_alert": "该页面不存在,请创建一个新页面"
   },
   "custom_navigation": {
-    "no_page_list": "There are no pages under this page."
+    "no_page_list": "There are no pages under this page.",
+    "link_sharing_is_disabled": "链接共享已被禁用"
   },
 	"installer": {
 		"setup": "安装",
@@ -421,6 +423,7 @@
 		"deactivate_user_success": "Succeeded to deactivate {{username}}",
 		"remove_user_success": "Succeeded to removing {{username}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} ",
+    "switch_disable_link_sharing_success": "成功更新分享链接设置",
     "failed_to_reset_password":"Failed to reset password"
   },
 	"template": {
@@ -575,6 +578,9 @@
 			"restricted": "受限(需要管理员批准)",
 			"closed": "已关闭(仅限邀请)"
 		},
+    "share_link_rights": "分享链接权",
+    "enable_link_sharing": "启用链接共享",
+    "all_share_links": "所有共享链接",
 		"configuration": " 配置",
 		"optional": "可选的",
 		"Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",

+ 34 - 1
src/client/js/components/Admin/Security/ShareLinkSetting.jsx

@@ -51,6 +51,7 @@ class ShareLinkSetting extends React.Component {
     this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
     this.deleteAllLinksButtonHandler = this.deleteAllLinksButtonHandler.bind(this);
     this.deleteLinkById = this.deleteLinkById.bind(this);
+    this.switchDisableLinkSharing = this.switchDisableLinkSharing.bind(this);
   }
 
   componentWillMount() {
@@ -105,11 +106,22 @@ class ShareLinkSetting extends React.Component {
     this.getShareLinkList(shareLinksActivePage);
   }
 
+  async switchDisableLinkSharing() {
+    const { t, adminGeneralSecurityContainer } = this.props;
+    try {
+      await adminGeneralSecurityContainer.switchDisableLinkSharing();
+      toastSuccess(t('toaster.switch_disable_link_sharing_success'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
 
   render() {
     const { t, adminGeneralSecurityContainer } = this.props;
     const {
-      shareLinks, shareLinksActivePage, totalshareLinks, shareLinksPagingLimit,
+      shareLinks, shareLinksActivePage, totalshareLinks, shareLinksPagingLimit, disableLinkSharing,
     } = adminGeneralSecurityContainer.state;
 
     return (
@@ -125,6 +137,27 @@ class ShareLinkSetting extends React.Component {
           </button>
           <h2 className="alert-anchor border-bottom">{t('share_links.share_link_management')}</h2>
         </div>
+        <h4>{t('security_setting.share_link_rights')}</h4>
+        <div className="row mb-5">
+          <div className="col-6 offset-3">
+            <div className="custom-control custom-switch custom-checkbox-success">
+              <input
+                type="checkbox"
+                className="custom-control-input"
+                id="disableLinkSharing"
+                checked={!disableLinkSharing}
+                onChange={() => this.switchDisableLinkSharing()}
+              />
+              <label className="custom-control-label" htmlFor="disableLinkSharing">
+                {t('security_setting.enable_link_sharing')}
+              </label>
+            </div>
+            {!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && disableLinkSharing && (
+              <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>
+            )}
+          </div>
+        </div>
+        <h4>{t('security_setting.all_share_links')}</h4>
         <Pager
           links={shareLinks}
           activePage={shareLinksActivePage}

+ 3 - 2
src/client/js/components/PageAccessoriesModal.jsx

@@ -26,6 +26,7 @@ const PageAccessoriesModal = (props) => {
   const {
     t, pageAccessoriesContainer, onClose, isGuestUser, isSharedUser, isNotFoundPage,
   } = props;
+  const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { activeTab, activeComponents } = pageAccessoriesContainer.state;
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
@@ -60,10 +61,10 @@ const PageAccessoriesModal = (props) => {
         Icon: ShareLinkIcon,
         i18n: t('share_links.share_link_management'),
         index: 4,
-        isLinkEnabled: v => !isGuestUser && !isSharedUser && !isNotFoundPage,
+        isLinkEnabled: v => !isGuestUser && !isSharedUser && !isNotFoundPage && !isLinkSharingDisabled,
       },
     };
-  }, [t, isGuestUser, isSharedUser, isNotFoundPage]);
+  }, [t, isGuestUser, isSharedUser, isNotFoundPage, isLinkSharingDisabled]);
 
   const closeModalHandler = useCallback(() => {
     if (onClose == null) {

+ 6 - 2
src/client/js/components/PageAccessoriesModalControl.jsx

@@ -19,6 +19,7 @@ const PageAccessoriesModalControl = (props) => {
   const {
     t, pageAccessoriesContainer, isGuestUser, isSharedUser, isNotFoundPage,
   } = props;
+  const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
 
   const accessoriesBtnList = useMemo(() => {
     return [
@@ -49,11 +50,11 @@ const PageAccessoriesModalControl = (props) => {
       {
         name: 'shareLink',
         Icon: <ShareLinkIcon />,
-        disabled: isGuestUser || isSharedUser || isNotFoundPage,
+        disabled: isGuestUser || isSharedUser || isNotFoundPage || isLinkSharingDisabled,
         i18n: t('share_links.share_link_management'),
       },
     ];
-  }, [t, isGuestUser, isSharedUser, isNotFoundPage]);
+  }, [t, isGuestUser, isSharedUser, isNotFoundPage, isLinkSharingDisabled]);
 
   return (
     <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
@@ -62,6 +63,9 @@ const PageAccessoriesModalControl = (props) => {
         let tooltipMessage;
         if (accessory.disabled) {
           tooltipMessage = isNotFoundPage ? t('not_found_page.page_not_exist') : t('Not available for guest');
+          if (accessory.name === 'shareLink' && isLinkSharingDisabled) {
+            tooltipMessage = t('Link sharing is disabled');
+          }
         }
         else {
           tooltipMessage = accessory.i18n;

+ 8 - 0
src/client/js/components/PageList.jsx

@@ -64,6 +64,14 @@ const PageList = (props) => {
       </div>
     );
   }
+  if (appContainer.config.disableLinkSharing) {
+    return (
+      <div className="mt-2">
+        {/* eslint-disable-next-line react/no-danger */}
+        <p>{t('custom_navigation.link_sharing_is_disabled')}</p>
+      </div>
+    );
+  }
 
   return (
     <div className="page-list">

+ 22 - 1
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -35,6 +35,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isGitHubEnabled: false,
       isTwitterEnabled: false,
       setupStrategies: [],
+      disableLinkSharing: false,
       shareLinks: [],
       totalshareLinks: 0,
       shareLinksPagingLimit: Infinity,
@@ -46,7 +47,7 @@ export default class AdminGeneralSecurityContainer extends Container {
   async retrieveSecurityData() {
     await this.retrieveSetupStratedies();
     const response = await this.appContainer.apiv3.get('/security-setting/');
-    const { generalSetting, generalAuth } = response.data.securityParams;
+    const { generalSetting, shareLinkSetting, generalAuth } = response.data.securityParams;
     this.setState({
       currentRestrictGuestMode: generalSetting.restrictGuestMode,
       currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
@@ -54,6 +55,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
+      disableLinkSharing: shareLinkSetting.disableLinkSharing,
       isLocalEnabled: generalAuth.isLocalEnabled,
       isLdapEnabled: generalAuth.isLdapEnabled,
       isSamlEnabled: generalAuth.isSamlEnabled,
@@ -88,6 +90,13 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ sessionMaxAge });
   }
 
+  /**
+   * setter for disableLinkSharing
+   */
+  setDisableLinkSharing(disableLinkSharing) {
+    this.setState({ disableLinkSharing });
+  }
+
   /**
    * Change restrictGuestMode
    */
@@ -137,6 +146,18 @@ export default class AdminGeneralSecurityContainer extends Container {
     return securitySettingParams;
   }
 
+  /**
+   * Switch disableLinkSharing
+   */
+  async switchDisableLinkSharing() {
+    const requestParams = {
+      disableLinkSharing: !this.state.disableLinkSharing,
+    };
+    const response = await this.appContainer.apiv3.put('/security-setting/share-link-setting', requestParams);
+    this.setDisableLinkSharing(!this.state.disableLinkSharing);
+    return response;
+  }
+
   /**
    * Switch authentication
    */

+ 2 - 0
src/server/models/config.js

@@ -47,6 +47,7 @@ module.exports = function(crowi) {
       'security:list-policy:hideRestrictedByOwner' : false,
       'security:list-policy:hideRestrictedByGroup' : false,
       'security:pageCompleteDeletionAuthority' : undefined,
+      'security:disableLinkSharing' : false,
 
       'security:passport-local:isEnabled' : true,
       'security:passport-ldap:isEnabled' : false,
@@ -193,6 +194,7 @@ module.exports = function(crowi) {
         file: crowi.fileUploadService.getFileUploadEnabled(),
       },
       registrationWhiteList: crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
+      disableLinkSharing: crowi.configManager.getConfig('crowi', 'security:disableLinkSharing'),
       themeType: crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),

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

@@ -22,6 +22,9 @@ const validator = {
     body('hideRestrictedByOwner').if(value => value != null).isBoolean(),
     body('hideRestrictedByGroup').if(value => value != null).isBoolean(),
   ],
+  shareLinkSetting: [
+    body('disableLinkSharing').if(value => value != null).isBoolean(),
+  ],
   authenticationSetting: [
     body('isEnabled').if(value => value != null).isBoolean(),
     body('authId').isString().isIn([
@@ -129,6 +132,12 @@ const validator = {
  *          hideRestrictedByGroup:
  *            type: boolean
  *            description: enable hide by group
+ *      ShareLinkSetting:
+ *        type: object
+ *        properties:
+ *          disableLinkSharing:
+ *            type: boolean
+ *            description: disable link sharing
  *      LocalSetting:
  *        type: object
  *        properties:
@@ -364,6 +373,9 @@ module.exports = (crowi) => {
         wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
         sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
       },
+      shareLinkSetting: {
+        disableLinkSharing: await crowi.configManager.getConfig('crowi', 'security:disableLinkSharing'),
+      },
       localSetting: {
         useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
         registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
@@ -589,6 +601,47 @@ module.exports = (crowi) => {
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
       };
+
+      return res.apiv3({ securitySettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating security setting';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-secuirty-setting failed'));
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/share-link-setting:
+   *      put:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Update ShareLink Setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/ShareLinkSetting'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update ShareLink Setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/ShareLinkSetting'
+   */
+  router.put('/share-link-setting', loginRequiredStrictly, adminRequired, csrf, validator.generalSetting, apiV3FormValidator, async(req, res) => {
+    const updateData = {
+      'security:disableLinkSharing': req.body.disableLinkSharing,
+    };
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', updateData);
+      const securitySettingParams = {
+        disableLinkSharing: crowi.configManager.getConfig('crowi', 'security:disableLinkSharing'),
+      };
+
       return res.apiv3({ securitySettingParams });
     }
     catch (err) {

+ 15 - 2
src/server/routes/apiv3/share-links.js

@@ -30,6 +30,19 @@ module.exports = (crowi) => {
   const ShareLink = crowi.model('ShareLink');
   const Page = crowi.model('Page');
 
+  /**
+   * middleware to limit link sharing
+   */
+  const linkSharingRequired = (req, res, next) => {
+    const isLinkSharingDisabled = crowi.configManager.getConfig('crowi', 'security:disableLinkSharing');
+    logger.debug(`isLinkSharingDisabled: ${isLinkSharingDisabled}`);
+
+    if (isLinkSharingDisabled) {
+      return res.apiv3Err(new ErrorV3('Link sharing is disabled', 'link-sharing-disabled'));
+    }
+    next();
+  };
+
   validator.getShareLinks = [
     // validate the page id is MongoId
     query('relatedPage').isMongoId().withMessage('Page Id is required'),
@@ -54,7 +67,7 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to get share links
    */
-  router.get('/', loginRequired, validator.getShareLinks, apiV3FormValidator, async(req, res) => {
+  router.get('/', loginRequired, linkSharingRequired, validator.getShareLinks, apiV3FormValidator, async(req, res) => {
     const { relatedPage } = req.query;
 
     const page = await Page.findByIdAndViewer(relatedPage, req.user);
@@ -115,7 +128,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to create one share link
    */
 
-  router.post('/', loginRequired, csrf, validator.shareLinkStatus, apiV3FormValidator, async(req, res) => {
+  router.post('/', loginRequired, linkSharingRequired, csrf, validator.shareLinkStatus, apiV3FormValidator, async(req, res) => {
     const { relatedPage, expiredAt, description } = req.body;
 
     const page = await Page.findByIdAndViewer(relatedPage, req.user);

+ 3 - 0
src/server/routes/page.js

@@ -413,6 +413,9 @@ module.exports = function(crowi, app) {
       // page or sharelink are not found
       return res.render('layout-growi/not_found_shared_page');
     }
+    if (crowi.configManager.getConfig('crowi', 'security:disableLinkSharing')) {
+      return res.render('layout-growi/forbidden');
+    }
 
     const renderVars = {};
 

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

@@ -254,6 +254,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.NUMBER,
     default: Infinity,
   },
+  DISABLE_LINK_SHARING: {
+    ns:      'crowi',
+    key:     'security:disableSharing',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
   LOCAL_STRATEGY_ENABLED: {
     ns:      'crowi',
     key:     'security:passport-local:isEnabled',