page-grant.ts 28 KB

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