Taichi Masuyama 4 лет назад
Родитель
Сommit
ce824b4836

+ 1 - 1
packages/app/src/server/models/obsolete-page.js

@@ -1016,7 +1016,7 @@ export const getPageSchema = (crowi) => {
        */
       let isGrantNormalized = false;
       try {
-        isGrantNormalized = await crowi.pageGrantService.pageValidationForCreate(path, user, grant, grantedUserIds, grantUserGroupId);
+        isGrantNormalized = await crowi.pageGrantService.isGrantNormalized(path, user, grant, grantedUserIds, grantUserGroupId);
       }
       catch (err) {
         logger.error(`Failed to validate grant of page at "${path}" of grant ${grant}:`, err);

+ 139 - 46
packages/app/src/server/service/page-grant.ts

@@ -4,12 +4,31 @@ import { pagePathUtils } from '@growi/core';
 import UserGroup from '~/server/models/user-group';
 import { PageModel } from '~/server/models/page';
 import { PageQueryBuilder } from '../models/obsolete-page';
-import { excludeTestIdsFromTargetIds, removeDuplicates } from '~/server/util/compare-objectId';
+import { isIncludesObjectId, removeDuplicates, excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 
 const { isTopPage } = pagePathUtils;
 
 type ObjectId = mongoose.Types.ObjectId;
 
+type ComparableTarget = {
+  grant: number,
+  grantedUserIds: ObjectId[],
+  grantedGroupId: ObjectId,
+  applicableGroupIds?: ObjectId[],
+};
+
+type ComparableAncestor = {
+  grant: number,
+  grantedUserIds: ObjectId[],
+  applicableUserIds: ObjectId[],
+  applicableGroupIds: ObjectId[],
+};
+
+type ComparableDescendants = {
+  grantedUserIds: ObjectId[],
+  descendantGroupIds: ObjectId[],
+};
+
 class PageGrantService {
 
   crowi!: any;
@@ -18,63 +37,133 @@ class PageGrantService {
     this.crowi = crowi;
   }
 
-  private validateGrantValues(grant, grantedGroupId) {
+  private validateComparableTarget(comparable: ComparableTarget) {
     const Page = mongoose.model('Page') as PageModel;
 
+    const { grant, grantedUserIds, grantedGroupId } = comparable;
+
+    if (grant === Page.GRANT_OWNER && (grantedUserIds == null || grantedUserIds.length !== 1)) {
+      throw Error('grantedUserIds must not be null and must have 1 length');
+    }
     if (grant === Page.GRANT_USER_GROUP && grantedGroupId == null) {
       throw Error('grantedGroupId is not specified');
     }
   }
 
-  validateByGrantUsers(targetUsers: ObjectId[], ancestorsUsers: ObjectId[], descendantsUsers?: ObjectId[]): boolean {
-    const usersShouldNotExist1 = excludeTestIdsFromTargetIds(targetUsers, ancestorsUsers);
-    if (usersShouldNotExist1.length > 0) {
-      return false;
-    }
+  /**
+   * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
+   * @returns boolean
+   */
+  private processValidation(target: ComparableTarget, ancestor: ComparableAncestor, descendants?: ComparableDescendants): boolean {
+    this.validateComparableTarget(target);
 
-    if (descendantsUsers == null) {
-      return true;
+    const Page = mongoose.model('Page') as PageModel;
+
+    /*
+     * ancestor side
+     */
+    // GRANT_PUBLIC
+    if (ancestor.grant === Page.GRANT_PUBLIC) {
+      // DO NOTHING
     }
+    // GRANT_OWNER
+    if (ancestor.grant === Page.GRANT_OWNER) {
+      if (target.grant !== Page.GRANT_OWNER) {
+        return false;
+      }
 
-    const usersShouldNotExist2 = excludeTestIdsFromTargetIds(descendantsUsers, targetUsers);
-    if (usersShouldNotExist2.length > 0) {
-      return false;
+      if (ancestor.grantedUserIds[0].equals(target.grantedUserIds[0])) {
+        return false;
+      }
     }
+    // GRANT_USER_GROUP
+    if (ancestor.grant === Page.GRANT_USER_GROUP) {
+      if (target.grant === Page.GRANT_PUBLIC) {
+        return false;
+      }
 
-    return true;
-  }
+      if (target.grant === Page.GRANT_OWNER) {
+        if (!isIncludesObjectId(ancestor.applicableUserIds, target.grantedUserIds[0])) {
+          return false;
+        }
+      }
 
-  private async generateTargetsGrantedUsersForCreate(user, grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId): Promise<ObjectId[]> {
-    // validate values
-    this.validateGrantValues(grant, grantedGroupId);
+      if (target.grant === Page.GRANT_USER_GROUP) {
+        if (!isIncludesObjectId(ancestor.applicableGroupIds, target.grantedGroupId)) {
+          return false;
+        }
+      }
+    }
 
-    let targetGrantedUsers: ObjectId[] = [];
+    if (descendants == null) {
+      return true;
+    }
+    /*
+     * descendant side
+     */
 
-    const Page = mongoose.model('Page') as PageModel;
-    const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
+    if (target.applicableGroupIds == null) {
+      throw Error('applicableGroupIds must not be null');
+    }
 
-    if (grant === Page.GRANT_USER_GROUP) {
-      const targetGroup = await UserGroup.findOne({ _id: grantedGroupId });
-      if (targetGroup == null) {
-        throw Error('The group of grantedGroupId does not exist.');
+    // GRANT_PUBLIC
+    if (target.grant === Page.GRANT_PUBLIC) {
+      if (descendants.descendantGroupIds.length !== 0 || descendants.descendantGroupIds.length !== 0) {
+        return false;
+      }
+    }
+    // GRANT_OWNER
+    if (target.grant === Page.GRANT_OWNER) {
+      if (descendants.descendantGroupIds.length !== 0 || descendants.grantedUserIds.length > 1) {
+        return false;
       }
 
-      const targetRelations = await UserGroupRelation.find({ relatedGroup: grantedGroupId }, { _id: 0, relatedUser: 1 });
-      targetGrantedUsers = targetRelations.map(r => r.relatedUser) as ObjectId[];
+      if (descendants.grantedUserIds.length === 1 && descendants.grantedUserIds[0].equals(target.grantedGroupId)) {
+        return false;
+      }
+    }
+    // GRANT_USER_GROUP
+    if (target.grant === Page.GRANT_USER_GROUP) {
+      const shouldNotExistIds = excludeTestIdsFromTargetIds(descendants.descendantGroupIds, target.applicableGroupIds);
+      if (shouldNotExistIds.length !== 0) {
+        return false;
+      }
     }
-    if (grant === Page.GRANT_OWNER) {
-      targetGrantedUsers = [user._id];
+
+    return true;
+  }
+
+  private async generateComparableTarget(
+      grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId, hasChild: boolean,
+  ): Promise<ComparableTarget> {
+    if (hasChild) {
+      const root = await UserGroup.findOne({ _id: grantedGroupId });
+      if (root == null) {
+        throw Error('The userGroup to compare does not exist');
+      }
+      const applicableGroupIds = await UserGroup.findGroupsWithDescendantsRecursively(root);
+
+      return {
+        grant,
+        grantedUserIds,
+        grantedGroupId,
+        applicableGroupIds,
+      };
     }
 
-    return targetGrantedUsers;
+    return {
+      grant,
+      grantedUserIds,
+      grantedGroupId,
+    };
   }
 
   /**
-   * It finds the nearest ancestor page from the targetPath. Then returns an array of grantedUsers of the ancestor page.
+   * WIP
    * @param targetPath string of the target path
-   * @returns Promise<ObjectId[]>
+   * @returns Promise<ComparableAncestor>
    */
-  private async generateAncestorsGrantedUsers(targetPath: string): Promise<ObjectId[]> {
+  private async generateComparableAncestor(targetPath: string): Promise<ComparableAncestor> {
     const Page = mongoose.model('Page') as PageModel;
     const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
 
@@ -93,8 +182,6 @@ class PageGrantService {
     if (testAncestor == null) {
       throw Error('testAncestor must exist');
     }
-    // validate values
-    this.validateGrantValues(testAncestor.grant, testAncestor.grantedGroup);
 
     if (testAncestor.grant === Page.GRANT_PUBLIC) {
       ancestorUsers = [];
@@ -112,11 +199,11 @@ class PageGrantService {
   }
 
   /**
-   * It calculates and returns the set of the all grantedUsers of all descendant pages of the targetPath.
+   * WIP
    * @param targetPath string of the target path
-   * @returns Promise<ObjectId[]>
+   * @returns ComparableDescendants
    */
-  private async generateDescendantsGrantedUsers(targetPath: string): Promise<ObjectId[]> {
+  private async generateComparableDescendants(targetPath: string): Promise<ComparableDescendants> {
     const Page = mongoose.model('Page') as PageModel;
     const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
 
@@ -148,25 +235,31 @@ class PageGrantService {
     return removeDuplicates([...grantedUsersOfGrantOwner, ...grantedUsersOfGrantUserGroup]);
   }
 
-  async pageValidationForCreate(pathToCreate: string, user, grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId): Promise<boolean> {
-    if (isTopPage(pathToCreate)) {
+  /**
+   * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
+   * @returns Promise<boolean>
+   */
+  async isGrantNormalized(targetPath: string, user, grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId): Promise<boolean> {
+    if (isTopPage(targetPath)) {
       return true;
     }
 
-    const targetUsers = await this.generateTargetsGrantedUsersForCreate(user, grant, grantedUserIds, grantedGroupId);
-    const ancestorUsers = await this.generateAncestorsGrantedUsers(pathToCreate);
+    const Page = mongoose.model('Page') as PageModel;
+
+    const comparableAncestor = await this.generateComparableAncestor(targetPath);
 
     // find existing empty page at target path
     // it will be unnecessary to check the descendant if emptyTarget is null
-    const Page = mongoose.model('Page') as PageModel;
-    const emptyTarget = await Page.findOne({ path: pathToCreate });
+    const emptyTarget = await Page.findOne({ path: targetPath });
     if (emptyTarget == null) { // checking the parent is enough
-      return this.validateByGrantUsers(targetUsers, ancestorUsers);
+      const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupId, false);
+      return this.processValidation(comparableTarget, comparableAncestor);
     }
 
-    const descendantUsers = await this.generateDescendantsGrantedUsers(pathToCreate);
+    const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupId, true);
+    const comparableDescendants = await this.generateComparableDescendants(targetPath);
 
-    return this.validateByGrantUsers(targetUsers, ancestorUsers, descendantUsers);
+    return this.processValidation(comparableTarget, comparableAncestor, comparableDescendants);
   }
 
 }