diff --git a/.changeset/heavy-cars-wash.md b/.changeset/heavy-cars-wash.md new file mode 100644 index 0000000000..87da5a6d95 --- /dev/null +++ b/.changeset/heavy-cars-wash.md @@ -0,0 +1,70 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +Moved from `ldapjs` dependency to `ldapts` + +### Breaking Changes + +**Type Migration** + +Custom transformers must now accept `Entry` from ldapts instead of `SearchEntry` +from ldapjs The Entry type provides direct property access without need for +`.object()` or `.raw()` methods. + +If you have custom user or group transformers, update the signature from: + +```typescript +(vendor: LdapVendor, config: UserConfig, entry: SearchEntry) => + Promise; +``` + +to + +```typescript +(vendor: LdapVendor, config: UserConfig, entry: Entry) => + Promise; +``` + +**Search Options** + +Updated LDAP search configuration `typesOnly: false` → `attributeValues: true` +This inverts the boolean logic: ldapjs used negative form while ldapts uses +positive form. Both achieve the same result: retrieving attribute values rather +than just attribute names. + +Update LDAP search options in configuration from + +```yaml +options: + typesOnly: false +``` + +to + +```yaml +options: + attributeValues: true +``` + +**API Changes** Removed `LdapClient.searchStreaming()` method. Users should +migrate to `LdapClient.search()` instead + +If you're using `searchStreaming` directly: + +```typescript +// Before +await client.searchStreaming(dn, options, async entry => { + // process each entry +}); + +// After +const entries = await client.search(dn, options); +for (const entry of entries) { + // process each entry +} +``` + +> **_NOTE:_**: Both methods have always loaded all entries into memory. The +> searchStreaming method was only needed internally to handle ldapjs's +> event-based API. diff --git a/plugins/catalog-backend-module-ldap/config.d.ts b/plugins/catalog-backend-module-ldap/config.d.ts index f9b49eabee..6e9eb7b837 100644 --- a/plugins/catalog-backend-module-ldap/config.d.ts +++ b/plugins/catalog-backend-module-ldap/config.d.ts @@ -85,7 +85,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -165,7 +165,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -250,7 +250,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -340,7 +340,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -500,7 +500,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -580,7 +580,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -666,7 +666,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -756,7 +756,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -914,7 +914,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { @@ -997,7 +997,7 @@ export interface Config { sizeLimit?: number; timeLimit?: number; derefAliases?: number; - typesOnly?: boolean; + returnAttributeValues?: boolean; paged?: | boolean | { diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 2b59d65daf..61a96b8792 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -45,8 +45,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", - "@types/ldapjs": "^2.2.5", - "ldapjs": "^2.3.3", + "ldapts": "^8.0.6", "lodash": "^4.17.21", "uuid": "^11.0.0" }, diff --git a/plugins/catalog-backend-module-ldap/report.api.md b/plugins/catalog-backend-module-ldap/report.api.md index 1bcf59affb..b33f367a16 100644 --- a/plugins/catalog-backend-module-ldap/report.api.md +++ b/plugins/catalog-backend-module-ldap/report.api.md @@ -6,10 +6,11 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node'; -import { Client } from 'ldapjs'; +import { Client } from 'ldapts'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { Entry } from 'ldapts'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { GroupEntity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; @@ -18,8 +19,8 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; -import { SearchEntry } from 'ldapjs'; -import { SearchOptions } from 'ldapjs'; +import { SearchOptions } from 'ldapts'; +import { SearchResult } from 'ldapts'; import { UserEntity } from '@backstage/catalog-model'; // @public @@ -36,14 +37,14 @@ export default catalogModuleLdapOrgEntityProvider; export function defaultGroupTransformer( vendor: LdapVendor, config: GroupConfig, - entry: SearchEntry, + entry: Entry, ): Promise; // @public export function defaultUserTransformer( vendor: LdapVendor, config: UserConfig, - entry: SearchEntry, + entry: Entry, ): Promise; // @public @@ -70,7 +71,7 @@ export type GroupConfig = { export type GroupTransformer = ( vendor: LdapVendor, config: GroupConfig, - group: SearchEntry, + group: Entry, ) => Promise; // @public @@ -92,14 +93,9 @@ export class LdapClient { bind?: BindConfig, tls?: TLSConfig, ): Promise; - getRootDSE(): Promise; + getRootDSE(): Promise; getVendor(): Promise; - search(dn: string, options: SearchOptions): Promise; - searchStreaming( - dn: string, - options: SearchOptions, - f: (entry: SearchEntry) => Promise | void, - ): Promise; + search(dn: string, options: SearchOptions): Promise; } // @public @@ -203,12 +199,12 @@ export type LdapProviderConfig = { export type LdapVendor = { dnAttributeName: string; uuidAttributeName: string; - decodeStringAttribute: (entry: SearchEntry, name: string) => string[]; + decodeStringAttribute: (entry: Entry, name: string) => string[]; }; // @public export function mapStringAttr( - entry: SearchEntry, + entry: Entry, vendor: LdapVendor, attributeName: string | undefined, setter: (value: string) => void, @@ -265,7 +261,7 @@ export type UserConfig = { export type UserTransformer = ( vendor: LdapVendor, config: UserConfig, - user: SearchEntry, + user: Entry, ) => Promise; // @public diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index 3608612f5f..703387f5fe 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -14,21 +14,19 @@ * limitations under the License. */ -import { ForwardedError, stringifyError } from '@backstage/errors'; +import { ForwardedError } from '@backstage/errors'; import { readFile } from 'fs/promises'; -import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs'; -import { cloneDeep } from 'lodash'; +import { Client, Entry, SearchOptions, SearchResult } from 'ldapts'; import tlsLib from 'tls'; import { BindConfig, TLSConfig } from './config'; -import { createOptions, errorString } from './util'; import { AEDirVendor, ActiveDirectoryVendor, DefaultLdapVendor, - GoogleLdapVendor, LLDAPVendor, FreeIpaVendor, LdapVendor, + GoogleLdapVendor, } from './vendors'; import { LoggerService } from '@backstage/backend-plugin-api'; @@ -39,6 +37,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; * * @public */ + export class LdapClient { private vendor: Promise | undefined; @@ -57,36 +56,31 @@ export class LdapClient { key: key, }); } - - const client = ldap.createClient({ + const tlsOptions: tlsLib.ConnectionOptions = { + secureContext, + rejectUnauthorized: tls?.rejectUnauthorized, + }; + const client = new Client({ url: target, - tlsOptions: { - secureContext, - rejectUnauthorized: tls?.rejectUnauthorized, - }, + ...(Object.values(tlsOptions).some(v => v !== undefined) + ? { tlsOptions } + : undefined), }); - // We want to have a catch-all error handler at the top, since the default - // behavior of the client is to blow up the entire process when it fails, - // unless an error handler is set. - client.on('error', (err: ldap.Error) => { - logger.warn(`LDAP client threw an error, ${errorString(err)}`); - }); + const ldapClient = new LdapClient(client, logger); - if (!bind) { - return new LdapClient(client, logger); + if (bind) { + try { + await client.bind(bind.dn, bind.secret); + } catch (error) { + await client.unbind(); + throw new ForwardedError( + `LDAP bind failed for ${bind.dn}, ${error}`, + error, + ); + } } - - return new Promise((resolve, reject) => { - const { dn, secret } = bind; - client.bind(dn, secret, err => { - if (err) { - reject(`LDAP bind failed for ${dn}, ${errorString(err)}`); - } else { - resolve(new LdapClient(client, logger)); - } - }); - }); + return ldapClient; } private readonly client: Client; @@ -103,129 +97,22 @@ export class LdapClient { * @param dn - The fully qualified base DN to search within * @param options - The search options */ - async search(dn: string, options: SearchOptions): Promise { + async search(dn: string, options: SearchOptions): Promise { + this.logger.debug(`Reading LDAP entries so far`); try { - const output: SearchEntry[] = []; + const ldaptsOptions: SearchOptions = { + scope: options.scope, + filter: options.filter, + attributes: options.attributes, + sizeLimit: options.sizeLimit, + timeLimit: options.timeLimit, + derefAliases: options.derefAliases, + paged: options.paged, + }; - const logInterval = setInterval(() => { - this.logger.debug(`Read ${output.length} LDAP entries so far...`); - }, 5000); + const result = await this.client.search(dn, ldaptsOptions); - const search = new Promise((resolve, reject) => { - // Note that we clone the (frozen) options, since ldapjs rudely tries to - // overwrite parts of them - this.client.search(dn, cloneDeep(options), (err, res) => { - if (err) { - reject(new Error(errorString(err))); - return; - } - - res.on('searchReference', () => { - this.logger.warn('Received unsupported search referral'); - }); - - res.on('searchEntry', entry => { - output.push(entry); - }); - - res.on('error', e => { - reject(new Error(errorString(e))); - }); - - res.on('page', (_result, cb) => { - if (cb) { - cb(); - } - }); - - res.on('end', r => { - if (!r) { - reject(new Error('Null response')); - } else if (r.status !== 0) { - reject(new Error(`Got status ${r.status}: ${r.errorMessage}`)); - } else { - resolve(output); - } - }); - }); - }); - - return await search.finally(() => { - clearInterval(logInterval); - }); - } catch (e) { - throw new ForwardedError(`LDAP search at DN "${dn}" failed`, e); - } - } - - /** - * 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 - */ - async searchStreaming( - dn: string, - options: SearchOptions, - f: (entry: SearchEntry) => Promise | void, - ): Promise { - try { - return await new Promise((resolve, reject) => { - // Note that we clone the (frozen) options, since ldapjs rudely tries to - // overwrite parts of them - this.client.search(dn, createOptions(options), (err, res) => { - if (err) { - reject(new Error(errorString(err))); - } - let awaitList: Array | void> = []; - let transformError = false; - - const transformReject = (e: Error) => { - transformError = true; - reject( - new Error( - `Transform function threw an exception, ${stringifyError(e)}`, - ), - ); - }; - - res.on('searchReference', () => { - this.logger.warn('Received unsupported search referral'); - }); - - res.on('searchEntry', entry => { - if (!transformError) awaitList.push(f(entry)); - }); - - res.on('page', (_, cb) => { - // awaits completion before fetching next page - Promise.all(awaitList) - .then(() => { - // flush list - awaitList = []; - if (cb) cb(); - }) - .catch(transformReject); - }); - - 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 { - Promise.all(awaitList) - .then(() => resolve()) - .catch(transformReject); - } - }); - }); - }); + return result; } catch (e) { throw new ForwardedError(`LDAP search at DN "${dn}" failed`, e); } @@ -241,18 +128,18 @@ export class LdapClient { if (this.vendor) { return this.vendor; } - const clientHost = this.client?.host || ''; + // const clientHost = this.client?.host || ''; this.vendor = this.getRootDSE() .then(root => { - if (root && root.raw?.forestFunctionality) { + if (root && root.forestFunctionality) { return ActiveDirectoryVendor; - } else if (root && root.raw?.ipaDomainLevel) { + } else if (root && root.ipaDomainLevel) { return FreeIpaVendor; - } else if (root && 'aeRoot' in root.raw) { + } else if (root && 'aeRoot' in root) { return AEDirVendor; - } else if (clientHost === 'ldap.google.com') { + } else if (this.isGoogleLDAP(root)) { return GoogleLdapVendor; - } else if (root && root.raw?.vendorName?.toString() === 'LLDAP') { + } else if (root && root.vendorName?.toString() === 'LLDAP') { return LLDAPVendor; } return DefaultLdapVendor; @@ -264,18 +151,73 @@ export class LdapClient { return this.vendor; } + /** + * Check if the LDAP server is Google LDAP by examining RootDSE and schema + */ + private isGoogleLDAP(rootDSE: Entry | undefined): boolean { + if (!rootDSE) { + return false; + } + + // RootDSE characteristics + const hasGoogleRootDSEPattern = + !rootDSE.namingContexts && // No namingContexts + !rootDSE.supportedControl && // No supportedControl + !rootDSE.vendorName && // No vendor info + !rootDSE.vendorVersion && + rootDSE.subschemaSubentry === 'cn=subschema'; + + if (!hasGoogleRootDSEPattern) { + return false; + } + + try { + const schemaHasGoogleAttributes = this.checkGoogleSchema(rootDSE); + return schemaHasGoogleAttributes; + } catch (error) { + throw new ForwardedError('Schema check failed', error); + } + } + + // Check a shema for Google-specific patterns + private checkGoogleSchema(rootDSE: Entry): boolean { + try { + const objectClasses = this.parseSchemaValues(rootDSE.objectClasses); + const attributeTypes = this.parseSchemaValues(rootDSE.attributeTypes); + + // Check if any Google-specific attributes are present + const hasGoogleAttributes = + objectClasses.some(oc => oc.includes('googleUid')) || + attributeTypes.some(at => at.includes('googleAdminCreated')); + + return hasGoogleAttributes; + } catch (error) { + this.logger.warn('Error checking schema:', error); + return false; + } + } + + private parseSchemaValues( + schemaValue: Buffer | Buffer[] | string[] | string, + ): string[] { + if (!schemaValue) return []; + + const values = Array.isArray(schemaValue) ? schemaValue : [schemaValue]; + return values.map(v => v.toString()); + } + /** * Get the Root DSE. * * @see https://ldapwiki.com/wiki/RootDSE */ - async getRootDSE(): Promise { - const result = await this.search('', { + async getRootDSE(): Promise { + const result = await this.client.search('', { scope: 'base', filter: '(objectclass=*)', } as SearchOptions); - if (result && result.length === 1) { - return result[0]; + if (result && result.searchEntries.length === 1) { + return result.searchEntries[0]; } return undefined; } diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts index 598bcc8839..d57fcec12c 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts @@ -179,7 +179,7 @@ describe('readLdapConfig', () => { timeLimit: 42, sizeLimit: 100, derefAliases: 0, - typesOnly: false, + attributeValues: true, }, set: { p: 'v' }, map: { @@ -205,7 +205,7 @@ describe('readLdapConfig', () => { timeLimit: 42, sizeLimit: 100, derefAliases: 1, - typesOnly: true, + attributeValues: false, }, set: { p: 'v' }, map: { @@ -247,7 +247,7 @@ describe('readLdapConfig', () => { timeLimit: 42, sizeLimit: 100, derefAliases: 0, - typesOnly: false, + returnAttributeValues: true, }, set: { p: 'v' }, map: { @@ -275,7 +275,7 @@ describe('readLdapConfig', () => { timeLimit: 42, sizeLimit: 100, derefAliases: 1, - typesOnly: true, + returnAttributeValues: false, }, set: { p: 'v' }, map: { diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 8c7fd0dda5..abf4ce469b 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { JsonValue } from '@backstage/types'; -import { SearchOptions } from 'ldapjs'; +import { SearchOptions } from 'ldapts'; import mergeWith from 'lodash/mergeWith'; import { trimEnd } from 'lodash'; import { RecursivePartial } from './util'; @@ -274,8 +274,10 @@ function readOptionsConfig(c: Config | undefined): SearchOptions { attributes: c.getOptionalStringArray('attributes'), sizeLimit: c.getOptionalNumber('sizeLimit'), timeLimit: c.getOptionalNumber('timeLimit'), - derefAliases: c.getOptionalNumber('derefAliases'), - typesOnly: c.getOptionalBoolean('typesOnly'), + derefAliases: c.getOptionalNumber( + 'derefAliases', + ) as SearchOptions['derefAliases'], + returnAttributeValues: c.getOptionalBoolean('attributeValues'), ...(paged !== undefined ? { paged } : undefined), }; } 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 374cc2afd0..9e840f08ba 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -15,7 +15,7 @@ */ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; -import { SearchEntry } from 'ldapjs'; +import { Entry } from 'ldapts'; import merge from 'lodash/merge'; import { LdapClient } from './client'; import { GroupConfig, UserConfig, VendorConfig } from './config'; @@ -36,6 +36,7 @@ import { ActiveDirectoryVendor, DefaultLdapVendor, FreeIpaVendor, + GoogleLdapVendor, } from './vendors'; function user(data: RecursivePartial): UserEntity { @@ -64,40 +65,32 @@ function group(data: RecursivePartial): GroupEntity { ); } -function searchEntry( - attributes: Record, -): SearchEntry { - return { - raw: Object.entries(attributes).reduce((obj, [key, values]) => { - obj[key] = values; - return obj; - }, {} as any), - } as any; -} - describe('readLdapUsers', () => { const client: jest.Mocked = { - searchStreaming: jest.fn(), + search: jest.fn(), getVendor: jest.fn(), } as any; afterEach(() => jest.resetAllMocks()); it('transfers all attributes from a default ldap vendor', async () => { + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + 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.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - }), - ); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: UserConfig[] = [ { @@ -142,20 +135,23 @@ describe('readLdapUsers', () => { }); it('override default vendor configs', async () => { + const searchEntries: Entry[] = [ + { + dn: 'dn-vaule', + uid: 'uid-value', + description: 'description-value', + cn: 'cn-value', + mail: 'mail-value', + avatarUrl: 'avatarUrl-value', + memberOf: ['x', 'y', 'z'], + customDN: 'dn-value', + customUUID: 'uuid-value', + }, + ]; client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await fn( - searchEntry({ - uid: ['uid-value'], - description: ['description-value'], - cn: ['cn-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - customDN: ['dn-value'], - customUUID: ['uuid-value'], - }), - ); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: UserConfig[] = [ { @@ -211,25 +207,23 @@ describe('readLdapUsers', () => { }); it('should allow skipping memberOf', async () => { + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + customDN: ['dn-value'], + customUUID: ['uuid-value'], + }, + ]; client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await fn(searchEntry({ memberOf: ['x', 'y', 'z'] })); - }); - - client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await fn( - searchEntry({ - uid: ['uid-value'], - description: ['description-value'], - cn: ['cn-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - customDN: ['dn-value'], - customUUID: ['uuid-value'], - }), - ); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: UserConfig[] = [ { @@ -257,26 +251,90 @@ describe('readLdapUsers', () => { expect(userMemberOf.size).toBe(0); }); + it('transfers all attributes from Google', async () => { + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + uid: 'uid-value', + cn: 'cn-value', + mail: 'email-value', + description: 'description-value', + entryUuid: 'uid-value', + memberOf: ['x', 'y', 'z'], + }, + ]; + + client.getVendor.mockResolvedValue(GoogleLdapVendor); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], + }); + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + memberOf: 'memberOf', + }, + }, + ]; + + const { users, userMemberOf } = await readLdapUsers(client, config, {}); + + expect(users).toEqual([ + expect.objectContaining({ + metadata: { + name: 'uid-value', + description: 'description-value', + annotations: { + [LDAP_DN_ANNOTATION]: 'dn-value', + [LDAP_RDN_ANNOTATION]: 'uid-value', + [LDAP_UUID_ANNOTATION]: 'uid-value', + }, + }, + spec: { + profile: { + displayName: 'cn-value', + email: 'email-value', + }, + memberOf: [], + }, + }), + ]); + + expect(userMemberOf).toEqual( + new Map([['dn-value', new Set(['x', 'y', 'z'])]]), + ); + }); + it('transfers all attributes from Microsoft Active Directory', async () => { + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + 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.getVendor.mockResolvedValue(ActiveDirectoryVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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, - ]), - ], - }), - ); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: UserConfig[] = [ { @@ -321,20 +379,22 @@ describe('readLdapUsers', () => { }); it('transfers all attributes from FreeIPA', async () => { + const searchEntries: Entry[] = [ + { + uid: 'uid-value', + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + dn: 'dn-value', + ipaUniqueID: ['uuid-value'], + }, + ]; client.getVendor.mockResolvedValue(FreeIpaVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await fn( - searchEntry({ - uid: ['uid-value'], - description: ['description-value'], - cn: ['cn-value'], - mail: ['mail-value'], - avatarUrl: ['avatarUrl-value'], - memberOf: ['x', 'y', 'z'], - dn: ['dn-value'], - ipaUniqueID: ['uuid-value'], - }), - ); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: UserConfig[] = [ { @@ -380,21 +440,25 @@ describe('readLdapUsers', () => { it('transfers all attributes from for vendorDN case sensitivity', async () => { const vendor = DefaultLdapVendor; + const searchEntries: Entry[] = [ + { + dn: 'dn-VALUE', + 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.getVendor.mockResolvedValue(vendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - }), - ); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); + const config: UserConfig[] = [ { dn: 'ddd', @@ -436,23 +500,28 @@ describe('readLdapUsers', () => { new Map([['dn-VALUE', new Set(['x', 'Y', 'z'])]]), ); }); + it('fails to transfer all attributes from for due case sensitivity', async () => { const vendor = DefaultLdapVendor; - client.getVendor.mockResolvedValue(vendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-VALUE', + 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.getVendor.mockResolvedValue(ActiveDirectoryVendor); + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); + const config: UserConfig[] = [ { dn: 'ddd', @@ -498,19 +567,22 @@ describe('readLdapUsers', () => { it('can process a list of UserConfigs', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + 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.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: UserConfig[] = [ { @@ -552,7 +624,7 @@ describe('readLdapUsers', () => { describe('readLdapGroups', () => { const client: jest.Mocked = { - searchStreaming: jest.fn(), + search: jest.fn(), getVendor: jest.fn(), } as any; @@ -560,20 +632,23 @@ describe('readLdapGroups', () => { it('transfers all attributes from a default ldap vendor', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + 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.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: GroupConfig[] = [ { @@ -627,27 +702,94 @@ describe('readLdapGroups', () => { ); }); + it('transfers all attributes from Google', async () => { + client.getVendor.mockResolvedValue(GoogleLdapVendor); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + uid: 'uid-value', + cn: 'cn-value', + mail: 'email-value', + tt: 'type-value', + description: 'description-value', + entryUuid: 'uid-value', + member: ['e', 'f', 'g'], + }, + ]; + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], + }); + const config: GroupConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + type: 'tt', + members: 'member', + memberOf: 'memberOf', + }, + }, + ]; + const { groups, groupMember, groupMemberOf } = await readLdapGroups( + client, + config, + {}, + ); + expect(groups).toEqual([ + expect.objectContaining({ + metadata: { + name: 'uid-value', + description: 'description-value', + annotations: { + [LDAP_DN_ANNOTATION]: 'dn-value', + [LDAP_RDN_ANNOTATION]: 'uid-value', + [LDAP_UUID_ANNOTATION]: 'uid-value', + }, + }, + spec: { + type: 'type-value', + profile: { + displayName: 'cn-value', + email: 'email-value', + }, + children: [], + }, + }), + ]); + expect(groupMember).toEqual( + new Map([['dn-value', new Set(['e', 'f', 'g'])]]), + ); + expect(groupMemberOf).toEqual(new Map([['dn-value', new Set()]])); + }); + it('transfers all attributes from Microsoft Active Directory', async () => { client.getVendor.mockResolvedValue(ActiveDirectoryVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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, - ]), - ], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + 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.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: GroupConfig[] = [ { @@ -703,20 +845,23 @@ describe('readLdapGroups', () => { it('override default vendor configs', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - customDN: ['dn-value'], - customUUID: ['uuid-value'], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + cn: 'cn-value', + description: 'description-value', + tt: 'type-value', + mail: 'mail-value', + avatarUrl: 'avatarUrl-value', + memberOf: ['x', 'y', 'z'], + member: ['e', 'f', 'g'], + customDN: 'dn-value', + customUUID: 'uuid-value', + }, + ]; + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: GroupConfig[] = [ { @@ -778,20 +923,23 @@ describe('readLdapGroups', () => { it('should allow skipping members', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - customDN: ['dn-value'], - customUUID: ['uuid-value'], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + cn: 'cn-value', + description: 'description-value', + tt: 'type-value', + mail: 'mail-value', + avatarUrl: 'avatarUrl-value', + memberOf: ['x', 'y', 'z'], + member: ['e', 'f', 'g'], + customDN: 'dn-value', + customUUID: 'uuid-value', + }, + ]; + client.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: GroupConfig[] = [ { @@ -823,20 +971,23 @@ describe('readLdapGroups', () => { it('can process a list of GroupConfigs', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); - client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { - await 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'], - }), - ); + const searchEntries: Entry[] = [ + { + dn: 'dn-value', + 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.search.mockResolvedValue({ + searchEntries, + searchReferences: [], }); const config: GroupConfig[] = [ { @@ -1026,7 +1177,8 @@ describe('defaultUserTransformer', () => { }, }; - const entry = searchEntry({ + const entry: Entry = { + dn: 'dn-value', uid: ['uid-value'], description: ['description-value'], cn: ['cn-value'], @@ -1035,7 +1187,7 @@ describe('defaultUserTransformer', () => { memberOf: ['x', 'y', 'z'], entryDN: ['dn-value'], entryUUID: ['uuid-value'], - }); + }; let output = await defaultUserTransformer(DefaultLdapVendor, config, entry); expect(output).toEqual({ @@ -1095,12 +1247,13 @@ describe('defaultUserTransformer', () => { set: {}, }; - const entry = searchEntry({ + const entry: Entry = { + dn: 'ddd', description: ['description-value'], cn: ['cn-value'], mail: ['mail-value'], memberOf: ['x', 'y', 'z'], - }); + }; await expect( defaultUserTransformer(DefaultLdapVendor, config, entry), @@ -1131,7 +1284,8 @@ describe('defaultGroupTransformer', () => { }, }; - const entry = searchEntry({ + const entry: Entry = { + dn: 'dn-value', uid: ['uid-value'], description: ['description-value'], cn: ['cn-value'], @@ -1140,7 +1294,7 @@ describe('defaultGroupTransformer', () => { memberOf: ['x', 'y', 'z'], entryDN: ['dn-value'], entryUUID: ['uuid-value'], - }); + }; let output = await defaultGroupTransformer( DefaultLdapVendor, @@ -1210,14 +1364,15 @@ describe('defaultGroupTransformer', () => { }, }; - const entry = searchEntry({ + const entry: Entry = { + dn: 'dn-value', description: ['description-value'], mail: ['mail-value'], avatarUrl: ['avatarUrl-value'], memberOf: ['x', 'y', 'z'], entryDN: ['dn-value'], entryUUID: ['uuid-value'], - }); + }; await expect( defaultGroupTransformer(DefaultLdapVendor, config, entry), @@ -1248,7 +1403,8 @@ describe('defaultUserTransformerWithCaseSensitiveDNs', () => { }, }; - const entry = searchEntry({ + const entry: Entry = { + dn: 'dn-VALUE', uid: ['uid-value'], description: ['description-value'], cn: ['cn-value'], @@ -1257,7 +1413,7 @@ describe('defaultUserTransformerWithCaseSensitiveDNs', () => { memberOf: ['x', 'y', 'z'], entryDN: ['dn-VALUE'], entryUUID: ['uuid-value'], - }); + }; const vendor = DefaultLdapVendor; let output = await defaultUserTransformer(vendor, config, entry); @@ -1326,7 +1482,8 @@ describe('defaultGroupTransformerWithCaseSensitiveDNs', () => { }, }; - const entry = searchEntry({ + const entry: Entry = { + dn: 'dn-VALUE', uid: ['uid-value'], description: ['description-value'], cn: ['cn-value'], @@ -1335,7 +1492,7 @@ describe('defaultGroupTransformerWithCaseSensitiveDNs', () => { memberOf: ['x', 'y', 'z'], entryDN: ['dn-VALUE'], entryUUID: ['uuid-value'], - }); + }; const vendor = DefaultLdapVendor; diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index a3f3ce1e75..b3d21c24dc 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -19,7 +19,7 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { SearchEntry } from 'ldapjs'; +import { Entry } from 'ldapts'; import lodashSet from 'lodash/set'; import cloneDeep from 'lodash/cloneDeep'; import { buildOrgHierarchy } from './org'; @@ -45,7 +45,7 @@ import { InputError } from '@backstage/errors'; export async function defaultUserTransformer( vendor: LdapVendor, config: UserConfig, - entry: SearchEntry, + entry: Entry, ): Promise { const { set, map } = config; @@ -136,19 +136,20 @@ export async function readLdapUsers( for (const cfg of userConfig) { const { dn, options, map } = cfg; - await client.searchStreaming(dn, options, async user => { - const entity = await transformer(vendor, cfg, user); + const searchResult = await client.search(dn, options); + for (const entry of searchResult.searchEntries) { + const entity = await transformer(vendor, cfg, entry); if (!entity) { - return; + continue; } - mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => { + mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { ensureItems(userMemberOf, myDn, vs); }); entities.push(entity); - }); + } } return { users: entities, userMemberOf }; @@ -163,7 +164,7 @@ export async function readLdapUsers( export async function defaultGroupTransformer( vendor: LdapVendor, config: GroupConfig, - entry: SearchEntry, + entry: Entry, ): Promise { const { set, map } = config; const entity: GroupEntity = { @@ -263,16 +264,12 @@ export async function readLdapGroups( for (const cfg of groupConfig) { const { dn, map, options } = cfg; - - await client.searchStreaming(dn, options, async entry => { - if (!entry) { - return; - } - + const searchResult = await client.search(dn, options); + for (const entry of searchResult.searchEntries) { const entity = await transformer(vendor, cfg, entry); if (!entity) { - return; + continue; } mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { @@ -284,7 +281,7 @@ export async function readLdapGroups( }); groups.push(entity); - }); + } } return { @@ -349,7 +346,7 @@ export async function readLdapOrg( // Maps a multi-valued attribute of references to other objects, to a consumer function mapReferencesAttr( - entry: SearchEntry, + entry: Entry, vendor: LdapVendor, attributeName: string | undefined | null, setter: (sourceDn: string, targets: string[]) => void, diff --git a/plugins/catalog-backend-module-ldap/src/ldap/types.ts b/plugins/catalog-backend-module-ldap/src/ldap/types.ts index 9d7c326add..5726caade7 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/types.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; -import { SearchEntry } from 'ldapjs'; +import { Entry } from 'ldapts'; import { LdapVendor } from './vendors'; import { GroupConfig, UserConfig } from './config'; @@ -34,7 +34,7 @@ import { GroupConfig, UserConfig } from './config'; export type UserTransformer = ( vendor: LdapVendor, config: UserConfig, - user: SearchEntry, + user: Entry, ) => Promise; /** @@ -53,5 +53,5 @@ export type UserTransformer = ( export type GroupTransformer = ( vendor: LdapVendor, config: GroupConfig, - group: SearchEntry, + group: Entry, ) => Promise; diff --git a/plugins/catalog-backend-module-ldap/src/ldap/util.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/util.test.ts deleted file mode 100644 index d8787b1226..0000000000 --- a/plugins/catalog-backend-module-ldap/src/ldap/util.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { errorString, createOptions } from './util'; - -describe('errorString', () => { - it('formats', () => { - const e = { code: 1, name: 'n', message: 'm' }; - expect(errorString(e)).toEqual('1 n: m'); - }); -}); - -describe('createOptions', () => { - it('should add pagePause', () => { - const options = { - filter: 'f', - paged: true, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 0, - typesOnly: false, - }; - expect(createOptions(options)).toEqual({ - filter: 'f', - paged: { pagePause: true }, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 0, - typesOnly: false, - }); - const options2 = { - filter: 'f', - paged: {}, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 0, - typesOnly: false, - }; - expect(createOptions(options2)).toEqual({ - filter: 'f', - paged: { pagePause: true }, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 0, - typesOnly: false, - }); - }); - - it('should not add pagePause', () => { - const options = { - filter: 'f', - paged: false, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 0, - typesOnly: false, - }; - expect(createOptions(options)).toEqual(options); - }); -}); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/util.ts b/plugins/catalog-backend-module-ldap/src/ldap/util.ts index 74b8fe4f88..6ca0fae0a2 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/util.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/util.ts @@ -14,19 +14,9 @@ * limitations under the License. */ -import { Error as LDAPError, SearchEntry, SearchOptions } from 'ldapjs'; -import { cloneDeep } from 'lodash'; +import { Entry } from 'ldapts'; import { LdapVendor } from './vendors'; -/** - * Builds a string form of an LDAP Error structure. - * - * @param error - The error - */ -export function errorString(error: LDAPError) { - return `${error.code} ${error.name}: ${error.message}`; -} - /** * Maps a single-valued attribute to a consumer. * @@ -42,7 +32,7 @@ export function errorString(error: LDAPError) { * @public */ export function mapStringAttr( - entry: SearchEntry, + entry: Entry, vendor: LdapVendor, attributeName: string | undefined, setter: (value: string) => void, @@ -55,18 +45,6 @@ export function mapStringAttr( } } -export function createOptions(inputOptions: SearchOptions): SearchOptions { - const result = cloneDeep(inputOptions); - - if (result.paged === true) { - result.paged = { pagePause: true }; - } else if (typeof result.paged === 'object') { - result.paged.pagePause = true; - } - - return result; -} - export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial[] diff --git a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts index 872941608d..9d620d1ea0 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SearchEntry } from 'ldapjs'; +import { Entry } from 'ldapts'; /** * An LDAP Vendor handles unique nuances between different vendors. @@ -37,7 +37,7 @@ export type LdapVendor = { * @param entry - The ldap entry * @param name - The attribute to decode */ - decodeStringAttribute: (entry: SearchEntry, name: string) => string[]; + decodeStringAttribute: (entry: Entry, name: string) => string[]; }; export const DefaultLdapVendor: LdapVendor = { @@ -106,11 +106,11 @@ export const LLDAPVendor: LdapVendor = { // Decode an attribute to a consumer function decode( - entry: SearchEntry, + entry: Entry, attributeName: string, decoder: (value: string | Buffer) => string, ): string[] { - const values = entry.raw[attributeName]; + const values = entry[attributeName]; if (Array.isArray(values)) { return values.map(v => { return decoder(v); diff --git a/yarn.lock b/yarn.lock index 93d6f61f92..1535b4d5e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5023,9 +5023,8 @@ __metadata: "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/types": "workspace:^" - "@types/ldapjs": "npm:^2.2.5" "@types/lodash": "npm:^4.14.151" - ldapjs: "npm:^2.3.3" + ldapts: "npm:^8.0.6" lodash: "npm:^4.17.21" uuid: "npm:^11.0.0" languageName: unknown @@ -20313,6 +20312,15 @@ __metadata: languageName: node linkType: hard +"@types/asn1@npm:>=0.2.4": + version: 0.2.4 + resolution: "@types/asn1@npm:0.2.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/01fee5a896a650f92c95556c83678d66677060b5aba7936b714b5c8ca2945cc8cabf57441e323f1800a783a55958b6230d109e988ca45e83b2d7a0e071c24069 + languageName: node + linkType: hard + "@types/aws-lambda@npm:8.10.150, @types/aws-lambda@npm:^8.10.83": version: 8.10.150 resolution: "@types/aws-lambda@npm:8.10.150" @@ -21124,15 +21132,6 @@ __metadata: languageName: node linkType: hard -"@types/ldapjs@npm:^2.2.5": - version: 2.2.5 - resolution: "@types/ldapjs@npm:2.2.5" - dependencies: - "@types/node": "npm:*" - checksum: 10/6981e24944c8132eff6bf76d367a18aa34238fcb86c385b4711c577d62f087d770aa81f6975beddcca9f69c8ce002de73922e1f3689e1315f4ad9401edc54dcd - languageName: node - linkType: hard - "@types/libsodium-wrappers@npm:^0.7.10": version: 0.7.14 resolution: "@types/libsodium-wrappers@npm:0.7.14" @@ -23854,13 +23853,6 @@ __metadata: languageName: node linkType: hard -"abstract-logging@npm:^2.0.0": - version: 2.0.0 - resolution: "abstract-logging@npm:2.0.0" - checksum: 10/2f3e0ef758a18d98ddfd4fc01532724d6ada0945e2f679a0ad3b73fbe4b3594b1a3047dafb25bb88d3cb4031c4ea6090b25f8f5b7cfb4d2c7bcf125a56aca6df - languageName: node - linkType: hard - "accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -24635,7 +24627,7 @@ __metadata: languageName: node linkType: hard -"asn1@npm:^0.2.4, asn1@npm:^0.2.6": +"asn1@npm:0.2.6, asn1@npm:^0.2.6": version: 0.2.6 resolution: "asn1@npm:0.2.6" dependencies: @@ -24644,13 +24636,6 @@ __metadata: languageName: node linkType: hard -"assert-plus@npm:^1.0.0": - version: 1.0.0 - resolution: "assert-plus@npm:1.0.0" - checksum: 10/f4f991ae2df849cc678b1afba52d512a7cbf0d09613ba111e72255409ff9158550c775162a47b12d015d1b82b3c273e8e25df0e4783d3ddb008a293486d00a07 - languageName: node - linkType: hard - "assert@npm:^2.0.0": version: 2.1.0 resolution: "assert@npm:2.1.0" @@ -25108,15 +25093,6 @@ __metadata: languageName: node linkType: hard -"backoff@npm:^2.5.0": - version: 2.5.0 - resolution: "backoff@npm:2.5.0" - dependencies: - precond: "npm:0.2" - checksum: 10/5286c3f02665f3347591e04728ba0755e76a45aa40e037f7db3f2029ede927bfe94755a03033e93cc971a4b6c6605d8cfe514433e362c5a86c0b5bbc5d47acce - languageName: node - linkType: hard - "bail@npm:^2.0.0": version: 2.0.1 resolution: "bail@npm:2.0.1" @@ -27264,7 +27240,7 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:1.0.2, core-util-is@npm:~1.0.0": +"core-util-is@npm:~1.0.0": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" checksum: 10/d0f7587346b44a1fe6c269267e037dd34b4787191e473c3e685f507229d88561c40eb18872fabfff02977301815d474300b7bfbd15396c13c5377393f7e87ec3 @@ -28114,15 +28090,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:4.4.1, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.1 + resolution: "debug@npm:4.4.1" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe languageName: node linkType: hard @@ -30865,13 +30841,6 @@ __metadata: languageName: node linkType: hard -"extsprintf@npm:^1.2.0": - version: 1.4.0 - resolution: "extsprintf@npm:1.4.0" - checksum: 10/c1e6cc79d7efc23770b3688bac3b8ec1f0200bca18c2a5e4e2697f9b9d4b9b1f2e5439541437fe90923bbd1afbeb9507cd68b10832e14ca475a9354b990872c3 - languageName: node - linkType: hard - "fast-copy@npm:^3.0.2": version: 3.0.2 resolution: "fast-copy@npm:3.0.2" @@ -36491,28 +36460,17 @@ __metadata: languageName: node linkType: hard -"ldap-filter@npm:^0.3.3": - version: 0.3.3 - resolution: "ldap-filter@npm:0.3.3" +"ldapts@npm:^8.0.6": + version: 8.0.6 + resolution: "ldapts@npm:8.0.6" dependencies: - assert-plus: "npm:^1.0.0" - checksum: 10/9d34e812a16e328e53ceef7821fd7aa143492d1c3accd1976ef38ec2864344e82c3de812f49ac59f083d1f7a9aa956a2831f75c5ec72189f729bd495b2a513ee - languageName: node - linkType: hard - -"ldapjs@npm:^2.3.3": - version: 2.3.3 - resolution: "ldapjs@npm:2.3.3" - dependencies: - abstract-logging: "npm:^2.0.0" - asn1: "npm:^0.2.4" - assert-plus: "npm:^1.0.0" - backoff: "npm:^2.5.0" - ldap-filter: "npm:^0.3.3" - once: "npm:^1.4.0" - vasync: "npm:^2.2.0" - verror: "npm:^1.8.1" - checksum: 10/98ef566afe83bc4e138b200e4edff1aa8558cb3b0cfb806f772a4fdb85df49c694607ca725993f34a399d06e14e9461d3a39ca782d84b22eebff5d1fbaaf5fbc + "@types/asn1": "npm:>=0.2.4" + asn1: "npm:0.2.6" + debug: "npm:4.4.1" + strict-event-emitter-types: "npm:2.0.0" + uuid: "npm:11.1.0" + whatwg-url: "npm:14.2.0" + checksum: 10/583fdf70762858fab1fc660670f482aa146481b4247b6e34eebe56628c7c069fe4167584c4159b3998503de18a716cb845e02b28794bc783de9b8c09834e4b15 languageName: node linkType: hard @@ -42301,13 +42259,6 @@ __metadata: languageName: node linkType: hard -"precond@npm:0.2": - version: 0.2.3 - resolution: "precond@npm:0.2.3" - checksum: 10/d5215e17cc812996f72ee57a684ff159709bdfa48538e71c361d17aecd750bf25f64f85ba2c49031860708e0e70ac4394b9c8280725f227b88bce0fe76f8389a - languageName: node - linkType: hard - "preferred-pm@npm:^3.0.3": version: 3.0.3 resolution: "preferred-pm@npm:3.0.3" @@ -46556,6 +46507,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter-types@npm:2.0.0": + version: 2.0.0 + resolution: "strict-event-emitter-types@npm:2.0.0" + checksum: 10/d7b28708bf09648302e8ea5a6c4999d7e3186a06f68568a6196a71e315bf4ef44f78011db11576b0b4ed45df4a8f0baf0d64232d2a6eaf21fa92c7ec4085f481 + languageName: node + linkType: hard + "strict-event-emitter@npm:^0.2.4": version: 0.2.8 resolution: "strict-event-emitter@npm:0.2.8" @@ -47883,12 +47841,12 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^5.0.0": - version: 5.0.0 - resolution: "tr46@npm:5.0.0" +"tr46@npm:^5.1.0": + version: 5.1.1 + resolution: "tr46@npm:5.1.1" dependencies: punycode: "npm:^2.3.1" - checksum: 10/29155adb167d048d3c95d181f7cb5ac71948b4e8f3070ec455986e1f34634acae50ae02a3c8d448121c3afe35b76951cd46ed4c128fd80264280ca9502237a3e + checksum: 10/833a0e1044574da5790148fd17866d4ddaea89e022de50279967bcd6b28b4ce0d30d59eb3acf9702b60918975b3bad481400337e3a2e6326cffa5c77b874753d languageName: node linkType: hard @@ -49262,6 +49220,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:11.1.0, uuid@npm:^11.0.0, uuid@npm:^11.0.2, uuid@npm:^11.0.3": + version: 11.1.0 + resolution: "uuid@npm:11.1.0" + bin: + uuid: dist/esm/bin/uuid + checksum: 10/d2da43b49b154d154574891ced66d0c83fc70caaad87e043400cf644423b067542d6f3eb641b7c819224a7cd3b4c2f21906acbedd6ec9c6a05887aa9115a9cf5 + languageName: node + linkType: hard + "uuid@npm:8.3.2, uuid@npm:^8.0.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -49280,15 +49247,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.0.0, uuid@npm:^11.0.2, uuid@npm:^11.0.3": - version: 11.1.0 - resolution: "uuid@npm:11.1.0" - bin: - uuid: dist/esm/bin/uuid - checksum: 10/d2da43b49b154d154574891ced66d0c83fc70caaad87e043400cf644423b067542d6f3eb641b7c819224a7cd3b4c2f21906acbedd6ec9c6a05887aa9115a9cf5 - languageName: node - linkType: hard - "uuid@npm:^3.4.0": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -49416,26 +49374,6 @@ __metadata: languageName: node linkType: hard -"vasync@npm:^2.2.0": - version: 2.2.0 - resolution: "vasync@npm:2.2.0" - dependencies: - verror: "npm:1.10.0" - checksum: 10/192a15eb5caf22af5b2ec578400a6b00ef22230575df11e815144da49170841fedd3a5af96a2a62f8fc5ec5e60ce67e8ae311b509a95043e55081665c464cf7b - languageName: node - linkType: hard - -"verror@npm:1.10.0, verror@npm:^1.8.1": - version: 1.10.0 - resolution: "verror@npm:1.10.0" - dependencies: - assert-plus: "npm:^1.0.0" - core-util-is: "npm:1.0.2" - extsprintf: "npm:^1.2.0" - checksum: 10/da548149dd9c130a8a2587c9ee71ea30128d1526925707e2d01ed9c5c45c9e9f86733c66a328247cdd5f7c1516fb25b0f959ba754bfbe15072aa99ff96468a29 - languageName: node - linkType: hard - "vfile-message@npm:^3.0.0": version: 3.0.2 resolution: "vfile-message@npm:3.0.2" @@ -49906,6 +49844,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:14.2.0, whatwg-url@npm:^14.0.0": + version: 14.2.0 + resolution: "whatwg-url@npm:14.2.0" + dependencies: + tr46: "npm:^5.1.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10/f0a95b0601c64f417c471536a2d828b4c16fe37c13662483a32f02f183ed0f441616609b0663fb791e524e8cd56d9a86dd7366b1fc5356048ccb09b576495e7c + languageName: node + linkType: hard + "whatwg-url@npm:^11.0.0": version: 11.0.0 resolution: "whatwg-url@npm:11.0.0" @@ -49916,16 +49864,6 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^14.0.0": - version: 14.0.0 - resolution: "whatwg-url@npm:14.0.0" - dependencies: - tr46: "npm:^5.0.0" - webidl-conversions: "npm:^7.0.0" - checksum: 10/67ea7a359a90663b28c816d76379b4be62d13446e9a4c0ae0b5ae0294b1c22577750fcdceb40827bb35a61777b7093056953c856604a28b37d6a209ba59ad062 - languageName: node - linkType: hard - "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0"