Explorar o código

fix large ldap group sync error

Futa Arai %!s(int64=2) %!d(string=hai) anos
pai
achega
0b22326bbf

+ 7 - 1
apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts

@@ -1,6 +1,7 @@
 import { configManager } from '~/server/service/config-manager';
 import LdapService, { SearchResultEntry } from '~/server/service/ldap';
 import PassportService from '~/server/service/passport';
+import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 import {
@@ -9,10 +10,12 @@ import {
 
 import ExternalUserGroupSyncService from './external-user-group-sync';
 
+const logger = loggerFactory('growi:service:ldap-user-sync-service');
+
 // When d = max depth of group trees
 // Max space complexity of generateExternalUserGroupTrees will be:
 // O(TREES_BATCH_SIZE * d * USERS_BATCH_SIZE)
-const TREES_BATCH_SIZE = 10;
+const TREES_BATCH_SIZE = 30;
 const USERS_BATCH_SIZE = 30;
 
 class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
@@ -37,9 +40,11 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
 
     let groupEntries: SearchResultEntry[];
     try {
+      await this.ldapService.bind();
       groupEntries = await this.ldapService.searchGroupDir();
     }
     catch (e) {
+      logger.error(e.message);
       throw Error('external_user_group.ldap.group_search_failed');
     }
 
@@ -123,6 +128,7 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
       userEntries = await getUserEntries();
     }
     catch (e) {
+      logger.error(e.message);
       throw Error('external_user_group.ldap.user_search_failed');
     }
 

+ 40 - 29
apps/app/src/server/service/ldap.ts

@@ -27,20 +27,34 @@ class LdapService {
 
   password?: string; // Necessary when bind type is user bind
 
+  client: ldap.Client;
+
+  searchBase: string;
+
   constructor(username?: string, password?: string) {
+    const serverUrl = configManager?.getConfig('crowi', 'security:passport-ldap:serverUrl');
+
     this.username = username;
     this.password = password;
+
+    // parse serverUrl
+    // see: https://regex101.com/r/0tuYBB/1
+    const match = serverUrl.match(/(ldaps?:\/\/[^/]+)\/(.*)?/);
+    if (match == null || match.length < 1) {
+      const urlInvalidMessage = 'serverUrl is invalid';
+      logger.error(urlInvalidMessage);
+      throw new Error(urlInvalidMessage);
+    }
+    const url = match[1];
+    this.searchBase = match[2] || '';
+
+    this.client = ldap.createClient({
+      url,
+    });
   }
 
-  /**
-   * Execute search on LDAP server and return result
-   * @param {string} filter Search filter
-   * @param {string} base Base DN to execute search on
-   * @returns {SearchEntry[]} Search result. Default scope is set to 'sub'.
-   */
-  search(filter?: string, base?: string, scope: 'sub' | 'base' | 'one' = 'sub'): Promise<SearchResultEntry[]> {
+  bind(): Promise<void> {
     const isLdapEnabled = configManager?.getConfig('crowi', 'security:passport-ldap:isEnabled');
-
     if (!isLdapEnabled) {
       const notEnabledMessage = 'LDAP is not enabled';
       logger.error(notEnabledMessage);
@@ -49,41 +63,38 @@ class LdapService {
 
     // get configurations
     const isUserBind = configManager?.getConfig('crowi', 'security:passport-ldap:isUserBind');
-    const serverUrl = configManager?.getConfig('crowi', 'security:passport-ldap:serverUrl');
     const bindDN = configManager?.getConfig('crowi', 'security:passport-ldap:bindDN');
     const bindCredentials = configManager?.getConfig('crowi', 'security:passport-ldap:bindDNPassword');
 
-    // parse serverUrl
-    // see: https://regex101.com/r/0tuYBB/1
-    const match = serverUrl.match(/(ldaps?:\/\/[^/]+)\/(.*)?/);
-    if (match == null || match.length < 1) {
-      const urlInvalidMessage = 'serverUrl is invalid';
-      logger.error(urlInvalidMessage);
-      throw new Error(urlInvalidMessage);
-    }
-    const url = match[1];
-    const searchBase = match[2] || '';
-
     // user bind
     const fixedBindDN = (isUserBind)
       ? bindDN.replace(/{{username}}/, this.username)
       : bindDN;
     const fixedBindCredentials = (isUserBind) ? this.password : bindCredentials;
 
-    const client = ldap.createClient({
-      url,
-    });
-
-    const searchResults: SearchResultEntry[] = [];
-
-    return new Promise((resolve, reject) => {
-      client.bind(fixedBindDN, fixedBindCredentials, (err) => {
+    return new Promise<void>((resolve, reject) => {
+      this.client.bind(fixedBindDN, fixedBindCredentials, (err) => {
         if (err != null) {
           reject(err);
         }
+        resolve();
       });
+    });
+  }
 
-      client.search(base || searchBase, { scope, filter }, (err, res) => {
+  /**
+   * Execute search on LDAP server and return result
+   * @param {string} filter Search filter
+   * @param {string} base Base DN to execute search on
+   * @returns {SearchEntry[]} Search result. Default scope is set to 'sub'.
+   */
+  search(filter?: string, base?: string, scope: 'sub' | 'base' | 'one' = 'sub'): Promise<SearchResultEntry[]> {
+    const searchResults: SearchResultEntry[] = [];
+
+    return new Promise((resolve, reject) => {
+      this.client.search(base || this.searchBase, {
+        scope, filter, paged: true, sizeLimit: 200,
+      }, (err, res) => {
         if (err != null) {
           reject(err);
         }