diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index d859c5e08b..eddd4291d1 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -24,6 +24,10 @@ import { LdapVendor, } from './vendors'; +export interface SearchCallback { + (entry: SearchEntry): void; +} + /** * Basic wrapper for the ldapjs library. * @@ -110,66 +114,50 @@ export class LdapClient { } /** - * Performs an LDAP search operation, creates a generator to limit memory usage + * Performs an LDAP search operation, calls a function on each entry to limit memory usage * * @param dn The fully qualified base DN to search within * @param options The search options + * @param f The callback to call on each search entry */ - *searchStreaming( + async searchStreaming( dn: string, options: SearchOptions, - ): IterableIterator { - let queueSize = 25; - if (options) { - if ( - options.paged && - typeof options.paged === 'object' && - typeof options.paged.pageSize === 'number' && - options.paged.pageSize > 0 - ) { - queueSize = options.paged.pageSize; - } - } - const queue: SearchEntry[] = new Array(queueSize); - let done = false; + f: SearchCallback, + ): Promise { try { - this.client.search(dn, options, (err, res) => { - if (err) { - throw new Error(errorString(err)); - } - - res.on('searchReference', () => { - throw new Error('Unable to handle referral'); - }); - - res.on('searchEntry', entry => { - queue.push(entry); - }); - - res.on('error', e => { - throw new Error(errorString(e)); - }); - - res.on('end', r => { - if (!r) { - throw new Error('Null response'); - } else if (r.status !== 0) { - throw new Error(`Got status ${r.status}: ${r.errorMessage}`); - } else { - done = true; + return await new Promise((resolve, reject) => { + this.client.search(dn, options, (err, res) => { + if (err) { + reject(new Error(errorString(err))); } + + res.on('searchReference', () => { + reject(new Error('Unable to handle referral')); + }); + + res.on('searchEntry', async entry => { + await f(entry); + }); + + res.on('error', e => { + reject(new Error(errorString(e))); + }); + + res.on('end', r => { + if (!r) { + throw new Error('Null response'); + } else if (r.status !== 0) { + throw new Error(`Got status ${r.status}: ${r.errorMessage}`); + } else { + resolve(); + } + }); }); }); } catch (e) { throw new Error(`LDAP search at DN "${dn}" failed, ${e.message}`); } - while (!done && queue.length > 0) { - const res = queue.pop(); - if (!res) { - continue; - } - yield res; - } } /** diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts index b357b6b408..3615d8b2ad 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -67,7 +67,7 @@ function searchEntry( describe('readLdapUsers', () => { const client: jest.Mocked = { - search: jest.fn(), + searchStreaming: jest.fn(), getVendor: jest.fn(), } as any; @@ -75,18 +75,25 @@ describe('readLdapUsers', () => { it('transfers all attributes from a default ldap vendor', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.search.mockResolvedValue([ - searchEntry({ - uid: ['uid-value'], - description: ['description-value'], - cn: ['cn-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - entryDN: ['dn-value'], - entryUUID: ['uuid-value'], - }), - ]); + client.searchStreaming.mockImplementation( + async (_dn, _opts, fn): Promise => { + return await new Promise((resolve, _reject) => { + fn( + searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + entryDN: ['dn-value'], + entryUUID: ['uuid-value'], + }), + ); + resolve(); + }); + }, + ); const config: UserConfig = { dn: 'ddd', options: {}, @@ -129,37 +136,45 @@ describe('readLdapUsers', () => { it('transfers all attributes from Microsoft Active Directory', async () => { client.getVendor.mockResolvedValue(ActiveDirectoryVendor); - client.search.mockResolvedValue([ - searchEntry({ - uid: ['uid-value'], - description: ['description-value'], - cn: ['cn-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - distinguishedName: ['dn-value'], - objectGUID: [ - Buffer.from([ - 68, - 2, - 125, - 190, - 209, - 0, - 94, - 73, - 133, - 33, - 230, - 174, - 234, - 195, - 160, - 152, - ]), - ], - }), - ]); + client.searchStreaming.mockImplementation( + async (_dn, _opts, fn): Promise => { + return await new Promise((resolve, _reject) => { + fn( + searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + distinguishedName: ['dn-value'], + objectGUID: [ + Buffer.from([ + 68, + 2, + 125, + 190, + 209, + 0, + 94, + 73, + 133, + 33, + 230, + 174, + 234, + 195, + 160, + 152, + ]), + ], + }), + ); + resolve(); + }); + }, + ); + const config: UserConfig = { dn: 'ddd', options: {}, @@ -203,7 +218,7 @@ describe('readLdapUsers', () => { describe('readLdapGroups', () => { const client: jest.Mocked = { - search: jest.fn(), + searchStreaming: jest.fn(), getVendor: jest.fn(), } as any; @@ -211,19 +226,26 @@ describe('readLdapGroups', () => { it('transfers all attributes from a default ldap vendor', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.search.mockResolvedValue([ - searchEntry({ - cn: ['cn-value'], - description: ['description-value'], - tt: ['type-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - member: ['e', 'f', 'g'], - entryDN: ['dn-value'], - entryUUID: ['uuid-value'], - }), - ]); + client.searchStreaming.mockImplementation( + async (_dn, _opts, fn): Promise => { + return await new Promise((resolve, _reject) => { + fn( + searchEntry({ + cn: ['cn-value'], + description: ['description-value'], + tt: ['type-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + member: ['e', 'f', 'g'], + entryDN: ['dn-value'], + entryUUID: ['uuid-value'], + }), + ); + resolve(); + }); + }, + ); const config: GroupConfig = { dn: 'ddd', options: {}, @@ -274,38 +296,45 @@ describe('readLdapGroups', () => { }); it('transfers all attributes from Microsoft Active Directory', async () => { client.getVendor.mockResolvedValue(ActiveDirectoryVendor); - client.search.mockResolvedValue([ - searchEntry({ - cn: ['cn-value'], - description: ['description-value'], - tt: ['type-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - member: ['e', 'f', 'g'], - distinguishedName: ['dn-value'], - objectGUID: [ - Buffer.from([ - 68, - 2, - 125, - 190, - 209, - 0, - 94, - 73, - 133, - 33, - 230, - 174, - 234, - 195, - 160, - 152, - ]), - ], - }), - ]); + client.searchStreaming.mockImplementation( + async (_dn, _opts, fn): Promise => { + return await new Promise((resolve, _reject) => { + fn( + searchEntry({ + cn: ['cn-value'], + description: ['description-value'], + tt: ['type-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + member: ['e', 'f', 'g'], + distinguishedName: ['dn-value'], + objectGUID: [ + Buffer.from([ + 68, + 2, + 125, + 190, + 209, + 0, + 94, + 73, + 133, + 33, + 230, + 174, + 234, + 195, + 160, + 152, + ]), + ], + }), + ); + resolve(); + }); + }, + ); const config: GroupConfig = { dn: 'ddd', options: {}, diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index 96a9966628..37e69c5bcf 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -107,24 +107,19 @@ export async function readLdapUsers( const transformer = opts?.transformer ?? defaultUserTransformer; - const entries = client.searchStreaming(dn, options); - for (const entry of entries) { - if (!entry) { - continue; - } - - const entity = await transformer(vendor, config, entry); + await client.searchStreaming(dn, options, async user => { + const entity = await transformer(vendor, config, user); if (!entity) { - continue; + return; } - mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { + mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => { ensureItems(userMemberOf, myDn, vs); }); entities.push(entity); - } + }); return { users: entities, userMemberOf }; } @@ -213,17 +208,15 @@ export async function readLdapGroups( const transformer = opts?.transformer ?? defaultGroupTransformer; - const entries = client.searchStreaming(dn, options); - - for (const entry of entries) { + await client.searchStreaming(dn, options, async entry => { if (!entry) { - continue; + return; } const entity = await transformer(vendor, config, entry); if (!entity) { - continue; + return; } mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { @@ -234,7 +227,7 @@ export async function readLdapGroups( }); groups.push(entity); - } + }); return { groups,