Przeglądaj źródła

Merge branch 'fix-api-to-use-granted-user-groups' into granted-groups-tests

Futa Arai 2 lat temu
rodzic
commit
a9c475689f

+ 26 - 6
apps/app/src/features/external-user-group/server/models/external-user-group-relation.integ.ts

@@ -18,10 +18,17 @@ describe('ExternalUserGroupRelation model', () => {
   let user1;
   const userId1 = new mongoose.Types.ObjectId();
 
+  let user2;
+  const userId2 = new mongoose.Types.ObjectId();
+
   beforeAll(async() => {
     user1 = await User.create({
       _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
     });
+
+    user2 = await User.create({
+      _id: userId2, name: 'user2', username: 'user2', email: 'user2@example.com',
+    });
   });
 
   afterEach(async() => {
@@ -78,13 +85,7 @@ describe('ExternalUserGroupRelation model', () => {
     const groupId2 = new mongoose.Types.ObjectId();
     const groupId3 = new mongoose.Types.ObjectId();
 
-    let user2;
-
     beforeAll(async() => {
-      user2 = await User.create({
-        name: 'user2', username: 'user2', email: 'user2@example.com',
-      });
-
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
     });
@@ -94,4 +95,23 @@ describe('ExternalUserGroupRelation model', () => {
       expect(userIds).toStrictEqual([userId1.toString(), user2._id.toString()]);
     });
   });
+
+  describe('findAllUserGroupIdsRelatedToUser', () => {
+    const groupId1 = new mongoose.Types.ObjectId();
+    const groupId2 = new mongoose.Types.ObjectId();
+    const groupId3 = new mongoose.Types.ObjectId();
+
+    beforeAll(async() => {
+      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+      await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
+    });
+
+    it('finds all group ids related to user', async() => {
+      const groupIds = await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user1);
+      expect(groupIds).toStrictEqual([groupId1, groupId2]);
+
+      const groupIds2 = await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user2);
+      expect(groupIds2).toStrictEqual([groupId3]);
+    });
+  });
 });

+ 4 - 2
apps/app/src/features/external-user-group/server/models/external-user-group-relation.ts

@@ -21,7 +21,7 @@ export interface ExternalUserGroupRelationModel extends Model<ExternalUserGroupR
 
   findGroupsWithDescendantsByGroupAndUser: (group: ExternalUserGroupDocument, user) => Promise<ExternalUserGroupDocument[]>,
 
-  countByGroupIdAndUser: (userGroupId: string, userData) => Promise<number>
+  countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
 }
 
 const schema = new Schema<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>({
@@ -41,8 +41,10 @@ schema.statics.removeAllInvalidRelations = UserGroupRelation.removeAllInvalidRel
 
 schema.statics.findGroupsWithDescendantsByGroupAndUser = UserGroupRelation.findGroupsWithDescendantsByGroupAndUser;
 
-schema.statics.countByGroupIdAndUser = UserGroupRelation.countByGroupIdAndUser;
+schema.statics.countByGroupIdsAndUser = UserGroupRelation.countByGroupIdsAndUser;
 
 schema.statics.findAllUserIdsForUserGroups = UserGroupRelation.findAllUserIdsForUserGroups;
 
+schema.statics.findAllUserGroupIdsRelatedToUser = UserGroupRelation.findAllUserGroupIdsRelatedToUser;
+
 export default getOrCreateModel<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>('ExternalUserGroupRelation', schema);

+ 22 - 22
apps/app/src/server/models/obsolete-page.js

@@ -3,6 +3,7 @@ import { templateChecker, pagePathUtils, pathUtils } from '@growi/core/dist/util
 import escapeStringRegexp from 'escape-string-regexp';
 
 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 loggerFactory from '~/utils/logger';
 
 import UserGroup from './user-group';
@@ -14,11 +15,10 @@ import UserGroupRelation from './user-group-relation';
 
 /* eslint-disable no-use-before-define */
 
-const debug = require('debug')('growi:models:page');
-
 const nodePath = require('path');
 
 const differenceInYears = require('date-fns/differenceInYears');
+const debug = require('debug')('growi:models:page');
 const mongoose = require('mongoose');
 const urljoin = require('url-join');
 
@@ -323,10 +323,10 @@ export const getPageSchema = (crowi) => {
   pageSchema.statics.isAccessiblePageByViewer = async function(id, user) {
     const baseQuery = this.count({ _id: id });
 
-    let userGroups = [];
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : [];
 
     const queryBuilder = new this.PageQueryBuilder(baseQuery);
     queryBuilder.addConditionToFilteringByViewer(user, userGroups, true);
@@ -343,10 +343,10 @@ export const getPageSchema = (crowi) => {
   pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups, includeEmpty = false) {
     const baseQuery = this.findOne({ _id: id });
 
-    let relatedUserGroups = userGroups;
-    if (user != null && relatedUserGroups == null) {
-      relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const relatedUserGroups = (user != null && userGroups == null) ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : userGroups;
 
     const queryBuilder = new this.PageQueryBuilder(baseQuery, includeEmpty);
     queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
@@ -384,10 +384,10 @@ export const getPageSchema = (crowi) => {
     // pick the longest one
     const baseQuery = this.findOne({ path: { $in: ancestorsPaths } }).sort({ path: -1 });
 
-    let relatedUserGroups = userGroups;
-    if (user != null && relatedUserGroups == null) {
-      relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const relatedUserGroups = (user != null && userGroups == null) ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : userGroups;
 
     const queryBuilder = new this.PageQueryBuilder(baseQuery, includeEmpty);
     queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
@@ -511,10 +511,10 @@ export const getPageSchema = (crowi) => {
     const hidePagesRestrictedByGroup = crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
 
     // determine UserGroup condition
-    let userGroups = null;
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
 
     return builder.addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink, !hidePagesRestrictedByOwner, !hidePagesRestrictedByGroup);
   }
@@ -529,10 +529,10 @@ export const getPageSchema = (crowi) => {
    */
   async function addConditionToFilteringByViewerToEdit(builder, user) {
     // determine UserGroup condition
-    let userGroups = null;
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
 
     return builder.addConditionToFilteringByViewer(user, userGroups, false, false, false);
   }

+ 9 - 8
apps/app/src/server/models/page.ts

@@ -18,6 +18,7 @@ import mongoose, {
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
 
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 
 import loggerFactory from '../../utils/logger';
@@ -319,10 +320,10 @@ export class PageQueryBuilder {
 
   async addConditionForParentNormalization(user): Promise<PageQueryBuilder> {
     // determine UserGroup condition
-    let userGroups;
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
 
     const grantConditions: any[] = [
       { grant: null },
@@ -370,10 +371,10 @@ export class PageQueryBuilder {
 
   // add viewer condition to PageQueryBuilder instance
   async addViewerCondition(user, userGroups = null, includeAnyoneWithTheLink = false): Promise<PageQueryBuilder> {
-    let relatedUserGroups = userGroups;
-    if (user != null && relatedUserGroups == null) {
-      relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const relatedUserGroups = (user != null && userGroups == null) ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : userGroups;
 
     this.addConditionToFilteringByViewer(user, relatedUserGroups, includeAnyoneWithTheLink);
     return this;

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

@@ -25,7 +25,7 @@ export interface UserGroupRelationModel extends Model<UserGroupRelationDocument>
 
   findGroupsWithDescendantsByGroupAndUser: (group: UserGroupDocument, user) => Promise<UserGroupDocument[]>,
 
-  countByGroupIdAndUser: (userGroupId: string, userData) => Promise<number>
+  countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
 }
 
 /*
@@ -156,9 +156,9 @@ schema.statics.findAllUserGroupIdsRelatedToUser = async function(user) {
  * @param {User} userData find query param for relatedUser
  * @returns {Promise<number>}
  */
-schema.statics.countByGroupIdAndUser = async function(userGroupId: string, userData): Promise<number> {
+schema.statics.countByGroupIdsAndUser = async function(userGroupIds: ObjectIdLike[], userData): Promise<number> {
   const query = {
-    relatedGroup: userGroupId,
+    relatedGroup: { $in: userGroupIds },
     relatedUser: userData.id,
   };
 

+ 0 - 5
apps/app/src/server/routes/admin.js

@@ -7,11 +7,6 @@ const debug = require('debug')('growi:routes:admin');
 
 /* eslint-disable no-use-before-define */
 module.exports = function(crowi, app) {
-
-  const models = crowi.models;
-  const UserGroupRelation = models.UserGroupRelation;
-  const GlobalNotificationSetting = models.GlobalNotificationSetting;
-
   const {
     configManager,
     aclService,

+ 1 - 1
apps/app/src/server/routes/apiv3/page.js

@@ -508,7 +508,7 @@ module.exports = (crowi) => {
     const {
       grantedUserGroups: parentGrantedUserGroupIds,
       grantedExternalUserGroups: parentGrantedExternalUserGroupIds,
-    } = divideByType(parentPage.grantedGroup);
+    } = divideByType(parentPage.grantedGroups);
     const parentPageUserGroups = await UserGroup.find({ _id: { $in: parentGrantedUserGroupIds } });
     const parentPageExternalUserGroups = await ExternalUserGroup.find({ _id: { $in: parentGrantedExternalUserGroupIds } });
     const parentGrantedUserGroupData = parentPageUserGroups.map((group) => {

+ 1 - 2
apps/app/src/server/routes/apiv3/user-group-relation.js

@@ -1,5 +1,6 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 
+import UserGroupRelation from '~/server/models/user-group-relation';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:user-group-relation'); // eslint-disable-line no-unused-vars
@@ -23,8 +24,6 @@ module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
 
-  const { UserGroupRelation } = crowi.models;
-
   validator.list = [
     query('groupIds', 'groupIds is required and must be an array').isArray(),
     query('childGroupIds', 'childGroupIds must be an array').optional().isArray(),

+ 1 - 1
apps/app/src/server/routes/apiv3/user-group.js

@@ -5,6 +5,7 @@ import { ErrorV3 } from '@growi/core/dist/models';
 import { SupportedAction } from '~/interfaces/activity';
 import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
 import UserGroup from '~/server/models/user-group';
+import UserGroupRelation from '~/server/models/user-group-relation';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 
@@ -42,7 +43,6 @@ module.exports = (crowi) => {
   const activityEvent = crowi.event('activity');
 
   const {
-    UserGroupRelation,
     User,
     Page,
   } = crowi.models;

+ 3 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -2,9 +2,11 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
 
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
+import UserGroupRelation from '~/server/models/user-group-relation';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
@@ -90,7 +92,6 @@ module.exports = (crowi) => {
   const {
     User,
     Page,
-    UserGroupRelation,
   } = crowi.models;
 
 
@@ -813,6 +814,7 @@ module.exports = (crowi) => {
       const homepagePath = userHomepagePath(user);
 
       await UserGroupRelation.remove({ relatedUser: user });
+      await ExternalUserGroupRelation.remove({ relatedUser: user });
       await user.statusDelete();
       await ExternalAccount.remove({ user });
 

+ 2 - 2
apps/app/src/server/routes/me.js

@@ -1,3 +1,5 @@
+import UserGroupRelation from '../models/user-group-relation';
+
 /**
  * @swagger
  *
@@ -49,8 +51,6 @@
  */
 
 module.exports = function(crowi, app) {
-  const models = crowi.models;
-  const UserGroupRelation = models.UserGroupRelation;
   const ApiResponse = require('../util/apiResponse');
 
   // , pluginService = require('../service/plugin')

+ 5 - 4
apps/app/src/server/routes/search.ts

@@ -1,3 +1,4 @@
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
@@ -128,10 +129,10 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('SearchService is not reachable.'));
     }
 
-    let userGroups = [];
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
 
     const searchOpts = {
       ...paginateOpts, type, sort, order,

+ 10 - 4
apps/app/src/server/service/page-grant.ts

@@ -344,7 +344,10 @@ class PageGrantService {
 
     if (includeNotMigratedPages) {
       // Add grantCondition for not normalized pages
-      const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+      const userGroups = [
+        ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+        ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ];
       const grantCondition = Page.generateGrantCondition(user, userGroups);
       const conditionForNotNormalizedPages = {
         $and: [
@@ -539,10 +542,10 @@ class PageGrantService {
       const applicableGroups = [...applicableUserGroups, ...applicableExternalUserGroups];
 
       const isUserExistInUserGroup = (await Promise.all(targetUserGroups.map((group) => {
-        return UserGroupRelation.countByGroupIdAndUser(group._id, user);
+        return UserGroupRelation.countByGroupIdsAndUser([group._id], user);
       }))).some(count => count > 0);
       const isUserExistInExternalUserGroup = (await Promise.all(targetExternalUserGroups.map((group) => {
-        return ExternalUserGroupRelation.countByGroupIdAndUser(group._id, user);
+        return ExternalUserGroupRelation.countByGroupIdsAndUser([group._id], user);
       }))).some(count => count > 0);
       const isUserExistInGroup = isUserExistInUserGroup || isUserExistInExternalUserGroup;
 
@@ -563,7 +566,10 @@ class PageGrantService {
    * @returns {Promise<boolean>}
    */
   async canOverwriteDescendants(targetPath: string, operator: { _id: ObjectIdLike }, updateGrantInfo: UpdateGrantInfo): Promise<boolean> {
-    const relatedGroupIds = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(operator);
+    const relatedGroupIds = [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(operator)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(operator)),
+    ];
     const operatorGrantInfo = {
       userId: operator._id,
       userGroupIds: new Set<ObjectIdLike>(relatedGroupIds),

+ 27 - 23
apps/app/src/server/service/page.ts

@@ -14,6 +14,7 @@ import escapeStringRegexp from 'escape-string-regexp';
 import mongoose, { ObjectId, Cursor } from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import {
@@ -39,6 +40,7 @@ import { serializePageSecurely } from '../models/serializers/page-serializer';
 import Subscription from '../models/subscription';
 import UserGroupRelation from '../models/user-group-relation';
 import { V5ConversionError } from '../models/vo/v5-conversion-error';
+import { divideByType } from '../util/granted-group';
 
 const debug = require('debug')('growi:services:page');
 
@@ -2430,10 +2432,10 @@ class PageService {
     const MAX_LENGTH = 350;
 
     // aggregation options
-    let userGroups;
-    if (user != null && userGroups == null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
     const viewerCondition = Page.generateGrantCondition(user, userGroups);
     const filterByIds = {
       _id: { $in: pageIds },
@@ -2980,10 +2982,10 @@ class PageService {
     pathAndRegExpsToNormalize.push(...paths);
 
     // determine UserGroup condition
-    let userGroups = null;
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
 
     const grantFiltersByUser: { $or: any[] } = Page.generateGrantCondition(user, userGroups);
 
@@ -3382,10 +3384,10 @@ class PageService {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
     const pipeline = this.buildBasePipelineToCreateEmptyPages(paths, onlyMigratedAsExistingPages, andFilter);
-    let userGroups = null;
-    if (user != null) {
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-    }
+    const userGroups = user != null ? [
+      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+    ] : null;
     const grantCondition = Page.generateGrantCondition(user, userGroups);
     pipeline.push({ $match: grantCondition });
 
@@ -3531,13 +3533,15 @@ class PageService {
     pageDocument.status = Page.STATUS_PUBLISHED;
   }
 
-  private async validateAppliedScope(user, grant, grantUserGroupId) {
-    if (grant === PageGrant.GRANT_USER_GROUP && grantUserGroupId == null) {
-      throw new Error('grant userGroupId is not specified');
+  private async validateAppliedScope(user, grant, grantUserGroupIds: GrantedGroup[]) {
+    if (grant === PageGrant.GRANT_USER_GROUP && grantUserGroupIds == null) {
+      throw new Error('grantUserGroupIds is not specified');
     }
 
     if (grant === PageGrant.GRANT_USER_GROUP) {
-      const count = await UserGroupRelation.countByGroupIdAndUser(grantUserGroupId, user);
+      const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantUserGroupIds);
+      const count = await UserGroupRelation.countByGroupIdsAndUser(grantedUserGroupIds, user)
+        + await ExternalUserGroupRelation.countByGroupIdsAndUser(grantedExternalUserGroupIds, user);
 
       if (count === 0) {
         throw new Error('no relations were exist for group and user.');
@@ -3736,7 +3740,7 @@ class PageService {
     const Revision = mongoose.model('Revision') as any; // TODO: TypeScriptize model
 
     const format = options.format || 'markdown';
-    const grantUserGroupId = options.grantUserGroupId || null;
+    const grantUserGroupIds = options.grantUserGroupIds || null;
     const expandContentWidth = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     // sanitize path
@@ -3762,8 +3766,8 @@ class PageService {
     if (expandContentWidth != null) {
       page.expandContentWidth = expandContentWidth;
     }
-    await this.validateAppliedScope(user, grant, grantUserGroupId);
-    page.applyScope(user, grant, grantUserGroupId);
+    await this.validateAppliedScope(user, grant, grantUserGroupIds);
+    page.applyScope(user, grant, grantUserGroupIds);
 
     let savedPage = await page.save();
     const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
@@ -4071,16 +4075,16 @@ class PageService {
   }
 
 
-  async updatePageV4(pageData, body, previousBody, user, options: any = {}): Promise<PageDocument> {
+  async updatePageV4(pageData, body, previousBody, user, options: IOptionsForUpdate = {}): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Revision = mongoose.model('Revision') as any; // TODO: TypeScriptize model
 
     const grant = options.grant || pageData.grant; // use the previous data if absence
-    const grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
+    const grantUserGroupIds = options.grantUserGroupIds || pageData.grantUserGroupIds; // use the previous data if absence
     const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
 
-    await this.validateAppliedScope(user, grant, grantUserGroupId);
-    pageData.applyScope(user, grant, grantUserGroupId);
+    await this.validateAppliedScope(user, grant, grantUserGroupIds);
+    pageData.applyScope(user, grant, grantUserGroupIds);
 
     // update existing page
     let savedPage = await pageData.save();