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

Merge pull request #8570 from weseek/imprv/145071-142310-disable-groups-not-assignable-to-page

imprv: Disable groups not assignable to page due to parent grant
Yuki Takei 2 лет назад
Родитель
Сommit
bcaecf3541

+ 2 - 2
apps/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -11,7 +11,7 @@ import { toastError, toastSuccess } from '~/client/util/toastr';
 import { UserGroupPageGrantStatus, type IPageGrantData } from '~/interfaces/page';
 import { UserGroupPageGrantStatus, type IPageGrantData } from '~/interfaces/page';
 import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
 import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
-import { useSWRxApplicableGrant, useSWRxIsGrantNormalized, useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxApplicableGrant, useSWRxCurrentGrantData, useSWRxCurrentPage } from '~/stores/page';
 
 
 type ModalProps = {
 type ModalProps = {
   isOpen: boolean
   isOpen: boolean
@@ -287,7 +287,7 @@ export const FixPageGrantAlert = (): JSX.Element => {
 
 
   const [isOpen, setOpen] = useState<boolean>(false);
   const [isOpen, setOpen] = useState<boolean>(false);
 
 
-  const { data: dataIsGrantNormalized } = useSWRxIsGrantNormalized(currentUser != null ? pageId : null);
+  const { data: dataIsGrantNormalized } = useSWRxCurrentGrantData(currentUser != null ? pageId : null);
   const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
   const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
 
 
   // Dependencies
   // Dependencies

+ 2 - 2
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -36,7 +36,7 @@ import {
   useWaitingSaveProcessing,
   useWaitingSaveProcessing,
 } from '~/stores/editor';
 } from '~/stores/editor';
 import {
 import {
-  useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId, useIsNotFound, useTemplateBodyData, useSWRxIsGrantNormalized,
+  useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId, useIsNotFound, useTemplateBodyData, useSWRxCurrentGrantData,
 } from '~/stores/page';
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import { mutatePageTree } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
@@ -104,7 +104,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: defaultIndentSize } = useDefaultIndentSize();
   const { data: defaultIndentSize } = useDefaultIndentSize();
   const { data: acceptedUploadFileType } = useAcceptedUploadFileType();
   const { data: acceptedUploadFileType } = useAcceptedUploadFileType();
   const { data: editorSettings } = useEditorSettings();
   const { data: editorSettings } = useEditorSettings();
-  const { mutate: mutateIsGrantNormalized } = useSWRxIsGrantNormalized(currentPage?._id);
+  const { mutate: mutateIsGrantNormalized } = useSWRxCurrentGrantData(currentPage?._id);
   const { data: user } = useCurrentUser();
   const { data: user } = useCurrentUser();
   const { onEditorsUpdated } = useEditingUsers();
   const { onEditorsUpdated } = useEditingUsers();
   const onConflict = useConflictResolver();
   const onConflict = useConflictResolver();

+ 3 - 3
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -15,7 +15,7 @@ import {
 import type { UserRelatedGroupsData } from '~/interfaces/page';
 import type { UserRelatedGroupsData } from '~/interfaces/page';
 import { UserGroupPageGrantStatus } from '~/interfaces/page';
 import { UserGroupPageGrantStatus } from '~/interfaces/page';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
-import { useCurrentPageId, useSWRxIsGrantNormalized } from '~/stores/page';
+import { useCurrentPageId, useSWRxCurrentGrantData } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import { useSelectedGrant } from '~/stores/ui';
 
 
 
 
@@ -64,7 +64,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
   const shouldFetch = isSelectGroupModalShown;
   const shouldFetch = isSelectGroupModalShown;
   const { data: selectedGrant, mutate: mutateSelectedGrant } = useSelectedGrant();
   const { data: selectedGrant, mutate: mutateSelectedGrant } = useSelectedGrant();
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
-  const { data: grantData } = useSWRxIsGrantNormalized(currentPageId);
+  const { data: grantData } = useSWRxCurrentGrantData(currentPageId);
 
 
   const currentPageGrantData = grantData?.grantData.currentPageGrant;
   const currentPageGrantData = grantData?.grantData.currentPageGrant;
   const groupGrantData = currentPageGrantData?.groupGrantData;
   const groupGrantData = currentPageGrantData?.groupGrantData;
@@ -253,7 +253,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
         { nonUserRelatedGrantedGroups.map((group) => {
         { nonUserRelatedGrantedGroups.map((group) => {
           return (
           return (
             <button
             <button
-              className="btn btn-outline-primary w-100 d-flex justify-content-start mb-3 align-items-center p-3 active"
+              className="btn btn-outline-primary d-flex justify-content-start mb-3 mx-4 align-items-center p-3 active"
               type="button"
               type="button"
               key={group.id}
               key={group.id}
               disabled
               disabled

+ 1 - 1
apps/app/src/server/models/user-group-relation.ts

@@ -1,5 +1,5 @@
 import {
 import {
-  getIdForRef, isPopulated, type IUserGroupHasId, type IUserGroupRelation,
+  getIdForRef, isPopulated, type IUserGroupRelation,
 } from '@growi/core';
 } from '@growi/core';
 import type { Model, Document } from 'mongoose';
 import type { Model, Document } from 'mongoose';
 import mongoose, { Schema } from 'mongoose';
 import mongoose, { Schema } from 'mongoose';

+ 5 - 5
apps/app/src/server/routes/apiv3/page/index.ts

@@ -539,12 +539,12 @@ module.exports = (crowi) => {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /page/is-grant-normalized:
+   *    /page/grant-data:
    *      get:
    *      get:
    *        tags: [Page]
    *        tags: [Page]
    *        summary: /page/info
    *        summary: /page/info
-   *        description: Retrieve current page's isGrantNormalized value
-   *        operationId: getIsGrantNormalized
+   *        description: Retrieve current page's grant data
+   *        operationId: getPageGrantData
    *        parameters:
    *        parameters:
    *          - name: pageId
    *          - name: pageId
    *            in: query
    *            in: query
@@ -553,7 +553,7 @@ module.exports = (crowi) => {
    *              $ref: '#/components/schemas/Page/properties/_id'
    *              $ref: '#/components/schemas/Page/properties/_id'
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Successfully retrieved current isGrantNormalized.
+   *            description: Successfully retrieved current grant data.
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
@@ -566,7 +566,7 @@ module.exports = (crowi) => {
    *          500:
    *          500:
    *            description: Internal server error.
    *            description: Internal server error.
    */
    */
-  router.get('/is-grant-normalized', loginRequiredStrictly, validator.isGrantNormalized, apiV3FormValidator, async(req, res) => {
+  router.get('/grant-data', loginRequiredStrictly, validator.isGrantNormalized, apiV3FormValidator, async(req, res) => {
     const { pageId } = req.query;
     const { pageId } = req.query;
 
 
     const Page = mongoose.model<IPage, PageModel>('Page');
     const Page = mongoose.model<IPage, PageModel>('Page');

+ 70 - 43
apps/app/src/server/service/page-grant.ts

@@ -12,6 +12,7 @@ import mongoose from 'mongoose';
 import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
 import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
 import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
 import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import type { UserRelatedGroupsData } from '~/interfaces/page';
 import { UserGroupPageGrantStatus, type GroupGrantData } from '~/interfaces/page';
 import { UserGroupPageGrantStatus, type GroupGrantData } from '~/interfaces/page';
 import type { IRecordApplicableGrant, PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import type { IRecordApplicableGrant, PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import type { PageDocument, PageModel } from '~/server/models/page';
@@ -117,7 +118,7 @@ class PageGrantService implements IPageGrantService {
   }
   }
 
 
   private validateComparableTarget(comparable: ComparableTarget) {
   private validateComparableTarget(comparable: ComparableTarget) {
-    const Page = mongoose.model('Page') as unknown as PageModel;
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
 
     const { grant, grantedUserIds, grantedGroupIds } = comparable;
     const { grant, grantedUserIds, grantedGroupIds } = comparable;
 
 
@@ -139,7 +140,7 @@ class PageGrantService implements IPageGrantService {
      */
      */
     this.validateComparableTarget(target);
     this.validateComparableTarget(target);
 
 
-    const Page = mongoose.model('Page') as unknown as PageModel;
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
 
     /*
     /*
      * ancestor side
      * ancestor side
@@ -289,53 +290,45 @@ class PageGrantService implements IPageGrantService {
    * Prepare ComparableTarget
    * Prepare ComparableTarget
    * @returns Promise<ComparableAncestor>
    * @returns Promise<ComparableAncestor>
    */
    */
-  private async generateComparableTarget(
-      grant: PageGrant | undefined, grantedUserIds: ObjectIdLike[] | undefined, grantedGroupIds: IGrantedGroup[] | undefined, includeApplicable: boolean,
+  private async generateComparableTargetWithApplicableData(
+      grant: PageGrant | undefined, grantedUserIds: ObjectIdLike[] | undefined, grantedGroupIds: IGrantedGroup[] | undefined,
   ): Promise<ComparableTarget> {
   ): Promise<ComparableTarget> {
-    if (includeApplicable) {
-      const Page = mongoose.model('Page') as unknown as PageModel;
-
-      let applicableUserIds: ObjectIdLike[] | undefined;
-      let applicableGroupIds: ObjectIdLike[] | undefined;
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
 
-      if (grant === Page.GRANT_USER_GROUP) {
-        if (grantedGroupIds == null || grantedGroupIds.length === 0) {
-          throw Error('Target user group is not given');
-        }
+    let applicableUserIds: ObjectIdLike[] | undefined;
+    let applicableGroupIds: ObjectIdLike[] | undefined;
 
 
-        const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantedGroupIds);
-        const targetUserGroups = await UserGroup.find({ _id: { $in: grantedUserGroupIds } });
-        const targetExternalUserGroups = await ExternalUserGroup.find({ _id: { $in: grantedExternalUserGroupIds } });
-        if (targetUserGroups.length === 0 && targetExternalUserGroups.length === 0) {
-          throw Error('Target user group does not exist');
-        }
+    if (grant === Page.GRANT_USER_GROUP) {
+      if (grantedGroupIds == null || grantedGroupIds.length === 0) {
+        throw Error('Target user group is not given');
+      }
 
 
-        const userGroupRelations = await UserGroupRelation.find({ relatedGroup: { $in: targetUserGroups.map(g => g._id) } });
-        const externalUserGroupRelations = await ExternalUserGroupRelation.find({ relatedGroup: { $in: targetExternalUserGroups.map(g => g._id) } });
-        applicableUserIds = Array.from(new Set([...userGroupRelations, ...externalUserGroupRelations].map(u => u.relatedUser as ObjectIdLike)));
-
-        const applicableUserGroups = (await Promise.all(targetUserGroups.map((group) => {
-          return UserGroup.findGroupsWithDescendantsById(group._id);
-        }))).flat();
-        const applicableExternalUserGroups = (await Promise.all(targetExternalUserGroups.map((group) => {
-          return ExternalUserGroup.findGroupsWithDescendantsById(group._id);
-        }))).flat();
-        applicableGroupIds = [...applicableUserGroups, ...applicableExternalUserGroups].map(g => g._id);
+      const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantedGroupIds);
+      const targetUserGroups = await UserGroup.find({ _id: { $in: grantedUserGroupIds } });
+      const targetExternalUserGroups = await ExternalUserGroup.find({ _id: { $in: grantedExternalUserGroupIds } });
+      if (targetUserGroups.length === 0 && targetExternalUserGroups.length === 0) {
+        throw Error('Target user group does not exist');
       }
       }
 
 
-      return {
-        grant,
-        grantedUserIds,
-        grantedGroupIds,
-        applicableUserIds,
-        applicableGroupIds,
-      };
+      const userGroupRelations = await UserGroupRelation.find({ relatedGroup: { $in: targetUserGroups.map(g => g._id) } });
+      const externalUserGroupRelations = await ExternalUserGroupRelation.find({ relatedGroup: { $in: targetExternalUserGroups.map(g => g._id) } });
+      applicableUserIds = Array.from(new Set([...userGroupRelations, ...externalUserGroupRelations].map(u => u.relatedUser as ObjectIdLike)));
+
+      const applicableUserGroups = (await Promise.all(targetUserGroups.map((group) => {
+        return UserGroup.findGroupsWithDescendantsById(group._id);
+      }))).flat();
+      const applicableExternalUserGroups = (await Promise.all(targetExternalUserGroups.map((group) => {
+        return ExternalUserGroup.findGroupsWithDescendantsById(group._id);
+      }))).flat();
+      applicableGroupIds = [...applicableUserGroups, ...applicableExternalUserGroups].map(g => g._id);
     }
     }
 
 
     return {
     return {
       grant,
       grant,
       grantedUserIds,
       grantedUserIds,
       grantedGroupIds,
       grantedGroupIds,
+      applicableUserIds,
+      applicableGroupIds,
     };
     };
   }
   }
 
 
@@ -345,7 +338,7 @@ class PageGrantService implements IPageGrantService {
    * @returns Promise<ComparableAncestor>
    * @returns Promise<ComparableAncestor>
    */
    */
   private async generateComparableAncestor(targetPath: string, includeNotMigratedPages: boolean): Promise<ComparableAncestor> {
   private async generateComparableAncestor(targetPath: string, includeNotMigratedPages: boolean): Promise<ComparableAncestor> {
-    const Page = mongoose.model('Page') as unknown as PageModel;
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const { PageQueryBuilder } = Page;
     const { PageQueryBuilder } = Page;
 
 
     let applicableUserIds: ObjectIdLike[] | undefined;
     let applicableUserIds: ObjectIdLike[] | undefined;
@@ -400,7 +393,7 @@ class PageGrantService implements IPageGrantService {
    * @returns ComparableDescendants
    * @returns ComparableDescendants
    */
    */
   private async generateComparableDescendants(targetPath: string, user, includeNotMigratedPages = false): Promise<ComparableDescendants> {
   private async generateComparableDescendants(targetPath: string, user, includeNotMigratedPages = false): Promise<ComparableDescendants> {
-    const Page = mongoose.model('Page') as unknown as PageModel;
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
 
     // Build conditions
     // Build conditions
     const $match: {$or: any} = {
     const $match: {$or: any} = {
@@ -520,11 +513,11 @@ class PageGrantService implements IPageGrantService {
     const comparableAncestor = await this.generateComparableAncestor(targetPath, includeNotMigratedPages);
     const comparableAncestor = await this.generateComparableAncestor(targetPath, includeNotMigratedPages);
 
 
     if (!shouldCheckDescendants) { // checking the parent is enough
     if (!shouldCheckDescendants) { // checking the parent is enough
-      const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupIds, false);
+      const comparableTarget: ComparableTarget = { grant, grantedUserIds, grantedGroupIds };
       return this.validateGrant(comparableTarget, comparableAncestor);
       return this.validateGrant(comparableTarget, comparableAncestor);
     }
     }
 
 
-    const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupIds, true);
+    const comparableTarget = await this.generateComparableTargetWithApplicableData(grant, grantedUserIds, grantedGroupIds);
     const comparableDescendants = await this.generateComparableDescendants(targetPath, user, includeNotMigratedPages);
     const comparableDescendants = await this.generateComparableDescendants(targetPath, user, includeNotMigratedPages);
 
 
     return this.validateGrant(comparableTarget, comparableAncestor, comparableDescendants);
     return this.validateGrant(comparableTarget, comparableAncestor, comparableDescendants);
@@ -660,12 +653,20 @@ class PageGrantService implements IPageGrantService {
     return data;
     return data;
   }
   }
 
 
+  /**
+   * Get the group grant data of page.
+   * To calculate if a group can be granted to page, the same logic as isGrantNormalized will be executed, except only the ancestor info will be used.
+   */
   async getPageGroupGrantData(page: PageDocument, user): Promise<GroupGrantData> {
   async getPageGroupGrantData(page: PageDocument, user): Promise<GroupGrantData> {
+    if (isTopPage(page.path)) {
+      return { userRelatedGroups: [], nonUserRelatedGrantedGroups: [] };
+    }
+
     const userRelatedGroups = await this.getUserRelatedGroups(user);
     const userRelatedGroups = await this.getUserRelatedGroups(user);
-    const userRelatedGroupsData = userRelatedGroups.map((group) => {
+    let userRelatedGroupsData: UserRelatedGroupsData[] = userRelatedGroups.map((group) => {
       const provider = group.type === GroupType.externalUserGroup ? group.item.provider : undefined;
       const provider = group.type === GroupType.externalUserGroup ? group.item.provider : undefined;
       return {
       return {
-        // TODO: change un-grantable groups to UserGroupPageGrantStatus.cannotGrant (https://redmine.weseek.co.jp/issues/142310)
+        // default status as notGranted
         id: group.item._id.toString(), name: group.item.name, type: group.type, provider, status: UserGroupPageGrantStatus.notGranted,
         id: group.item._id.toString(), name: group.item.name, type: group.type, provider, status: UserGroupPageGrantStatus.notGranted,
       };
       };
     });
     });
@@ -679,6 +680,8 @@ class PageGrantService implements IPageGrantService {
 
 
     const populatedGrantedGroups = await this.getPopulatedGrantedGroups(page.grantedGroups);
     const populatedGrantedGroups = await this.getPopulatedGrantedGroups(page.grantedGroups);
 
 
+    // Set the status of user-related granted groups as isGranted
+    // Append non-user-related granted groups to nonUserRelatedGrantedGroups
     populatedGrantedGroups.forEach((group) => {
     populatedGrantedGroups.forEach((group) => {
       const userRelatedGrantedGroup = userRelatedGroupsData.find((userRelatedGroup) => {
       const userRelatedGrantedGroup = userRelatedGroupsData.find((userRelatedGroup) => {
         return userRelatedGroup.id === group.item._id.toString();
         return userRelatedGroup.id === group.item._id.toString();
@@ -694,6 +697,30 @@ class PageGrantService implements IPageGrantService {
       }
       }
     });
     });
 
 
+    // Check if group can be granted to page for non-granted groups
+    const grantedUserIds = page.grantedUsers?.map(user => getIdForRef(user)) ?? [];
+    const comparableAncestor = await this.generateComparableAncestor(page.path, false);
+    userRelatedGroupsData = userRelatedGroupsData.map((groupData) => {
+      if (groupData.status === UserGroupPageGrantStatus.isGranted) {
+        return groupData;
+      }
+      const groupsToGrant = [...(page.grantedGroups ?? []), { item: groupData.id, type: groupData.type }];
+      const comparableTarget: ComparableTarget = {
+        grant: PageGrant.GRANT_USER_GROUP,
+        grantedUserIds,
+        grantedGroupIds: groupsToGrant,
+      };
+      const status = this.validateGrant(comparableTarget, comparableAncestor) ? UserGroupPageGrantStatus.notGranted : UserGroupPageGrantStatus.cannotGrant;
+      return { ...groupData, status };
+    });
+
+    const statusPriority = {
+      [UserGroupPageGrantStatus.notGranted]: 0,
+      [UserGroupPageGrantStatus.isGranted]: 1,
+      [UserGroupPageGrantStatus.cannotGrant]: 2,
+    };
+    userRelatedGroupsData.sort((a, b) => statusPriority[a.status] - statusPriority[b.status]);
+
     return { userRelatedGroups: userRelatedGroupsData, nonUserRelatedGrantedGroups };
     return { userRelatedGroups: userRelatedGroupsData, nonUserRelatedGrantedGroups };
   }
   }
 
 

+ 3 - 3
apps/app/src/stores/page.tsx

@@ -265,9 +265,9 @@ export const useSWRxInfinitePageRevisions = (
 };
 };
 
 
 /*
 /*
- * Grant normalization fetching hooks
+ * Grant data fetching hooks
  */
  */
-export const useSWRxIsGrantNormalized = (
+export const useSWRxCurrentGrantData = (
     pageId: string | null | undefined,
     pageId: string | null | undefined,
 ): SWRResponse<IResIsGrantNormalized, Error> => {
 ): SWRResponse<IResIsGrantNormalized, Error> => {
 
 
@@ -276,7 +276,7 @@ export const useSWRxIsGrantNormalized = (
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
 
 
   const key = !isGuestUser && !isReadOnlyUser && !isNotFound && pageId != null
   const key = !isGuestUser && !isReadOnlyUser && !isNotFound && pageId != null
-    ? ['/page/is-grant-normalized', pageId]
+    ? ['/page/grant-data', pageId]
     : null;
     : null;
 
 
   return useSWRImmutable(
   return useSWRImmutable(