page-grant.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import mongoose from 'mongoose';
  2. import { pagePathUtils } from '@growi/core';
  3. import UserGroup from '~/server/models/user-group';
  4. import { PageModel } from '~/server/models/page';
  5. import { PageQueryBuilder } from '../models/obsolete-page';
  6. import { isIncludesObjectId, removeDuplicates, excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
  7. const { isTopPage } = pagePathUtils;
  8. type ObjectId = mongoose.Types.ObjectId;
  9. type ComparableTarget = {
  10. grant: number,
  11. grantedUserIds: ObjectId[],
  12. grantedGroupId: ObjectId,
  13. applicableGroupIds?: ObjectId[],
  14. };
  15. type ComparableAncestor = {
  16. grant: number,
  17. grantedUserIds: ObjectId[],
  18. applicableUserIds?: ObjectId[],
  19. applicableGroupIds?: ObjectId[],
  20. };
  21. type ComparableDescendants = {
  22. grantedUserIds: ObjectId[],
  23. descendantGroupIds: ObjectId[],
  24. };
  25. class PageGrantService {
  26. crowi!: any;
  27. constructor(crowi: any) {
  28. this.crowi = crowi;
  29. }
  30. private validateComparableTarget(comparable: ComparableTarget) {
  31. const Page = mongoose.model('Page') as PageModel;
  32. const { grant, grantedUserIds, grantedGroupId } = comparable;
  33. if (grant === Page.GRANT_OWNER && (grantedUserIds == null || grantedUserIds.length !== 1)) {
  34. throw Error('grantedUserIds must not be null and must have 1 length');
  35. }
  36. if (grant === Page.GRANT_USER_GROUP && grantedGroupId == null) {
  37. throw Error('grantedGroupId is not specified');
  38. }
  39. }
  40. /**
  41. * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
  42. * @returns boolean
  43. */
  44. private processValidation(target: ComparableTarget, ancestor: ComparableAncestor, descendants?: ComparableDescendants): boolean {
  45. this.validateComparableTarget(target);
  46. const Page = mongoose.model('Page') as PageModel;
  47. /*
  48. * ancestor side
  49. */
  50. // GRANT_PUBLIC
  51. if (ancestor.grant === Page.GRANT_PUBLIC) {
  52. // do nothing
  53. }
  54. // GRANT_OWNER
  55. else if (ancestor.grant === Page.GRANT_OWNER) {
  56. if (target.grant !== Page.GRANT_OWNER) {
  57. return false;
  58. }
  59. if (!ancestor.grantedUserIds[0].equals(target.grantedUserIds[0])) {
  60. return false;
  61. }
  62. }
  63. // GRANT_USER_GROUP
  64. else if (ancestor.grant === Page.GRANT_USER_GROUP) {
  65. if (ancestor.applicableGroupIds == null || ancestor.applicableUserIds == null) {
  66. throw Error('applicableGroupIds and applicableUserIds are not specified');
  67. }
  68. if (target.grant === Page.GRANT_PUBLIC) {
  69. return false;
  70. }
  71. if (target.grant === Page.GRANT_OWNER) {
  72. if (!isIncludesObjectId(ancestor.applicableUserIds, target.grantedUserIds[0])) {
  73. return false;
  74. }
  75. }
  76. if (target.grant === Page.GRANT_USER_GROUP) {
  77. if (!isIncludesObjectId(ancestor.applicableGroupIds, target.grantedGroupId)) {
  78. return false;
  79. }
  80. }
  81. }
  82. if (descendants == null) {
  83. return true;
  84. }
  85. /*
  86. * descendant side
  87. */
  88. // GRANT_PUBLIC
  89. if (target.grant === Page.GRANT_PUBLIC) {
  90. // do nothing
  91. }
  92. // GRANT_OWNER
  93. else if (target.grant === Page.GRANT_OWNER) {
  94. if (descendants.descendantGroupIds.length !== 0 || descendants.grantedUserIds.length > 1) {
  95. return false;
  96. }
  97. if (descendants.grantedUserIds.length === 1 && descendants.grantedUserIds[0].equals(target.grantedGroupId)) {
  98. return false;
  99. }
  100. }
  101. // GRANT_USER_GROUP
  102. else if (target.grant === Page.GRANT_USER_GROUP) {
  103. if (target.applicableGroupIds == null) {
  104. throw Error('applicableGroupIds must not be null');
  105. }
  106. const shouldNotExistIds = excludeTestIdsFromTargetIds(descendants.descendantGroupIds, target.applicableGroupIds);
  107. if (shouldNotExistIds.length !== 0) {
  108. return false;
  109. }
  110. }
  111. return true;
  112. }
  113. /**
  114. * Prepare ComparableTarget
  115. * @returns Promise<ComparableAncestor>
  116. */
  117. private async generateComparableTarget(
  118. grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId, includeApplicable: boolean,
  119. ): Promise<ComparableTarget> {
  120. if (includeApplicable) {
  121. const applicableGroups = grantedGroupId != null ? await UserGroup.findGroupsWithDescendantsById(grantedGroupId) : null;
  122. const applicableGroupIds = applicableGroups?.map(g => g._id) || null;
  123. return {
  124. grant,
  125. grantedUserIds,
  126. grantedGroupId,
  127. applicableGroupIds,
  128. };
  129. }
  130. return {
  131. grant,
  132. grantedUserIds,
  133. grantedGroupId,
  134. };
  135. }
  136. /**
  137. * Prepare ComparableAncestor
  138. * @param targetPath string of the target path
  139. * @returns Promise<ComparableAncestor>
  140. */
  141. private async generateComparableAncestor(targetPath: string): Promise<ComparableAncestor> {
  142. const Page = mongoose.model('Page') as PageModel;
  143. const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
  144. let applicableUserIds: ObjectId[] | undefined;
  145. let applicableGroupIds: ObjectId[] | undefined;
  146. /*
  147. * make granted users list of ancestor's
  148. */
  149. const builderForAncestors = new PageQueryBuilder(Page.find(), false);
  150. const ancestors = await builderForAncestors
  151. .addConditionToListOnlyAncestors(targetPath)
  152. .addConditionToSortPagesByDescPath()
  153. .query
  154. .exec();
  155. const testAncestor = ancestors[0];
  156. if (testAncestor == null) {
  157. throw Error('testAncestor must exist');
  158. }
  159. if (testAncestor.grant === Page.GRANT_USER_GROUP) {
  160. // make a set of all users
  161. const grantedRelations = await UserGroupRelation.find({ relatedGroup: testAncestor.grantedGroup }, { _id: 0, relatedUser: 1 });
  162. const grantedGroups = await UserGroup.findGroupsWithDescendantsById(testAncestor.grantedGroup);
  163. applicableGroupIds = grantedGroups.map(g => g._id);
  164. applicableUserIds = Array.from(new Set(grantedRelations.map(r => r.relatedUser))) as ObjectId[];
  165. }
  166. return {
  167. grant: testAncestor.grant,
  168. grantedUserIds: testAncestor.grantedUsers,
  169. applicableUserIds,
  170. applicableGroupIds,
  171. };
  172. }
  173. /**
  174. * Prepare ComparableDescendants
  175. * @param targetPath string of the target path
  176. * @returns ComparableDescendants
  177. */
  178. private async generateComparableDescendants(targetPath: string): Promise<ComparableDescendants> {
  179. const Page = mongoose.model('Page') as PageModel;
  180. /*
  181. * make granted users list of descendant's
  182. */
  183. // find all descendants excluding empty pages
  184. const builderForDescendants = new PageQueryBuilder(Page.find({}, { _id: 0, grantedUsers: 1, grantedGroup: 1 }), false);
  185. const descendants = await builderForDescendants
  186. .addConditionToListOnlyDescendants(targetPath)
  187. .query
  188. .exec();
  189. let grantedUsersOfGrantOwner: ObjectId[] = []; // users of GRANT_OWNER
  190. const grantedGroups: ObjectId[] = [];
  191. descendants.forEach((d) => {
  192. if (d.grantedUsers != null) {
  193. grantedUsersOfGrantOwner = grantedUsersOfGrantOwner.concat(d.grantedUsers);
  194. }
  195. if (d.grantedGroup != null) {
  196. grantedGroups.push(d.grantedGroup);
  197. }
  198. });
  199. const descendantGroupIds = removeDuplicates(grantedGroups);
  200. return {
  201. grantedUserIds: grantedUsersOfGrantOwner,
  202. descendantGroupIds,
  203. };
  204. }
  205. /**
  206. * About the rule of validation, see: https://dev.growi.org/61b2cdabaa330ce7d8152844
  207. * @returns Promise<boolean>
  208. */
  209. async isGrantNormalized(targetPath: string, grant, grantedUserIds: ObjectId[], grantedGroupId: ObjectId, shouldCheckDescendants = false): Promise<boolean> {
  210. if (isTopPage(targetPath)) {
  211. return true;
  212. }
  213. const comparableAncestor = await this.generateComparableAncestor(targetPath);
  214. if (!shouldCheckDescendants) { // checking the parent is enough
  215. const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupId, false);
  216. return this.processValidation(comparableTarget, comparableAncestor);
  217. }
  218. const comparableTarget = await this.generateComparableTarget(grant, grantedUserIds, grantedGroupId, true);
  219. const comparableDescendants = await this.generateComparableDescendants(targetPath);
  220. return this.processValidation(comparableTarget, comparableAncestor, comparableDescendants);
  221. }
  222. }
  223. export default PageGrantService;