page-grant.ts 38 KB

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