external-user-group-sync.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { IUserHasId } from '~/interfaces/user';
  2. import ExternalAccount from '~/server/models/external-account';
  3. import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
  4. import { batchProcessPromiseAll } from '~/utils/promise';
  5. import { configManager } from '../../../../server/service/config-manager';
  6. import { externalAccountService } from '../../../../server/service/external-account';
  7. import {
  8. ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, IExternalUserGroupHasId,
  9. } from '../../interfaces/external-user-group';
  10. import ExternalUserGroup from '../models/external-user-group';
  11. import ExternalUserGroupRelation from '../models/external-user-group-relation';
  12. // When d = max depth of group trees
  13. // Max space complexity of syncExternalUserGroups will be:
  14. // O(TREES_BATCH_SIZE * d * USERS_BATCH_SIZE)
  15. const TREES_BATCH_SIZE = 10;
  16. const USERS_BATCH_SIZE = 30;
  17. abstract class ExternalUserGroupSyncService {
  18. groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
  19. authProviderType: string; // auth provider type (e.g: ldap, oidc)
  20. constructor(groupProviderType: ExternalGroupProviderType, authProviderType: string) {
  21. this.groupProviderType = groupProviderType;
  22. this.authProviderType = authProviderType;
  23. }
  24. /** External user group tree sync method
  25. * 1. Generate external user group tree
  26. * 2. Use createUpdateExternalUserGroup on each node in the tree using DFS
  27. * 3. If preserveDeletedLDAPGroups is false、delete all ExternalUserGroups that were not found during tree search
  28. */
  29. async syncExternalUserGroups(): Promise<void> {
  30. const trees = await this.generateExternalUserGroupTrees();
  31. const existingExternalUserGroupIds: string[] = [];
  32. const syncNode = async(node: ExternalUserGroupTreeNode, parentId?: string) => {
  33. const externalUserGroup = await this.createUpdateExternalUserGroup(node, parentId);
  34. existingExternalUserGroupIds.push(externalUserGroup._id);
  35. // Do not use Promise.all, because the number of promises processed can
  36. // exponentially grow when group tree is enormous
  37. for await (const childNode of node.childGroupNodes) {
  38. await syncNode(childNode, externalUserGroup._id);
  39. }
  40. };
  41. await batchProcessPromiseAll(trees, TREES_BATCH_SIZE, (root) => {
  42. return syncNode(root);
  43. });
  44. const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
  45. if (!preserveDeletedLdapGroups) {
  46. await ExternalUserGroup.deleteMany({ _id: { $nin: existingExternalUserGroupIds }, groupProviderType: this.groupProviderType });
  47. await ExternalUserGroupRelation.removeAllInvalidRelations();
  48. }
  49. }
  50. /** External user group node sync method
  51. * 1. Create/Update ExternalUserGroup from using information of ExternalUserGroupTreeNode
  52. * 2. For every element in node.userInfos, call getMemberUser and create an ExternalUserGroupRelation with ExternalUserGroup if it does not have one
  53. * 3. Retrun ExternalUserGroup
  54. * @param {string} node Node of external group tree
  55. * @param {string} parentId Parent group id (id in GROWI) of the group we wan't to create/update
  56. * @returns {Promise<IExternalUserGroupHasId>} ExternalUserGroup that was created/updated
  57. */
  58. async createUpdateExternalUserGroup(node: ExternalUserGroupTreeNode, parentId?: string): Promise<IExternalUserGroupHasId> {
  59. const externalUserGroup = await ExternalUserGroup.findAndUpdateOrCreateGroup(
  60. node.name, node.id, this.groupProviderType, node.description, parentId,
  61. );
  62. await batchProcessPromiseAll(node.userInfos, USERS_BATCH_SIZE, async(userInfo) => {
  63. const user = await this.getMemberUser(userInfo);
  64. if (user != null) {
  65. const userGroups = await ExternalUserGroup.findGroupsWithAncestorsRecursively(externalUserGroup);
  66. const userGroupIds = userGroups.map(g => g._id);
  67. // remove existing relations from list to create
  68. const existingRelations = await ExternalUserGroupRelation.find({ relatedGroup: { $in: userGroupIds }, relatedUser: user._id });
  69. const existingGroupIds = existingRelations.map(r => r.relatedGroup.toString());
  70. const groupIdsToCreateRelation = excludeTestIdsFromTargetIds(userGroupIds, existingGroupIds);
  71. await ExternalUserGroupRelation.createRelations(groupIdsToCreateRelation, user);
  72. }
  73. });
  74. return externalUserGroup;
  75. }
  76. /** Method to get group member GROWI user
  77. * 1. Search for GROWI user based on user info of 1, and return user
  78. * 2. If autoGenerateUserOnHogeGroupSync is true and GROWI user is not found, create new GROWI user
  79. * @param {ExternalUserInfo} externalUserInfo Search external app/server using this identifier
  80. * @returns {Promise<IUserHasId | null>} User when found or created, null when neither
  81. */
  82. async getMemberUser(userInfo: ExternalUserInfo): Promise<IUserHasId | null> {
  83. const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
  84. const getExternalAccount = async() => {
  85. if (autoGenerateUserOnGroupSync && externalAccountService != null) {
  86. return externalAccountService.getOrCreateUser({
  87. id: userInfo.id, username: userInfo.username, name: userInfo.name, email: userInfo.email,
  88. }, this.authProviderType);
  89. }
  90. return ExternalAccount.findOne({ providerType: this.groupProviderType, accountId: userInfo.id });
  91. };
  92. const externalAccount = await getExternalAccount();
  93. if (externalAccount != null) {
  94. return (await externalAccount.populate<{user: IUserHasId | null}>('user')).user;
  95. }
  96. return null;
  97. }
  98. /** Method to generate external group tree structure
  99. * 1. Fetch user group info from external app/server
  100. * 2. Convert each group tree structure to ExternalUserGroupTreeNode
  101. * 3. Return the root node of each tree
  102. */
  103. abstract generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]>
  104. }
  105. export default ExternalUserGroupSyncService;