page-grant.ts 34 KB

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