ldap-user-group-sync.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { configManager } from '~/server/service/config-manager';
  2. import { ldapService, SearchResultEntry } from '~/server/service/ldap';
  3. import PassportService from '~/server/service/passport';
  4. import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
  5. import loggerFactory from '~/utils/logger';
  6. import { batchProcessPromiseAll } from '~/utils/promise';
  7. import {
  8. ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, LdapGroupMembershipAttributeType,
  9. } from '../../interfaces/external-user-group';
  10. import ExternalUserGroupSyncService from './external-user-group-sync';
  11. const logger = loggerFactory('growi:service:ldap-user-group-sync-service');
  12. // When d = max depth of group trees
  13. // Max space complexity of generateExternalUserGroupTrees will be:
  14. // O(TREES_BATCH_SIZE * d * USERS_BATCH_SIZE)
  15. const TREES_BATCH_SIZE = 10;
  16. const USERS_BATCH_SIZE = 30;
  17. export class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
  18. passportService: PassportService;
  19. isInitialized = false;
  20. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  21. constructor(passportService: PassportService, s2sMessagingService: S2sMessagingService, socketIoService) {
  22. super(ExternalGroupProviderType.ldap, s2sMessagingService, socketIoService);
  23. this.authProviderType = 'ldap';
  24. this.passportService = passportService;
  25. }
  26. async init(userBindUsername?: string, userBindPassword?: string): Promise<void> {
  27. await ldapService.initClient(userBindUsername, userBindPassword);
  28. this.isInitialized = true;
  29. }
  30. override syncExternalUserGroups(): Promise<void> {
  31. if (!this.isInitialized) {
  32. const msg = 'Service not initialized';
  33. logger.error(msg);
  34. throw new Error(msg);
  35. }
  36. return super.syncExternalUserGroups();
  37. }
  38. override async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
  39. const groupChildGroupAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute');
  40. const groupMembershipAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute');
  41. const groupNameAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute');
  42. const groupDescriptionAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute');
  43. const groupBase: string = ldapService.getGroupSearchBase();
  44. const groupEntries = await ldapService.searchGroupDir();
  45. const getChildGroupDnsFromGroupEntry = (groupEntry: SearchResultEntry) => {
  46. // groupChildGroupAttribute and groupMembershipAttribute may be the same,
  47. // so filter values of groupChildGroupAttribute to ones that include groupBase
  48. return ldapService.getArrayValFromSearchResultEntry(groupEntry, groupChildGroupAttribute).filter(attr => attr.includes(groupBase));
  49. };
  50. const getUserIdsFromGroupEntry = (groupEntry: SearchResultEntry) => {
  51. // groupChildGroupAttribute and groupMembershipAttribute may be the same,
  52. // so filter values of groupMembershipAttribute to ones that does not include groupBase
  53. return ldapService.getArrayValFromSearchResultEntry(groupEntry, groupMembershipAttribute).filter(attr => !attr.includes(groupBase));
  54. };
  55. const convert = async(entry: SearchResultEntry, converted: string[]): Promise<ExternalUserGroupTreeNode | null> => {
  56. const name = ldapService.getStringValFromSearchResultEntry(entry, groupNameAttribute);
  57. if (name == null) return null;
  58. if (converted.includes(entry.objectName)) {
  59. throw Error('Circular reference inside LDAP group tree');
  60. }
  61. converted.push(entry.objectName);
  62. const userIds = getUserIdsFromGroupEntry(entry);
  63. const userInfos = (await batchProcessPromiseAll(userIds, USERS_BATCH_SIZE, (id) => {
  64. return this.getUserInfo(id);
  65. })).filter((info): info is NonNullable<ExternalUserInfo> => info != null);
  66. const description = ldapService.getStringValFromSearchResultEntry(entry, groupDescriptionAttribute);
  67. const childGroupDNs = getChildGroupDnsFromGroupEntry(entry);
  68. const childGroupNodesWithNull: (ExternalUserGroupTreeNode | null)[] = [];
  69. // Do not use Promise.all, because the number of promises processed can
  70. // exponentially grow when group tree is enormous
  71. for await (const dn of childGroupDNs) {
  72. const childEntry = groupEntries.find(ge => ge.objectName === dn);
  73. childGroupNodesWithNull.push(childEntry != null ? await convert(childEntry, converted) : null);
  74. }
  75. const childGroupNodes: ExternalUserGroupTreeNode[] = childGroupNodesWithNull
  76. .filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
  77. return {
  78. id: entry.objectName,
  79. userInfos,
  80. childGroupNodes,
  81. name,
  82. description,
  83. };
  84. };
  85. // all the DNs of groups that are not a root of a tree
  86. const allChildGroupDNs = new Set(groupEntries.flatMap((entry) => {
  87. return getChildGroupDnsFromGroupEntry(entry);
  88. }));
  89. // root of every tree
  90. const rootEntries = groupEntries.filter((entry) => {
  91. return !allChildGroupDNs.has(entry.objectName);
  92. });
  93. return (await batchProcessPromiseAll(rootEntries, TREES_BATCH_SIZE, entry => convert(entry, [])))
  94. .filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
  95. }
  96. private async getUserInfo(userId: string): Promise<ExternalUserInfo | null> {
  97. const groupMembershipAttributeType = configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType');
  98. const attrMapUsername = this.passportService.getLdapAttrNameMappedToUsername();
  99. const attrMapName = this.passportService.getLdapAttrNameMappedToName();
  100. const attrMapMail = this.passportService.getLdapAttrNameMappedToMail();
  101. // get full user info from LDAP server using externalUserInfo (DN or UID)
  102. const getUserEntries = async() => {
  103. if (groupMembershipAttributeType === LdapGroupMembershipAttributeType.dn) {
  104. return ldapService.search(undefined, userId, 'base');
  105. }
  106. if (groupMembershipAttributeType === LdapGroupMembershipAttributeType.uid) {
  107. return ldapService.search(`(uid=${userId})`, undefined);
  108. }
  109. };
  110. const userEntries = await getUserEntries();
  111. if (userEntries != null && userEntries.length > 0) {
  112. const userEntry = userEntries[0];
  113. const uid = ldapService.getStringValFromSearchResultEntry(userEntry, 'uid');
  114. if (uid != null) {
  115. const usernameToBeRegistered = attrMapUsername === 'uid' ? uid : ldapService.getStringValFromSearchResultEntry(userEntry, attrMapUsername);
  116. const nameToBeRegistered = ldapService.getStringValFromSearchResultEntry(userEntry, attrMapName);
  117. const mailToBeRegistered = ldapService.getStringValFromSearchResultEntry(userEntry, attrMapMail);
  118. return usernameToBeRegistered != null ? {
  119. id: uid,
  120. username: usernameToBeRegistered,
  121. name: nameToBeRegistered,
  122. email: mailToBeRegistered,
  123. } : null;
  124. }
  125. }
  126. return null;
  127. }
  128. }