ldap-user-group-sync.ts 7.1 KB

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