page-grant.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import {
  2. PageGrant, type PageGrantCanBeOnTree,
  3. } from '@growi/core';
  4. import {
  5. pagePathUtils, pathUtils, pageUtils,
  6. } from '@growi/core/dist/utils';
  7. import escapeStringRegexp from 'escape-string-regexp';
  8. import mongoose from 'mongoose';
  9. import { IRecordApplicableGrant } from '~/interfaces/page-grant';
  10. import { PageDocument, PageModel } from '~/server/models/page';
  11. import UserGroup from '~/server/models/user-group';
  12. import { isIncludesObjectId, excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
  13. const { addTrailingSlash } = pathUtils;
  14. const { isTopPage } = pagePathUtils;
  15. const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
  16. type ObjectIdLike = mongoose.Types.ObjectId | string;
  17. type ComparableTarget = {
  18. grant: number,
  19. grantedUserIds?: ObjectIdLike[],
  20. grantedGroupId?: ObjectIdLike,
  21. applicableUserIds?: ObjectIdLike[],
  22. applicableGroupIds?: ObjectIdLike[],
  23. };
  24. type ComparableAncestor = {
  25. grant: number,
  26. grantedUserIds: ObjectIdLike[],
  27. applicableUserIds?: ObjectIdLike[],
  28. applicableGroupIds?: ObjectIdLike[],
  29. };
  30. type ComparableDescendants = {
  31. isPublicExist: boolean,
  32. grantedUserIds: ObjectIdLike[],
  33. grantedGroupIds: ObjectIdLike[],
  34. };
  35. /**
  36. * @param grantedUserGroupInfo This parameter has info to calculate whether the update operation is allowed.
  37. * - See the `calcCanOverwriteDescendants` private method for detail.
  38. */
  39. type UpdateGrantInfo = {
  40. grant: typeof PageGrant.GRANT_PUBLIC,
  41. } | {
  42. grant: typeof PageGrant.GRANT_OWNER,
  43. grantedUserId: ObjectIdLike,
  44. } | {
  45. grant: typeof PageGrant.GRANT_USER_GROUP,
  46. grantedUserGroupInfo: {
  47. groupId: ObjectIdLike,
  48. userIds: Set<ObjectIdLike>,
  49. childrenOrItselfGroupIds: Set<ObjectIdLike>,
  50. },
  51. };
  52. type DescendantPagesGrantInfo = {
  53. grantSet: Set<number>,
  54. grantedUserIds: Set<ObjectIdLike>, // all only me users of descendant pages
  55. grantedUserGroupIds: Set<ObjectIdLike>, // all user groups of descendant pages
  56. };
  57. /**
  58. * @param {ObjectIdLike} userId The _id of the operator.
  59. * @param {Set<ObjectIdLike>} userGroupIds The Set of the _id of the user groups that the operator belongs.
  60. */
  61. type OperatorGrantInfo = {
  62. userId: ObjectIdLike,
  63. userGroupIds: Set<ObjectIdLike>,
  64. };
  65. class PageGrantService {
  66. crowi!: any;
  67. constructor(crowi: any) {
  68. this.crowi = crowi;
  69. }
  70. private validateComparableTarget(comparable: ComparableTarget) {
  71. const Page = mongoose.model('Page') as unknown as PageModel;
  72. const { grant, grantedUserIds, grantedGroupId } = comparable;
  73. if (grant === Page.GRANT_OWNER && (grantedUserIds == null || grantedUserIds.length !== 1)) {
  74. throw Error('grantedUserIds must not be null and must have 1 length');
  75. }
  76. if (grant === Page.GRANT_USER_GROUP && grantedGroupId == null) {
  77. throw Error('grantedGroupId is not specified');
  78. }
  79. }
  80. /**
  81. * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
  82. * @returns boolean
  83. */
  84. private processValidation(target: ComparableTarget, ancestor: ComparableAncestor, descendants?: ComparableDescendants): boolean {
  85. this.validateComparableTarget(target);
  86. const Page = mongoose.model('Page') as unknown as PageModel;
  87. /*
  88. * ancestor side
  89. */
  90. // GRANT_PUBLIC
  91. if (ancestor.grant === Page.GRANT_PUBLIC) { // any page can exist under public page
  92. // do nothing
  93. }
  94. // GRANT_OWNER
  95. else if (ancestor.grant === Page.GRANT_OWNER) {
  96. if (target.grantedUserIds?.length !== 1) {
  97. return false;
  98. }
  99. if (target.grant !== Page.GRANT_OWNER) { // only GRANT_OWNER page can exist under GRANT_OWNER page
  100. return false;
  101. }
  102. if (ancestor.grantedUserIds[0].toString() !== target.grantedUserIds[0].toString()) { // the grantedUser must be the same as parent's under the GRANT_OWNER page
  103. return false;
  104. }
  105. }
  106. // GRANT_USER_GROUP
  107. else if (ancestor.grant === Page.GRANT_USER_GROUP) {
  108. if (ancestor.applicableGroupIds == null || ancestor.applicableUserIds == null) {
  109. throw Error('applicableGroupIds and applicableUserIds are not specified');
  110. }
  111. if (target.grant === Page.GRANT_PUBLIC) { // public page must not exist under GRANT_USER_GROUP page
  112. return false;
  113. }
  114. if (target.grant === Page.GRANT_OWNER) {
  115. if (target.grantedUserIds?.length !== 1) {
  116. throw Error('grantedUserIds must have one user');
  117. }
  118. if (!isIncludesObjectId(ancestor.applicableUserIds, target.grantedUserIds[0])) { // GRANT_OWNER pages under GRAND_USER_GROUP page must be owned by the member of the grantedGroup of the GRAND_USER_GROUP page
  119. return false;
  120. }
  121. }
  122. if (target.grant === Page.GRANT_USER_GROUP) {
  123. if (target.grantedGroupId == null) {
  124. throw Error('grantedGroupId must not be null');
  125. }
  126. if (!isIncludesObjectId(ancestor.applicableGroupIds, target.grantedGroupId)) { // only child groups or the same group can exist under GRANT_USER_GROUP page
  127. return false;
  128. }
  129. }
  130. }
  131. if (descendants == null) {
  132. return true;
  133. }
  134. /*
  135. * descendant side
  136. */
  137. // GRANT_PUBLIC
  138. if (target.grant === Page.GRANT_PUBLIC) { // any page can exist under public page
  139. // do nothing
  140. }
  141. // GRANT_OWNER
  142. else if (target.grant === Page.GRANT_OWNER) {
  143. if (target.grantedUserIds?.length !== 1) {
  144. throw Error('grantedUserIds must have one user');
  145. }
  146. if (descendants.isPublicExist) { // public page must not exist under GRANT_OWNER page
  147. return false;
  148. }
  149. if (descendants.grantedGroupIds.length !== 0 || descendants.grantedUserIds.length > 1) { // groups or more than 2 grantedUsers must not be in descendants
  150. return false;
  151. }
  152. if (descendants.grantedUserIds.length === 1 && descendants.grantedUserIds[0].toString() !== target.grantedUserIds[0].toString()) { // if Only me page exists, then all of them must be owned by the same user as the target page
  153. return false;
  154. }
  155. }
  156. // GRANT_USER_GROUP
  157. else if (target.grant === Page.GRANT_USER_GROUP) {
  158. if (target.applicableGroupIds == null || target.applicableUserIds == null) {
  159. throw Error('applicableGroupIds and applicableUserIds must not be null');
  160. }
  161. if (descendants.isPublicExist) { // public page must not exist under GRANT_USER_GROUP page
  162. return false;
  163. }
  164. const shouldNotExistGroupIds = excludeTestIdsFromTargetIds(descendants.grantedGroupIds, target.applicableGroupIds);
  165. const shouldNotExistUserIds = excludeTestIdsFromTargetIds(descendants.grantedUserIds, target.applicableUserIds);
  166. if (shouldNotExistGroupIds.length !== 0 || shouldNotExistUserIds.length !== 0) {
  167. return false;
  168. }
  169. }
  170. return true;
  171. }
  172. /**
  173. * Prepare ComparableTarget
  174. * @returns Promise<ComparableAncestor>
  175. */
  176. private async generateComparableTarget(
  177. grant, grantedUserIds: ObjectIdLike[] | undefined, grantedGroupId: ObjectIdLike | undefined, includeApplicable: boolean,
  178. ): Promise<ComparableTarget> {
  179. if (includeApplicable) {
  180. const Page = mongoose.model('Page') as unknown as PageModel;
  181. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  182. let applicableUserIds: ObjectIdLike[] | undefined;
  183. let applicableGroupIds: ObjectIdLike[] | undefined;
  184. if (grant === Page.GRANT_USER_GROUP) {
  185. const targetUserGroup = await UserGroup.findOne({ _id: grantedGroupId });
  186. if (targetUserGroup == null) {
  187. throw Error('Target user group does not exist');
  188. }
  189. const relatedUsers = await UserGroupRelation.find({ relatedGroup: targetUserGroup._id });
  190. applicableUserIds = relatedUsers.map(u => u.relatedUser);
  191. const applicableGroups = grantedGroupId != null ? await UserGroup.findGroupsWithDescendantsById(grantedGroupId) : null;
  192. applicableGroupIds = applicableGroups?.map(g => g._id) || null;
  193. }
  194. return {
  195. grant,
  196. grantedUserIds,
  197. grantedGroupId,
  198. applicableUserIds,
  199. applicableGroupIds,
  200. };
  201. }
  202. return {
  203. grant,
  204. grantedUserIds,
  205. grantedGroupId,
  206. };
  207. }
  208. /**
  209. * Prepare ComparableAncestor
  210. * @param targetPath string of the target path
  211. * @returns Promise<ComparableAncestor>
  212. */
  213. private async generateComparableAncestor(targetPath: string, includeNotMigratedPages: boolean): Promise<ComparableAncestor> {
  214. const Page = mongoose.model('Page') as unknown as PageModel;
  215. const { PageQueryBuilder } = Page;
  216. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  217. let applicableUserIds: ObjectIdLike[] | undefined;
  218. let applicableGroupIds: ObjectIdLike[] | undefined;
  219. /*
  220. * make granted users list of ancestor's
  221. */
  222. const builderForAncestors = new PageQueryBuilder(Page.find(), false);
  223. if (!includeNotMigratedPages) {
  224. builderForAncestors.addConditionAsOnTree();
  225. }
  226. const ancestors = await builderForAncestors
  227. .addConditionToListOnlyAncestors(targetPath)
  228. .addConditionToSortPagesByDescPath()
  229. .query
  230. .exec();
  231. const testAncestor = ancestors[0]; // TODO: consider when duplicate testAncestors exist
  232. if (testAncestor == null) {
  233. throw Error('testAncestor must exist');
  234. }
  235. if (testAncestor.grant === Page.GRANT_USER_GROUP) {
  236. // make a set of all users
  237. const grantedRelations = await UserGroupRelation.find({ relatedGroup: testAncestor.grantedGroup }, { _id: 0, relatedUser: 1 });
  238. const grantedGroups = await UserGroup.findGroupsWithDescendantsById(testAncestor.grantedGroup);
  239. applicableGroupIds = grantedGroups.map(g => g._id);
  240. applicableUserIds = Array.from(new Set(grantedRelations.map(r => r.relatedUser))) as ObjectIdLike[];
  241. }
  242. return {
  243. grant: testAncestor.grant,
  244. grantedUserIds: testAncestor.grantedUsers,
  245. applicableUserIds,
  246. applicableGroupIds,
  247. };
  248. }
  249. /**
  250. * Prepare ComparableDescendants
  251. * @param targetPath string of the target path
  252. * @returns ComparableDescendants
  253. */
  254. private async generateComparableDescendants(targetPath: string, user, includeNotMigratedPages = false): Promise<ComparableDescendants> {
  255. const Page = mongoose.model('Page') as unknown as PageModel;
  256. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  257. // Build conditions
  258. const $match: {$or: any} = {
  259. $or: [],
  260. };
  261. const commonCondition = {
  262. path: new RegExp(`^${escapeStringRegexp(addTrailingSlash(targetPath))}`, 'i'),
  263. isEmpty: false,
  264. };
  265. const conditionForNormalizedPages: any = {
  266. ...commonCondition,
  267. parent: { $ne: null },
  268. };
  269. $match.$or.push(conditionForNormalizedPages);
  270. if (includeNotMigratedPages) {
  271. // Add grantCondition for not normalized pages
  272. const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  273. const grantCondition = Page.generateGrantCondition(user, userGroups);
  274. const conditionForNotNormalizedPages = {
  275. $and: [
  276. {
  277. ...commonCondition,
  278. parent: null,
  279. },
  280. grantCondition,
  281. ],
  282. };
  283. $match.$or.push(conditionForNotNormalizedPages);
  284. }
  285. const result = await Page.aggregate([
  286. { // match to descendants excluding empty pages
  287. $match,
  288. },
  289. {
  290. $project: {
  291. _id: 0,
  292. grant: 1,
  293. grantedUsers: 1,
  294. grantedGroup: 1,
  295. },
  296. },
  297. { // remove duplicates from pipeline
  298. $group: {
  299. _id: '$grant',
  300. grantedGroupSet: { $addToSet: '$grantedGroup' },
  301. grantedUsersSet: { $addToSet: '$grantedUsers' },
  302. },
  303. },
  304. { // flatten granted user set
  305. $unwind: {
  306. path: '$grantedUsersSet',
  307. },
  308. },
  309. ]);
  310. // GRANT_PUBLIC group
  311. const isPublicExist = result.some(r => r._id === Page.GRANT_PUBLIC);
  312. // GRANT_OWNER group
  313. const grantOwnerResult = result.filter(r => r._id === Page.GRANT_OWNER)[0]; // users of GRANT_OWNER
  314. const grantedUserIds: ObjectIdLike[] = grantOwnerResult?.grantedUsersSet ?? [];
  315. // GRANT_USER_GROUP group
  316. const grantUserGroupResult = result.filter(r => r._id === Page.GRANT_USER_GROUP)[0]; // users of GRANT_OWNER
  317. const grantedGroupIds = grantUserGroupResult?.grantedGroupSet ?? [];
  318. return {
  319. isPublicExist,
  320. grantedUserIds,
  321. grantedGroupIds,
  322. };
  323. }
  324. /**
  325. * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
  326. * Only v5 schema pages will be used to compare by default (Set includeNotMigratedPages to true to include v4 schema pages as well).
  327. * When comparing, it will use path regex to collect pages instead of using parent attribute of the Page model. This is reasonable since
  328. * using the path attribute is safer than using the parent attribute in this case. 2022.02.13 -- Taichi Masuyama
  329. * @returns Promise<boolean>
  330. */
  331. async isGrantNormalized(
  332. // eslint-disable-next-line max-len
  333. user, targetPath: string, grant, grantedUserIds?: ObjectIdLike[], grantedGroupId?: ObjectIdLike, shouldCheckDescendants = false, includeNotMigratedPages = false,
  334. ): Promise<boolean> {
  335. if (isTopPage(targetPath)) {
  336. return true;
  337. }
  338. const comparableAncestor = await this.generateComparableAncestor(targetPath, includeNotMigratedPages);
  339. if (!shouldCheckDescendants) { // checking the parent is enough
  340. const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupId, false);
  341. return this.processValidation(comparableTarget, comparableAncestor);
  342. }
  343. const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupId, true);
  344. const comparableDescendants = await this.generateComparableDescendants(targetPath, user, includeNotMigratedPages);
  345. return this.processValidation(comparableTarget, comparableAncestor, comparableDescendants);
  346. }
  347. /**
  348. * Separate normalizable pages and NOT normalizable pages by PageService.prototype.isGrantNormalized method.
  349. * normalizable pages = Pages which are able to run normalizeParentRecursively method (grant & userGroup rule is correct)
  350. * @param pageIds pageIds to be tested
  351. * @returns a tuple with the first element of normalizable pages and the second element of NOT normalizable pages
  352. */
  353. async separateNormalizableAndNotNormalizablePages(user, pages): Promise<[(PageDocument & { _id: any })[], (PageDocument & { _id: any })[]]> {
  354. if (pages.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  355. throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
  356. }
  357. const shouldCheckDescendants = true;
  358. const shouldIncludeNotMigratedPages = true;
  359. const normalizable: (PageDocument & { _id: any })[] = [];
  360. const nonNormalizable: (PageDocument & { _id: any })[] = []; // can be used to tell user which page failed to migrate
  361. for await (const page of pages) {
  362. const {
  363. path, grant, grantedUsers: grantedUserIds, grantedGroup: grantedGroupId,
  364. } = page;
  365. if (!pageUtils.isPageNormalized(page)) {
  366. nonNormalizable.push(page);
  367. continue;
  368. }
  369. if (await this.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants, shouldIncludeNotMigratedPages)) {
  370. normalizable.push(page);
  371. }
  372. else {
  373. nonNormalizable.push(page);
  374. }
  375. }
  376. return [normalizable, nonNormalizable];
  377. }
  378. async calcApplicableGrantData(page, user): Promise<IRecordApplicableGrant> {
  379. const Page = mongoose.model('Page') as unknown as PageModel;
  380. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  381. // -- Public only if top page
  382. const isOnlyPublicApplicable = isTopPage(page.path);
  383. if (isOnlyPublicApplicable) {
  384. return {
  385. [PageGrant.GRANT_PUBLIC]: null,
  386. };
  387. }
  388. // Increment an object (type IRecordApplicableGrant)
  389. // grant is never public, anyone with the link, nor specified
  390. const data: IRecordApplicableGrant = {
  391. [PageGrant.GRANT_RESTRICTED]: null, // any page can be restricted
  392. };
  393. // -- Any grant is allowed if parent is null
  394. const isAnyGrantApplicable = page.parent == null;
  395. if (isAnyGrantApplicable) {
  396. data[PageGrant.GRANT_PUBLIC] = null;
  397. data[PageGrant.GRANT_OWNER] = null;
  398. data[PageGrant.GRANT_USER_GROUP] = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  399. return data;
  400. }
  401. const parent = await Page.findById(page.parent);
  402. if (parent == null) {
  403. throw Error('The page\'s parent does not exist.');
  404. }
  405. const {
  406. grant, grantedUsers, grantedGroup,
  407. } = parent;
  408. if (grant === PageGrant.GRANT_PUBLIC) {
  409. data[PageGrant.GRANT_PUBLIC] = null;
  410. data[PageGrant.GRANT_OWNER] = null;
  411. data[PageGrant.GRANT_USER_GROUP] = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
  412. }
  413. else if (grant === PageGrant.GRANT_OWNER) {
  414. const grantedUser = grantedUsers[0];
  415. const isUserApplicable = grantedUser.toString() === user._id.toString();
  416. if (isUserApplicable) {
  417. data[PageGrant.GRANT_OWNER] = null;
  418. }
  419. }
  420. else if (grant === PageGrant.GRANT_USER_GROUP) {
  421. const group = await UserGroup.findById(grantedGroup);
  422. if (group == null) {
  423. throw Error('Group not found to calculate grant data.');
  424. }
  425. const applicableGroups = await UserGroupRelation.findGroupsWithDescendantsByGroupAndUser(group, user);
  426. const isUserExistInGroup = await UserGroupRelation.countByGroupIdAndUser(group, user) > 0;
  427. if (isUserExistInGroup) {
  428. data[PageGrant.GRANT_OWNER] = null;
  429. }
  430. data[PageGrant.GRANT_USER_GROUP] = { applicableGroups };
  431. }
  432. return data;
  433. }
  434. /**
  435. * see: https://dev.growi.org/635a314eac6bcd85cbf359fc
  436. * @param {string} targetPath
  437. * @param operator
  438. * @param {UpdateGrantInfo} updateGrantInfo
  439. * @returns {Promise<boolean>}
  440. */
  441. async canOverwriteDescendants(targetPath: string, operator: { _id: ObjectIdLike }, updateGrantInfo: UpdateGrantInfo): Promise<boolean> {
  442. const UserGroupRelationModel = mongoose.model('UserGroupRelation') as any; // TODO: TypeScriptize model
  443. const relatedGroupIds = await UserGroupRelationModel.findAllUserGroupIdsRelatedToUser(operator);
  444. const operatorGrantInfo = {
  445. userId: operator._id,
  446. userGroupIds: new Set<ObjectIdLike>(relatedGroupIds),
  447. };
  448. const comparableDescendants = await this.generateComparableDescendants(targetPath, operator);
  449. const grantSet = new Set<PageGrant>();
  450. if (comparableDescendants.isPublicExist) {
  451. grantSet.add(PageGrant.GRANT_PUBLIC);
  452. }
  453. if (comparableDescendants.grantedUserIds.length > 0) {
  454. grantSet.add(PageGrant.GRANT_OWNER);
  455. }
  456. if (comparableDescendants.grantedGroupIds.length > 0) {
  457. grantSet.add(PageGrant.GRANT_USER_GROUP);
  458. }
  459. const descendantPagesGrantInfo = {
  460. grantSet,
  461. grantedUserIds: new Set(comparableDescendants.grantedUserIds), // all only me users of descendant pages
  462. grantedUserGroupIds: new Set(comparableDescendants.grantedGroupIds), // all user groups of descendant pages
  463. };
  464. return this.calcCanOverwriteDescendants(operatorGrantInfo, updateGrantInfo, descendantPagesGrantInfo);
  465. }
  466. async generateUpdateGrantInfoToOverwriteDescendants(operator, updateGrant: PageGrantCanBeOnTree, grantUserGroupId?: ObjectIdLike): Promise<UpdateGrantInfo> {
  467. let updateGrantInfo: UpdateGrantInfo | null = null;
  468. if (updateGrant === PageGrant.GRANT_PUBLIC) {
  469. updateGrantInfo = {
  470. grant: PageGrant.GRANT_PUBLIC,
  471. };
  472. }
  473. else if (updateGrant === PageGrant.GRANT_OWNER) {
  474. updateGrantInfo = {
  475. grant: PageGrant.GRANT_OWNER,
  476. grantedUserId: operator._id,
  477. };
  478. }
  479. else if (updateGrant === PageGrant.GRANT_USER_GROUP) {
  480. if (grantUserGroupId == null) {
  481. throw Error('The parameter `grantUserGroupId` is required.');
  482. }
  483. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  484. const userIds = await UserGroupRelation.findAllUserIdsForUserGroup(grantUserGroupId);
  485. const childrenOrItselfGroups = await UserGroup.findGroupsWithDescendantsById(grantUserGroupId);
  486. const childrenOrItselfGroupIds = childrenOrItselfGroups.map(d => d._id);
  487. updateGrantInfo = {
  488. grant: PageGrant.GRANT_USER_GROUP,
  489. grantedUserGroupInfo: {
  490. groupId: grantUserGroupId,
  491. userIds: new Set<ObjectIdLike>(userIds),
  492. childrenOrItselfGroupIds: new Set<ObjectIdLike>(childrenOrItselfGroupIds),
  493. },
  494. };
  495. }
  496. if (updateGrantInfo == null) {
  497. throw Error('The parameter `updateGrant` must be 1, 4, or 5');
  498. }
  499. return updateGrantInfo;
  500. }
  501. private calcIsAllDescendantsGrantedByOperator(operatorGrantInfo: OperatorGrantInfo, descendantPagesGrantInfo: DescendantPagesGrantInfo): boolean {
  502. if (descendantPagesGrantInfo.grantSet.has(PageGrant.GRANT_OWNER)) {
  503. const isNonApplicableOwnerExist = descendantPagesGrantInfo.grantedUserIds.size >= 2
  504. || !isIncludesObjectId([...descendantPagesGrantInfo.grantedUserIds], operatorGrantInfo.userId);
  505. if (isNonApplicableOwnerExist) {
  506. return false;
  507. }
  508. }
  509. if (descendantPagesGrantInfo.grantSet.has(PageGrant.GRANT_USER_GROUP)) {
  510. const isNonApplicableGroupExist = excludeTestIdsFromTargetIds(
  511. [...descendantPagesGrantInfo.grantedUserGroupIds], [...operatorGrantInfo.userGroupIds],
  512. ).length > 0;
  513. if (isNonApplicableGroupExist) {
  514. return false;
  515. }
  516. }
  517. return true;
  518. }
  519. private calcCanOverwriteDescendants(
  520. operatorGrantInfo: OperatorGrantInfo, updateGrantInfo: UpdateGrantInfo, descendantPagesGrantInfo: DescendantPagesGrantInfo,
  521. ): boolean {
  522. // 1. check is tree GRANTED and it returns true when GRANTED
  523. // - GRANTED is the tree with all pages granted by the operator
  524. const isAllDescendantsGranted = this.calcIsAllDescendantsGrantedByOperator(operatorGrantInfo, descendantPagesGrantInfo);
  525. if (isAllDescendantsGranted) {
  526. return true;
  527. }
  528. // 2. if not 1. then,
  529. // - when update grant is PUBLIC, return true
  530. if (updateGrantInfo.grant === PageGrant.GRANT_PUBLIC) {
  531. return true;
  532. }
  533. // - when update grant is ONLYME, return false
  534. if (updateGrantInfo.grant === PageGrant.GRANT_OWNER) {
  535. return false;
  536. }
  537. // - when update grant is USER_GROUP, return true if meets 2 conditions below
  538. // a. if all descendants user groups are children or itself of update user group
  539. // b. if all descendants grantedUsers belong to update user group
  540. if (updateGrantInfo.grant === PageGrant.GRANT_USER_GROUP) {
  541. const isAllDescendantGroupsChildrenOrItselfOfUpdateGroup = excludeTestIdsFromTargetIds(
  542. [...descendantPagesGrantInfo.grantedUserGroupIds], [...updateGrantInfo.grantedUserGroupInfo.childrenOrItselfGroupIds],
  543. ).length === 0; // a.
  544. const isUpdateGroupUsersIncludeAllDescendantsOwners = excludeTestIdsFromTargetIds(
  545. [...descendantPagesGrantInfo.grantedUserIds], [...updateGrantInfo.grantedUserGroupInfo.userIds],
  546. ).length === 0; // b.
  547. return isAllDescendantGroupsChildrenOrItselfOfUpdateGroup && isUpdateGroupUsersIncludeAllDescendantsOwners;
  548. }
  549. return false;
  550. }
  551. }
  552. export default PageGrantService;