page-grant.ts 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  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. 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. previousGrantedGroupIds?: IGrantedGroup[],
  448. ): Promise<boolean> {
  449. if (isTopPage(targetPath)) {
  450. return true;
  451. }
  452. if (previousGrantedGroupIds != null) {
  453. const isGrantChangeable = await this.validateGrantChange(user, previousGrantedGroupIds, grant, grantedGroupIds);
  454. if (!isGrantChangeable) {
  455. return false;
  456. }
  457. }
  458. const comparableAncestor = await this.generateComparableAncestor(targetPath, includeNotMigratedPages);
  459. if (!shouldCheckDescendants) { // checking the parent is enough
  460. const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupIds, false);
  461. return this.validateGrant(comparableTarget, comparableAncestor);
  462. }
  463. const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupIds, true);
  464. const comparableDescendants = await this.generateComparableDescendants(targetPath, user, includeNotMigratedPages);
  465. return this.validateGrant(comparableTarget, comparableAncestor, comparableDescendants);
  466. }
  467. /**
  468. * Separate normalizable pages and NOT normalizable pages by PageService.prototype.isGrantNormalized method.
  469. * normalizable pages = Pages which are able to run normalizeParentRecursively method (grant & userGroup rule is correct)
  470. * @param pageIds pageIds to be tested
  471. * @returns a tuple with the first element of normalizable pages and the second element of NOT normalizable pages
  472. */
  473. async separateNormalizableAndNotNormalizablePages(user, pages): Promise<[(PageDocument & { _id: any })[], (PageDocument & { _id: any })[]]> {
  474. if (pages.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
  475. throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
  476. }
  477. const shouldCheckDescendants = true;
  478. const shouldIncludeNotMigratedPages = true;
  479. const normalizable: (PageDocument & { _id: any })[] = [];
  480. const nonNormalizable: (PageDocument & { _id: any })[] = []; // can be used to tell user which page failed to migrate
  481. for await (const page of pages) {
  482. const {
  483. path, grant, grantedUsers: grantedUserIds, grantedGroups: grantedGroupIds,
  484. } = page;
  485. if (!pageUtils.isPageNormalized(page)) {
  486. nonNormalizable.push(page);
  487. continue;
  488. }
  489. if (await this.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupIds, shouldCheckDescendants, shouldIncludeNotMigratedPages)) {
  490. normalizable.push(page);
  491. }
  492. else {
  493. nonNormalizable.push(page);
  494. }
  495. }
  496. return [normalizable, nonNormalizable];
  497. }
  498. async calcApplicableGrantData(page, user): Promise<IRecordApplicableGrant> {
  499. const Page = mongoose.model('Page') as unknown as PageModel;
  500. // -- Public only if top page
  501. const isOnlyPublicApplicable = isTopPage(page.path);
  502. if (isOnlyPublicApplicable) {
  503. return {
  504. [PageGrant.GRANT_PUBLIC]: null,
  505. };
  506. }
  507. // Increment an object (type IRecordApplicableGrant)
  508. // grant is never public, anyone with the link, nor specified
  509. const data: IRecordApplicableGrant = {
  510. [PageGrant.GRANT_RESTRICTED]: null, // any page can be restricted
  511. };
  512. const userRelatedGroups = await this.getUserRelatedGroups(user);
  513. // -- Any grant is allowed if parent is null
  514. const isAnyGrantApplicable = page.parent == null;
  515. if (isAnyGrantApplicable) {
  516. data[PageGrant.GRANT_PUBLIC] = null;
  517. data[PageGrant.GRANT_OWNER] = null;
  518. data[PageGrant.GRANT_USER_GROUP] = { applicableGroups: userRelatedGroups };
  519. return data;
  520. }
  521. const parent = await Page.findById(page.parent);
  522. if (parent == null) {
  523. throw Error('The page\'s parent does not exist.');
  524. }
  525. const {
  526. grant, grantedUsers, grantedGroups,
  527. } = parent;
  528. if (grant === PageGrant.GRANT_PUBLIC) {
  529. data[PageGrant.GRANT_PUBLIC] = null;
  530. data[PageGrant.GRANT_OWNER] = null;
  531. data[PageGrant.GRANT_USER_GROUP] = { applicableGroups: userRelatedGroups };
  532. }
  533. else if (grant === PageGrant.GRANT_OWNER) {
  534. const grantedUser = grantedUsers[0];
  535. const isUserApplicable = grantedUser.toString() === user._id.toString();
  536. if (isUserApplicable) {
  537. data[PageGrant.GRANT_OWNER] = null;
  538. }
  539. }
  540. else if (grant === PageGrant.GRANT_USER_GROUP) {
  541. const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantedGroups);
  542. const targetUserGroups = await UserGroup.find({ _id: { $in: grantedUserGroupIds } });
  543. const targetExternalUserGroups = await ExternalUserGroup.find({ _id: { $in: grantedExternalUserGroupIds } });
  544. if (targetUserGroups.length === 0 && targetExternalUserGroups.length === 0) {
  545. throw Error('Group not found to calculate grant data.');
  546. }
  547. const isUserExistInUserGroup = (await Promise.all(targetUserGroups.map((group) => {
  548. return UserGroupRelation.countByGroupIdsAndUser([group._id], user);
  549. }))).some(count => count > 0);
  550. const isUserExistInExternalUserGroup = (await Promise.all(targetExternalUserGroups.map((group) => {
  551. return ExternalUserGroupRelation.countByGroupIdsAndUser([group._id], user);
  552. }))).some(count => count > 0);
  553. const isUserExistInGroup = isUserExistInUserGroup || isUserExistInExternalUserGroup;
  554. if (isUserExistInGroup) {
  555. data[PageGrant.GRANT_OWNER] = null;
  556. }
  557. const applicableUserGroups = (await Promise.all(targetUserGroups.map((group) => {
  558. return UserGroupRelation.findGroupsWithDescendantsByGroupAndUser(group, user);
  559. }))).flat();
  560. const applicableExternalUserGroups = (await Promise.all(targetExternalUserGroups.map((group) => {
  561. return ExternalUserGroupRelation.findGroupsWithDescendantsByGroupAndUser(group, user);
  562. }))).flat();
  563. const applicableGroups = [
  564. ...applicableUserGroups.map((group) => {
  565. return { type: GroupType.userGroup, item: group };
  566. }),
  567. ...applicableExternalUserGroups.map((group) => {
  568. return { type: GroupType.externalUserGroup, item: group };
  569. }),
  570. ];
  571. data[PageGrant.GRANT_USER_GROUP] = { applicableGroups };
  572. }
  573. return data;
  574. }
  575. /*
  576. * get all groups that user is related to
  577. */
  578. async getUserRelatedGroups(user): Promise<PopulatedGrantedGroup[]> {
  579. const userRelatedUserGroups = await UserGroupRelation.findAllGroupsForUser(user);
  580. const userRelatedExternalUserGroups = await ExternalUserGroupRelation.findAllGroupsForUser(user);
  581. return [
  582. ...userRelatedUserGroups.map((group) => {
  583. return { type: GroupType.userGroup, item: group };
  584. }),
  585. ...userRelatedExternalUserGroups.map((group) => {
  586. return { type: GroupType.externalUserGroup, item: group };
  587. }),
  588. ];
  589. }
  590. /*
  591. * get all groups of Page that user is related to
  592. */
  593. async getUserRelatedGrantedGroups(page: PageDocument, user): Promise<IGrantedGroup[]> {
  594. const userRelatedGroups = (await this.getUserRelatedGroups(user));
  595. return this.getUserRelatedGrantedGroupsSyncronously(userRelatedGroups, page);
  596. }
  597. /**
  598. * Use when you do not want to use getUserRelatedGrantedGroups with async/await (e.g inside loops that process a large amount of pages)
  599. * Specification of userRelatedGroups is necessary to avoid the cost of fetching userRelatedGroups from DB every time.
  600. */
  601. getUserRelatedGrantedGroupsSyncronously(userRelatedGroups: PopulatedGrantedGroup[], page: PageDocument): IGrantedGroup[] {
  602. const userRelatedGroupIds: string[] = userRelatedGroups.map(ug => ug.item._id.toString());
  603. return page.grantedGroups?.filter((group) => {
  604. if (isPopulated(group.item)) {
  605. return userRelatedGroupIds.includes(group.item._id.toString());
  606. }
  607. return userRelatedGroupIds.includes(group.item);
  608. }) || [];
  609. }
  610. /**
  611. * Check if user is granted access to page
  612. */
  613. isUserGrantedPageAccess(page: PageDocument, user, userRelatedGroups: PopulatedGrantedGroup[]): boolean {
  614. if (page.grant === PageGrant.GRANT_PUBLIC) return true;
  615. if (page.grant === PageGrant.GRANT_OWNER) return page.grantedUsers?.includes(user._id.toString()) ?? false;
  616. if (page.grant === PageGrant.GRANT_USER_GROUP) return this.getUserRelatedGrantedGroupsSyncronously(userRelatedGroups, page).length > 0;
  617. return false;
  618. }
  619. /**
  620. * see: https://dev.growi.org/635a314eac6bcd85cbf359fc
  621. * @param {string} targetPath
  622. * @param operator
  623. * @param {UpdateGrantInfo} updateGrantInfo
  624. * @returns {Promise<boolean>}
  625. */
  626. async canOverwriteDescendants(targetPath: string, operator: { _id: ObjectIdLike }, updateGrantInfo: UpdateGrantInfo): Promise<boolean> {
  627. const relatedGroupIds = [
  628. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(operator)),
  629. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(operator)),
  630. ];
  631. const operatorGrantInfo = {
  632. userId: operator._id,
  633. userGroupIds: new Set<ObjectIdLike>(relatedGroupIds),
  634. };
  635. const comparableDescendants = await this.generateComparableDescendants(targetPath, operator);
  636. const grantSet = new Set<PageGrant>();
  637. if (comparableDescendants.isPublicExist) {
  638. grantSet.add(PageGrant.GRANT_PUBLIC);
  639. }
  640. if (comparableDescendants.grantedUserIds.length > 0) {
  641. grantSet.add(PageGrant.GRANT_OWNER);
  642. }
  643. if (comparableDescendants.grantedGroupIds.length > 0) {
  644. grantSet.add(PageGrant.GRANT_USER_GROUP);
  645. }
  646. const descendantPagesGrantInfo = {
  647. grantSet,
  648. grantedUserIds: new Set(comparableDescendants.grantedUserIds), // all only me users of descendant pages
  649. grantedUserGroupIds: new Set(comparableDescendants.grantedGroupIds.map((g) => {
  650. return typeof g.item === 'string' ? g.item : g.item._id;
  651. })), // all user groups of descendant pages
  652. };
  653. return this.calcCanOverwriteDescendants(operatorGrantInfo, updateGrantInfo, descendantPagesGrantInfo);
  654. }
  655. async generateUpdateGrantInfoToOverwriteDescendants(
  656. operator, updateGrant?: PageGrant, grantGroupIds?: IGrantedGroup[],
  657. ): Promise<UpdateGrantInfo> {
  658. let updateGrantInfo: UpdateGrantInfo | null = null;
  659. if (updateGrant === PageGrant.GRANT_PUBLIC) {
  660. updateGrantInfo = {
  661. grant: PageGrant.GRANT_PUBLIC,
  662. };
  663. }
  664. else if (updateGrant === PageGrant.GRANT_OWNER) {
  665. updateGrantInfo = {
  666. grant: PageGrant.GRANT_OWNER,
  667. grantedUserId: operator._id,
  668. };
  669. }
  670. else if (updateGrant === PageGrant.GRANT_USER_GROUP) {
  671. if (grantGroupIds == null) {
  672. throw Error('The parameter `grantGroupIds` is required.');
  673. }
  674. const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantGroupIds);
  675. const userGroupUserIds = await UserGroupRelation.findAllUserIdsForUserGroups(grantedUserGroupIds);
  676. const externalUserGroupUserIds = await ExternalUserGroupRelation.findAllUserIdsForUserGroups(grantedExternalUserGroupIds);
  677. const userIds = [...userGroupUserIds, ...externalUserGroupUserIds];
  678. const childrenOrItselfUserGroups = (await Promise.all(grantedUserGroupIds.map((groupId) => {
  679. return UserGroup.findGroupsWithDescendantsById(groupId);
  680. }))).flat();
  681. const childrenOrItselfExternalUserGroups = (await Promise.all(grantedExternalUserGroupIds.map((groupId) => {
  682. return ExternalUserGroup.findGroupsWithDescendantsById(groupId);
  683. }))).flat();
  684. const childrenOrItselfGroups = [...childrenOrItselfUserGroups, ...childrenOrItselfExternalUserGroups];
  685. const childrenOrItselfGroupIds = childrenOrItselfGroups.map(d => d._id);
  686. updateGrantInfo = {
  687. grant: PageGrant.GRANT_USER_GROUP,
  688. grantedUserGroupInfo: {
  689. userIds: new Set<ObjectIdLike>(userIds),
  690. childrenOrItselfGroupIds: new Set<ObjectIdLike>(childrenOrItselfGroupIds),
  691. },
  692. };
  693. }
  694. if (updateGrantInfo == null) {
  695. // Neither pages with grant `GRANT_RESTRICTED` nor `GRANT_SPECIFIED` can be on a page tree.
  696. throw Error('The parameter `updateGrant` must be 1, 4, or 5');
  697. }
  698. return updateGrantInfo;
  699. }
  700. private calcIsAllDescendantsGrantedByOperator(operatorGrantInfo: OperatorGrantInfo, descendantPagesGrantInfo: DescendantPagesGrantInfo): boolean {
  701. if (descendantPagesGrantInfo.grantSet.has(PageGrant.GRANT_OWNER)) {
  702. const isNonApplicableOwnerExist = descendantPagesGrantInfo.grantedUserIds.size >= 2
  703. || !includesObjectIds([...descendantPagesGrantInfo.grantedUserIds], [operatorGrantInfo.userId]);
  704. if (isNonApplicableOwnerExist) {
  705. return false;
  706. }
  707. }
  708. if (descendantPagesGrantInfo.grantSet.has(PageGrant.GRANT_USER_GROUP)) {
  709. const isNonApplicableGroupExist = excludeTestIdsFromTargetIds(
  710. [...descendantPagesGrantInfo.grantedUserGroupIds], [...operatorGrantInfo.userGroupIds],
  711. ).length > 0;
  712. if (isNonApplicableGroupExist) {
  713. return false;
  714. }
  715. }
  716. return true;
  717. }
  718. private calcCanOverwriteDescendants(
  719. operatorGrantInfo: OperatorGrantInfo, updateGrantInfo: UpdateGrantInfo, descendantPagesGrantInfo: DescendantPagesGrantInfo,
  720. ): boolean {
  721. // 1. check is tree GRANTED and it returns true when GRANTED
  722. // - GRANTED is the tree with all pages granted by the operator
  723. const isAllDescendantsGranted = this.calcIsAllDescendantsGrantedByOperator(operatorGrantInfo, descendantPagesGrantInfo);
  724. if (isAllDescendantsGranted) {
  725. return true;
  726. }
  727. // 2. if not 1. then,
  728. // - when update grant is PUBLIC, return true
  729. if (updateGrantInfo.grant === PageGrant.GRANT_PUBLIC) {
  730. return true;
  731. }
  732. // - when update grant is ONLYME, return false
  733. if (updateGrantInfo.grant === PageGrant.GRANT_OWNER) {
  734. return false;
  735. }
  736. // - when update grant is USER_GROUP, return true if meets 2 conditions below
  737. // a. if all descendants user groups are children or itself of update user group
  738. // b. if all descendants grantedUsers belong to update user group
  739. if (updateGrantInfo.grant === PageGrant.GRANT_USER_GROUP) {
  740. const isAllDescendantGroupsChildrenOrItselfOfUpdateGroup = excludeTestIdsFromTargetIds(
  741. [...descendantPagesGrantInfo.grantedUserGroupIds], [...updateGrantInfo.grantedUserGroupInfo.childrenOrItselfGroupIds],
  742. ).length === 0; // a.
  743. const isUpdateGroupUsersIncludeAllDescendantsOwners = excludeTestIdsFromTargetIds(
  744. [...descendantPagesGrantInfo.grantedUserIds], [...updateGrantInfo.grantedUserGroupInfo.userIds],
  745. ).length === 0; // b.
  746. return isAllDescendantGroupsChildrenOrItselfOfUpdateGroup && isUpdateGroupUsersIncludeAllDescendantsOwners;
  747. }
  748. return false;
  749. }
  750. }
  751. export default PageGrantService;