page-grant.ts 32 KB

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