page-grant.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  1. import type { IPage } from '@growi/core';
  2. import {
  3. type IGrantedGroup,
  4. PageGrant, GroupType, getIdForRef, isPopulated,
  5. } from '@growi/core';
  6. import {
  7. pagePathUtils, pathUtils, pageUtils,
  8. } from '@growi/core/dist/utils';
  9. import escapeStringRegexp from 'escape-string-regexp';
  10. import mongoose from 'mongoose';
  11. import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
  12. import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
  13. import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
  14. import type { UserRelatedGroupsData } from '~/interfaces/page';
  15. import { UserGroupPageGrantStatus, type GroupGrantData } from '~/interfaces/page';
  16. import type { IRecordApplicableGrant, PopulatedGrantedGroup } from '~/interfaces/page-grant';
  17. import type { PageDocument, PageModel } from '~/server/models/page';
  18. import UserGroup from '~/server/models/user-group';
  19. import { includesObjectIds, excludeTestIdsFromTargetIds, hasIntersection } from '~/server/util/compare-objectId';
  20. import type { ObjectIdLike } from '../interfaces/mongoose-utils';
  21. import UserGroupRelation from '../models/user-group-relation';
  22. import { divideByType } from '../util/granted-group';
  23. const { addTrailingSlash } = pathUtils;
  24. const { isTopPage } = pagePathUtils;
  25. const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
  26. type ComparableTarget = {
  27. grant?: number,
  28. grantedUserIds?: ObjectIdLike[],
  29. grantedGroupIds?: IGrantedGroup[],
  30. applicableUserIds?: ObjectIdLike[],
  31. applicableGroupIds?: ObjectIdLike[],
  32. };
  33. type ComparableAncestor = {
  34. grant: number,
  35. grantedUserIds: ObjectIdLike[],
  36. applicableUserIds?: ObjectIdLike[],
  37. applicableGroupIds?: ObjectIdLike[],
  38. };
  39. type ComparableDescendants = {
  40. isPublicExist: boolean,
  41. grantedUserIds: ObjectIdLike[],
  42. grantedGroupIds: IGrantedGroup[],
  43. };
  44. /**
  45. * @param grantedUserGroupInfo This parameter has info to calculate whether the update operation is allowed.
  46. * - See the `calcCanOverwriteDescendants` private method for detail.
  47. */
  48. type UpdateGrantInfo = {
  49. grant: typeof PageGrant.GRANT_PUBLIC,
  50. } | {
  51. grant: typeof PageGrant.GRANT_OWNER,
  52. grantedUserId: ObjectIdLike,
  53. } | {
  54. grant: typeof PageGrant.GRANT_USER_GROUP,
  55. grantedUserGroupInfo: {
  56. userIds: Set<ObjectIdLike>,
  57. childrenOrItselfGroupIds: Set<ObjectIdLike>,
  58. },
  59. };
  60. type DescendantPagesGrantInfo = {
  61. grantSet: Set<number>,
  62. grantedUserIds: Set<ObjectIdLike>, // all only me users of descendant pages
  63. grantedUserGroupIds: Set<ObjectIdLike>, // all user groups of descendant pages
  64. };
  65. /**
  66. * @param {ObjectIdLike} userId The _id of the operator.
  67. * @param {Set<ObjectIdLike>} userGroupIds The Set of the _id of the user groups that the operator belongs.
  68. */
  69. type OperatorGrantInfo = {
  70. userId: ObjectIdLike,
  71. userGroupIds: Set<ObjectIdLike>,
  72. };
  73. export interface IPageGrantService {
  74. isGrantNormalized: (
  75. user,
  76. targetPath: string,
  77. grant?: PageGrant,
  78. grantedUserIds?: ObjectIdLike[],
  79. grantedGroupIds?: IGrantedGroup[],
  80. shouldCheckDescendants?: boolean,
  81. includeNotMigratedPages?: boolean,
  82. previousGrantedGroupIds?: IGrantedGroup[]
  83. ) => Promise<boolean>,
  84. separateNormalizableAndNotNormalizablePages: (user, pages) => Promise<[(PageDocument & { _id: any })[], (PageDocument & { _id: any })[]]>,
  85. generateUpdateGrantInfoToOverwriteDescendants: (
  86. operator, updateGrant?: PageGrant, grantGroupIds?: IGrantedGroup[],
  87. ) => Promise<UpdateGrantInfo>,
  88. canOverwriteDescendants: (targetPath: string, operator: { _id: ObjectIdLike }, updateGrantInfo: UpdateGrantInfo) => Promise<boolean>,
  89. validateGrantChange: (user, previousGrantedGroupIds: IGrantedGroup[], grant?: PageGrant, grantedGroupIds?: IGrantedGroup[]) => Promise<boolean>,
  90. validateGrantChangeSyncronously:(
  91. userRelatedGroups: PopulatedGrantedGroup[], previousGrantedGroups: IGrantedGroup[], grant?: PageGrant, grantedGroups?: IGrantedGroup[],
  92. ) => boolean,
  93. getUserRelatedGroups: (user) => Promise<PopulatedGrantedGroup[]>,
  94. getPopulatedGrantedGroups: (grantedGroups: IGrantedGroup[]) => Promise<PopulatedGrantedGroup[]>,
  95. getUserRelatedGrantedGroups: (page: PageDocument, user) => Promise<IGrantedGroup[]>,
  96. getUserRelatedGrantedGroupsSyncronously: (userRelatedGroups: PopulatedGrantedGroup[], page: PageDocument) => IGrantedGroup[],
  97. isUserGrantedPageAccess: (page: PageDocument, user, userRelatedGroups: PopulatedGrantedGroup[]) => boolean,
  98. getPageGroupGrantData: (page: PageDocument, user) => Promise<GroupGrantData>,
  99. calcApplicableGrantData: (page, user) => Promise<IRecordApplicableGrant>
  100. }
  101. class PageGrantService implements IPageGrantService {
  102. crowi!: any;
  103. constructor(crowi: any) {
  104. this.crowi = crowi;
  105. }
  106. private validateComparableTarget(comparable: ComparableTarget) {
  107. const Page = mongoose.model<IPage, PageModel>('Page');
  108. const { grant, grantedUserIds, grantedGroupIds } = comparable;
  109. if (grant === Page.GRANT_OWNER && (grantedUserIds == null || grantedUserIds.length !== 1)) {
  110. throw Error('grantedUserIds must not be null and must have 1 length');
  111. }
  112. if (grant === Page.GRANT_USER_GROUP && grantedGroupIds == null) {
  113. throw Error('grantedGroupIds is not specified');
  114. }
  115. }
  116. /**
  117. * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
  118. * @returns boolean
  119. */
  120. private validateGrant(target: ComparableTarget, ancestor: ComparableAncestor, descendants?: ComparableDescendants): boolean {
  121. /*
  122. * the page itself
  123. */
  124. this.validateComparableTarget(target);
  125. const Page = mongoose.model<IPage, PageModel>('Page');
  126. /*
  127. * ancestor side
  128. */
  129. // GRANT_PUBLIC
  130. if (ancestor.grant === Page.GRANT_PUBLIC) { // any page can exist under public page
  131. // do nothing
  132. }
  133. // GRANT_OWNER
  134. else if (ancestor.grant === Page.GRANT_OWNER) {
  135. if (target.grantedUserIds?.length !== 1) {
  136. return false;
  137. }
  138. if (target.grant !== Page.GRANT_OWNER) { // only GRANT_OWNER page can exist under GRANT_OWNER page
  139. return false;
  140. }
  141. if (ancestor.grantedUserIds[0].toString() !== target.grantedUserIds[0].toString()) { // the grantedUser must be the same as parent's under the GRANT_OWNER page
  142. return false;
  143. }
  144. }
  145. // GRANT_USER_GROUP
  146. else if (ancestor.grant === Page.GRANT_USER_GROUP) {
  147. if (ancestor.applicableGroupIds == null || ancestor.applicableUserIds == null) {
  148. throw Error('applicableGroupIds and applicableUserIds are not specified');
  149. }
  150. if (target.grant === Page.GRANT_PUBLIC) { // public page must not exist under GRANT_USER_GROUP page
  151. return false;
  152. }
  153. if (target.grant === Page.GRANT_OWNER) {
  154. if (target.grantedUserIds?.length !== 1) {
  155. throw Error('grantedUserIds must have one user');
  156. }
  157. 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
  158. return false;
  159. }
  160. }
  161. if (target.grant === Page.GRANT_USER_GROUP) {
  162. if (target.grantedGroupIds == null || target.grantedGroupIds.length === 0) {
  163. throw Error('grantedGroupId must not be empty');
  164. }
  165. const targetGrantedGroupStrIds = target.grantedGroupIds.map(e => (typeof e.item === 'string' ? e.item : e.item._id));
  166. if (!includesObjectIds(ancestor.applicableGroupIds, targetGrantedGroupStrIds)) { // only child groups or the same group can exist under GRANT_USER_GROUP page
  167. return false;
  168. }
  169. }
  170. }
  171. if (descendants == null) {
  172. return true;
  173. }
  174. /*
  175. * descendant side
  176. */
  177. // GRANT_PUBLIC
  178. if (target.grant === Page.GRANT_PUBLIC) { // any page can exist under public page
  179. // do nothing
  180. }
  181. // GRANT_OWNER
  182. else if (target.grant === Page.GRANT_OWNER) {
  183. if (target.grantedUserIds?.length !== 1) {
  184. throw Error('grantedUserIds must have one user');
  185. }
  186. if (descendants.isPublicExist) { // public page must not exist under GRANT_OWNER page
  187. return false;
  188. }
  189. if (descendants.grantedGroupIds.length !== 0 || descendants.grantedUserIds.length > 1) { // groups or more than 2 grantedUsers must not be in descendants
  190. return false;
  191. }
  192. 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
  193. return false;
  194. }
  195. }
  196. // GRANT_USER_GROUP
  197. else if (target.grant === Page.GRANT_USER_GROUP) {
  198. if (target.applicableGroupIds == null || target.applicableUserIds == null) {
  199. throw Error('applicableGroupIds and applicableUserIds must not be null');
  200. }
  201. if (descendants.isPublicExist) { // public page must not exist under GRANT_USER_GROUP page
  202. return false;
  203. }
  204. const shouldNotExistGroupIds = excludeTestIdsFromTargetIds(descendants.grantedGroupIds.map(g => g.item), target.applicableGroupIds);
  205. const shouldNotExistUserIds = excludeTestIdsFromTargetIds(descendants.grantedUserIds, target.applicableUserIds);
  206. if (shouldNotExistGroupIds.length !== 0 || shouldNotExistUserIds.length !== 0) {
  207. return false;
  208. }
  209. }
  210. return true;
  211. }
  212. /**
  213. * Validate if page grant can be changed from prior grant to specified grant.
  214. * Necessary for pages with multiple group grant.
  215. * 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
  216. * @param user The user who is changing the grant
  217. * @param previousGrantedGroups The groups that were granted priorly
  218. * @param grant The grant to be changed to
  219. * @param grantedGroups The groups to be granted
  220. */
  221. async validateGrantChange(user, previousGrantedGroups: IGrantedGroup[], grant?: PageGrant, grantedGroups?: IGrantedGroup[]): Promise<boolean> {
  222. const userRelatedGroups = await this.getUserRelatedGroups(user);
  223. return this.validateGrantChangeSyncronously(userRelatedGroups, previousGrantedGroups, grant, grantedGroups);
  224. }
  225. /**
  226. * Use when you do not want to use validateGrantChange with async/await (e.g inside loops that process a large amount of pages)
  227. * Specification of userRelatedGroups is necessary to avoid the cost of fetching userRelatedGroups from DB every time.
  228. */
  229. validateGrantChangeSyncronously(
  230. userRelatedGroups: PopulatedGrantedGroup[],
  231. previousGrantedGroups: IGrantedGroup[],
  232. grant?: PageGrant,
  233. grantedGroups?: IGrantedGroup[],
  234. ): boolean {
  235. const userRelatedGroupIds = userRelatedGroups.map(g => g.item._id);
  236. const userBelongsToAllPreviousGrantedGroups = excludeTestIdsFromTargetIds(
  237. previousGrantedGroups.map(g => getIdForRef(g.item)),
  238. userRelatedGroupIds,
  239. ).length === 0;
  240. if (!userBelongsToAllPreviousGrantedGroups) {
  241. if (grant !== PageGrant.GRANT_USER_GROUP) {
  242. return false;
  243. }
  244. const pageGrantIncludesUserRelatedGroup = hasIntersection(grantedGroups?.map(g => getIdForRef(g.item)) || [], userRelatedGroupIds);
  245. if (!pageGrantIncludesUserRelatedGroup) {
  246. return false;
  247. }
  248. }
  249. return true;
  250. }
  251. /**
  252. * Prepare ComparableTarget
  253. * @returns Promise<ComparableAncestor>
  254. */
  255. private async generateComparableTargetWithApplicableData(
  256. grant: PageGrant | undefined, grantedUserIds: ObjectIdLike[] | undefined, grantedGroupIds: IGrantedGroup[] | undefined,
  257. ): Promise<ComparableTarget> {
  258. const Page = mongoose.model<IPage, PageModel>('Page');
  259. let applicableUserIds: ObjectIdLike[] | undefined;
  260. let applicableGroupIds: ObjectIdLike[] | undefined;
  261. if (grant === Page.GRANT_USER_GROUP) {
  262. if (grantedGroupIds == null || grantedGroupIds.length === 0) {
  263. throw Error('Target user group is not given');
  264. }
  265. const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantedGroupIds);
  266. const targetUserGroups = await UserGroup.find({ _id: { $in: grantedUserGroupIds } });
  267. const targetExternalUserGroups = await ExternalUserGroup.find({ _id: { $in: grantedExternalUserGroupIds } });
  268. if (targetUserGroups.length === 0 && targetExternalUserGroups.length === 0) {
  269. throw Error('Target user group does not exist');
  270. }
  271. const userGroupRelations = await UserGroupRelation.find({ relatedGroup: { $in: targetUserGroups.map(g => g._id) } });
  272. const externalUserGroupRelations = await ExternalUserGroupRelation.find({ relatedGroup: { $in: targetExternalUserGroups.map(g => g._id) } });
  273. applicableUserIds = Array.from(new Set([...userGroupRelations, ...externalUserGroupRelations].map(u => u.relatedUser as ObjectIdLike)));
  274. const applicableUserGroups = (await Promise.all(targetUserGroups.map((group) => {
  275. return UserGroup.findGroupsWithDescendantsById(group._id);
  276. }))).flat();
  277. const applicableExternalUserGroups = (await Promise.all(targetExternalUserGroups.map((group) => {
  278. return ExternalUserGroup.findGroupsWithDescendantsById(group._id);
  279. }))).flat();
  280. applicableGroupIds = [...applicableUserGroups, ...applicableExternalUserGroups].map(g => g._id);
  281. }
  282. return {
  283. grant,
  284. grantedUserIds,
  285. grantedGroupIds,
  286. applicableUserIds,
  287. applicableGroupIds,
  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<IPage, PageModel>('Page');
  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<IPage, PageModel>('Page');
  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: ComparableTarget = { grant, grantedUserIds, grantedGroupIds };
  454. return this.validateGrant(comparableTarget, comparableAncestor);
  455. }
  456. const comparableTarget = await this.generateComparableTargetWithApplicableData(grant, grantedUserIds, grantedGroupIds);
  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<IPage, PageModel>('Page');
  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 the group grant data of page.
  570. * To calculate if a group can be granted to page, the same logic as isGrantNormalized will be executed, except only the ancestor info will be used.
  571. */
  572. async getPageGroupGrantData(page: PageDocument, user): Promise<GroupGrantData> {
  573. if (isTopPage(page.path)) {
  574. return { userRelatedGroups: [], nonUserRelatedGrantedGroups: [] };
  575. }
  576. const userRelatedGroups = await this.getUserRelatedGroups(user);
  577. let userRelatedGroupsData: UserRelatedGroupsData[] = userRelatedGroups.map((group) => {
  578. const provider = group.type === GroupType.externalUserGroup ? group.item.provider : undefined;
  579. return {
  580. // default status as notGranted
  581. id: group.item._id.toString(), name: group.item.name, type: group.type, provider, status: UserGroupPageGrantStatus.notGranted,
  582. };
  583. });
  584. const nonUserRelatedGrantedGroups: {
  585. id: string,
  586. name: string,
  587. type: GroupType,
  588. provider?: ExternalGroupProviderType,
  589. }[] = [];
  590. const populatedGrantedGroups = await this.getPopulatedGrantedGroups(page.grantedGroups);
  591. // Set the status of user-related granted groups as isGranted
  592. // Append non-user-related granted groups to nonUserRelatedGrantedGroups
  593. populatedGrantedGroups.forEach((group) => {
  594. const userRelatedGrantedGroup = userRelatedGroupsData.find((userRelatedGroup) => {
  595. return userRelatedGroup.id === group.item._id.toString();
  596. });
  597. if (userRelatedGrantedGroup != null) {
  598. userRelatedGrantedGroup.status = UserGroupPageGrantStatus.isGranted;
  599. }
  600. else {
  601. const provider = group.type === GroupType.externalUserGroup ? group.item.provider : undefined;
  602. nonUserRelatedGrantedGroups.push({
  603. id: group.item._id.toString(), name: group.item.name, type: group.type, provider,
  604. });
  605. }
  606. });
  607. // Check if group can be granted to page for non-granted groups
  608. const grantedUserIds = page.grantedUsers?.map(user => getIdForRef(user)) ?? [];
  609. const comparableAncestor = await this.generateComparableAncestor(page.path, false);
  610. userRelatedGroupsData = userRelatedGroupsData.map((groupData) => {
  611. if (groupData.status === UserGroupPageGrantStatus.isGranted) {
  612. return groupData;
  613. }
  614. const groupsToGrant = [...(page.grantedGroups ?? []), { item: groupData.id, type: groupData.type }];
  615. const comparableTarget: ComparableTarget = {
  616. grant: PageGrant.GRANT_USER_GROUP,
  617. grantedUserIds,
  618. grantedGroupIds: groupsToGrant,
  619. };
  620. const status = this.validateGrant(comparableTarget, comparableAncestor) ? UserGroupPageGrantStatus.notGranted : UserGroupPageGrantStatus.cannotGrant;
  621. return { ...groupData, status };
  622. });
  623. const statusPriority = {
  624. [UserGroupPageGrantStatus.notGranted]: 0,
  625. [UserGroupPageGrantStatus.isGranted]: 1,
  626. [UserGroupPageGrantStatus.cannotGrant]: 2,
  627. };
  628. userRelatedGroupsData.sort((a, b) => statusPriority[a.status] - statusPriority[b.status]);
  629. return { userRelatedGroups: userRelatedGroupsData, nonUserRelatedGrantedGroups };
  630. }
  631. /*
  632. * get all groups that user is related to
  633. */
  634. async getUserRelatedGroups(user): Promise<PopulatedGrantedGroup[]> {
  635. const userRelatedUserGroups = await UserGroupRelation.findAllGroupsForUser(user);
  636. const userRelatedExternalUserGroups = await ExternalUserGroupRelation.findAllGroupsForUser(user);
  637. return [
  638. ...userRelatedUserGroups.map((group) => {
  639. return { type: GroupType.userGroup, item: group };
  640. }),
  641. ...userRelatedExternalUserGroups.map((group) => {
  642. return { type: GroupType.externalUserGroup, item: group };
  643. }),
  644. ];
  645. }
  646. async getPopulatedGrantedGroups(grantedGroups: IGrantedGroup[]): Promise<PopulatedGrantedGroup[]> {
  647. const { grantedUserGroups, grantedExternalUserGroups } = divideByType(grantedGroups);
  648. const userGroupDocuments = await UserGroup.find({ _id: { $in: grantedUserGroups } });
  649. const externalUserGroupDocuments = await ExternalUserGroup.find({ _id: { $in: grantedExternalUserGroups } });
  650. return [
  651. ...(userGroupDocuments.map((group) => {
  652. return { type: GroupType.userGroup, item: group };
  653. })),
  654. ...(externalUserGroupDocuments.map((group) => {
  655. return { type: GroupType.externalUserGroup, item: group };
  656. })),
  657. ];
  658. }
  659. /*
  660. * get all groups of Page that user is related to
  661. */
  662. async getUserRelatedGrantedGroups(page: PageDocument, user): Promise<IGrantedGroup[]> {
  663. const userRelatedGroups = (await this.getUserRelatedGroups(user));
  664. return this.getUserRelatedGrantedGroupsSyncronously(userRelatedGroups, page);
  665. }
  666. /**
  667. * Use when you do not want to use getUserRelatedGrantedGroups with async/await (e.g inside loops that process a large amount of pages)
  668. * Specification of userRelatedGroups is necessary to avoid the cost of fetching userRelatedGroups from DB every time.
  669. */
  670. getUserRelatedGrantedGroupsSyncronously(userRelatedGroups: PopulatedGrantedGroup[], page: PageDocument): IGrantedGroup[] {
  671. const userRelatedGroupIds: string[] = userRelatedGroups.map(ug => ug.item._id.toString());
  672. return page.grantedGroups?.filter((group) => {
  673. if (isPopulated(group.item)) {
  674. return userRelatedGroupIds.includes(group.item._id.toString());
  675. }
  676. return userRelatedGroupIds.includes(group.item);
  677. }) || [];
  678. }
  679. /**
  680. * Check if user is granted access to page
  681. */
  682. isUserGrantedPageAccess(page: PageDocument, user, userRelatedGroups: PopulatedGrantedGroup[]): boolean {
  683. if (page.grant === PageGrant.GRANT_PUBLIC) return true;
  684. if (page.grant === PageGrant.GRANT_OWNER) return page.grantedUsers?.includes(user._id.toString()) ?? false;
  685. if (page.grant === PageGrant.GRANT_USER_GROUP) return this.getUserRelatedGrantedGroupsSyncronously(userRelatedGroups, page).length > 0;
  686. return false;
  687. }
  688. /**
  689. * see: https://dev.growi.org/635a314eac6bcd85cbf359fc
  690. * @param {string} targetPath
  691. * @param operator
  692. * @param {UpdateGrantInfo} updateGrantInfo
  693. * @returns {Promise<boolean>}
  694. */
  695. async canOverwriteDescendants(targetPath: string, operator: { _id: ObjectIdLike }, updateGrantInfo: UpdateGrantInfo): Promise<boolean> {
  696. const relatedGroupIds = [
  697. ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(operator)),
  698. ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(operator)),
  699. ];
  700. const operatorGrantInfo = {
  701. userId: operator._id,
  702. userGroupIds: new Set<ObjectIdLike>(relatedGroupIds),
  703. };
  704. const comparableDescendants = await this.generateComparableDescendants(targetPath, operator);
  705. const grantSet = new Set<PageGrant>();
  706. if (comparableDescendants.isPublicExist) {
  707. grantSet.add(PageGrant.GRANT_PUBLIC);
  708. }
  709. if (comparableDescendants.grantedUserIds.length > 0) {
  710. grantSet.add(PageGrant.GRANT_OWNER);
  711. }
  712. if (comparableDescendants.grantedGroupIds.length > 0) {
  713. grantSet.add(PageGrant.GRANT_USER_GROUP);
  714. }
  715. const descendantPagesGrantInfo = {
  716. grantSet,
  717. grantedUserIds: new Set(comparableDescendants.grantedUserIds), // all only me users of descendant pages
  718. grantedUserGroupIds: new Set(comparableDescendants.grantedGroupIds.map((g) => {
  719. return typeof g.item === 'string' ? g.item : g.item._id;
  720. })), // all user groups of descendant pages
  721. };
  722. return this.calcCanOverwriteDescendants(operatorGrantInfo, updateGrantInfo, descendantPagesGrantInfo);
  723. }
  724. async generateUpdateGrantInfoToOverwriteDescendants(
  725. operator, updateGrant?: PageGrant, grantGroupIds?: IGrantedGroup[],
  726. ): Promise<UpdateGrantInfo> {
  727. let updateGrantInfo: UpdateGrantInfo | null = null;
  728. if (updateGrant === PageGrant.GRANT_PUBLIC) {
  729. updateGrantInfo = {
  730. grant: PageGrant.GRANT_PUBLIC,
  731. };
  732. }
  733. else if (updateGrant === PageGrant.GRANT_OWNER) {
  734. updateGrantInfo = {
  735. grant: PageGrant.GRANT_OWNER,
  736. grantedUserId: operator._id,
  737. };
  738. }
  739. else if (updateGrant === PageGrant.GRANT_USER_GROUP) {
  740. if (grantGroupIds == null) {
  741. throw Error('The parameter `grantGroupIds` is required.');
  742. }
  743. const { grantedUserGroups: grantedUserGroupIds, grantedExternalUserGroups: grantedExternalUserGroupIds } = divideByType(grantGroupIds);
  744. const userGroupUserIds = await UserGroupRelation.findAllUserIdsForUserGroups(grantedUserGroupIds);
  745. const externalUserGroupUserIds = await ExternalUserGroupRelation.findAllUserIdsForUserGroups(grantedExternalUserGroupIds);
  746. const userIds = [...userGroupUserIds, ...externalUserGroupUserIds];
  747. const childrenOrItselfUserGroups = (await Promise.all(grantedUserGroupIds.map((groupId) => {
  748. return UserGroup.findGroupsWithDescendantsById(groupId);
  749. }))).flat();
  750. const childrenOrItselfExternalUserGroups = (await Promise.all(grantedExternalUserGroupIds.map((groupId) => {
  751. return ExternalUserGroup.findGroupsWithDescendantsById(groupId);
  752. }))).flat();
  753. const childrenOrItselfGroups = [...childrenOrItselfUserGroups, ...childrenOrItselfExternalUserGroups];
  754. const childrenOrItselfGroupIds = childrenOrItselfGroups.map(d => d._id);
  755. updateGrantInfo = {
  756. grant: PageGrant.GRANT_USER_GROUP,
  757. grantedUserGroupInfo: {
  758. userIds: new Set<ObjectIdLike>(userIds),
  759. childrenOrItselfGroupIds: new Set<ObjectIdLike>(childrenOrItselfGroupIds),
  760. },
  761. };
  762. }
  763. if (updateGrantInfo == null) {
  764. // Neither pages with grant `GRANT_RESTRICTED` nor `GRANT_SPECIFIED` can be on a page tree.
  765. throw Error('The parameter `updateGrant` must be 1, 4, or 5');
  766. }
  767. return updateGrantInfo;
  768. }
  769. private calcIsAllDescendantsGrantedByOperator(operatorGrantInfo: OperatorGrantInfo, descendantPagesGrantInfo: DescendantPagesGrantInfo): boolean {
  770. if (descendantPagesGrantInfo.grantSet.has(PageGrant.GRANT_OWNER)) {
  771. const isNonApplicableOwnerExist = descendantPagesGrantInfo.grantedUserIds.size >= 2
  772. || !includesObjectIds([...descendantPagesGrantInfo.grantedUserIds], [operatorGrantInfo.userId]);
  773. if (isNonApplicableOwnerExist) {
  774. return false;
  775. }
  776. }
  777. if (descendantPagesGrantInfo.grantSet.has(PageGrant.GRANT_USER_GROUP)) {
  778. const isNonApplicableGroupExist = excludeTestIdsFromTargetIds(
  779. [...descendantPagesGrantInfo.grantedUserGroupIds], [...operatorGrantInfo.userGroupIds],
  780. ).length > 0;
  781. if (isNonApplicableGroupExist) {
  782. return false;
  783. }
  784. }
  785. return true;
  786. }
  787. private calcCanOverwriteDescendants(
  788. operatorGrantInfo: OperatorGrantInfo, updateGrantInfo: UpdateGrantInfo, descendantPagesGrantInfo: DescendantPagesGrantInfo,
  789. ): boolean {
  790. // 1. check is tree GRANTED and it returns true when GRANTED
  791. // - GRANTED is the tree with all pages granted by the operator
  792. const isAllDescendantsGranted = this.calcIsAllDescendantsGrantedByOperator(operatorGrantInfo, descendantPagesGrantInfo);
  793. if (isAllDescendantsGranted) {
  794. return true;
  795. }
  796. // 2. if not 1. then,
  797. // - when update grant is PUBLIC, return true
  798. if (updateGrantInfo.grant === PageGrant.GRANT_PUBLIC) {
  799. return true;
  800. }
  801. // - when update grant is ONLYME, return false
  802. if (updateGrantInfo.grant === PageGrant.GRANT_OWNER) {
  803. return false;
  804. }
  805. // - when update grant is USER_GROUP, return true if meets 2 conditions below
  806. // a. if all descendants user groups are children or itself of update user group
  807. // b. if all descendants grantedUsers belong to update user group
  808. if (updateGrantInfo.grant === PageGrant.GRANT_USER_GROUP) {
  809. const isAllDescendantGroupsChildrenOrItselfOfUpdateGroup = excludeTestIdsFromTargetIds(
  810. [...descendantPagesGrantInfo.grantedUserGroupIds], [...updateGrantInfo.grantedUserGroupInfo.childrenOrItselfGroupIds],
  811. ).length === 0; // a.
  812. const isUpdateGroupUsersIncludeAllDescendantsOwners = excludeTestIdsFromTargetIds(
  813. [...descendantPagesGrantInfo.grantedUserIds], [...updateGrantInfo.grantedUserGroupInfo.userIds],
  814. ).length === 0; // b.
  815. return isAllDescendantGroupsChildrenOrItselfOfUpdateGroup && isUpdateGroupUsersIncludeAllDescendantsOwners;
  816. }
  817. return false;
  818. }
  819. }
  820. export default PageGrantService;