From a956e4473edfab43aa20b47e10e4a1795f7a23f6 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Thu, 5 Sep 2024 15:47:38 -0400 Subject: [PATCH 01/12] make dnAttributeName and uuidAttributeName for ldap vendors configurable Signed-off-by: Kashish Mittal --- .../src/ldap/client.ts | 24 +--- .../src/ldap/config.test.ts | 12 ++ .../src/ldap/config.ts | 41 ++++++ .../src/ldap/read.test.ts | 128 +++++++++++++----- .../src/ldap/read.ts | 33 +++-- .../src/ldap/vendors.ts | 63 +++------ .../src/processors/LdapOrgEntityProvider.ts | 1 + .../src/processors/LdapOrgReaderProcessor.ts | 1 + 8 files changed, 196 insertions(+), 107 deletions(-) diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index 48a3eedfb8..867a49b16a 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -19,15 +19,9 @@ import { readFile } from 'fs/promises'; import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs'; import { cloneDeep } from 'lodash'; import tlsLib from 'tls'; -import { BindConfig, TLSConfig } from './config'; +import { BindConfig, TLSConfig, VendorConfig } from './config'; import { createOptions, errorString } from './util'; -import { - AEDirVendor, - ActiveDirectoryVendor, - DefaultLdapVendor, - FreeIpaVendor, - LdapVendor, -} from './vendors'; +import { CreateLdapVendor, LdapVendor } from './vendors'; import { LoggerService } from '@backstage/backend-plugin-api'; /** @@ -232,20 +226,16 @@ export class LdapClient { * * @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor */ - async getVendor(): Promise { + async getVendor(vendorConfig: VendorConfig): Promise { if (this.vendor) { return this.vendor; } this.vendor = this.getRootDSE() .then(root => { - if (root && root.raw?.forestFunctionality) { - return ActiveDirectoryVendor; - } else if (root && root.raw?.ipaDomainLevel) { - return FreeIpaVendor; - } else if (root && 'aeRoot' in root.raw) { - return AEDirVendor; - } - return DefaultLdapVendor; + return CreateLdapVendor( + vendorConfig, + !!(root && root.raw?.forestFunctionality), + ); }) .catch(err => { this.vendor = 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..229ad9bd2a 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts @@ -78,6 +78,10 @@ describe('readLdapConfig', () => { }, }, ], + vendor: { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }, }, ]; expect(actual).toEqual(expected); @@ -151,6 +155,10 @@ describe('readLdapConfig', () => { }, }, ], + vendor: { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }, }, ]; expect(actual).toEqual(expected); @@ -291,6 +299,10 @@ describe('readLdapConfig', () => { }, }, ], + vendor: { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }, }, ]; expect(actual).toEqual(expected); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index d7e1035545..ec6a944eb9 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -47,6 +47,10 @@ export type LdapProviderConfig = { groups: GroupConfig[]; // Schedule configuration for refresh tasks. schedule?: SchedulerServiceTaskScheduleDefinition; + // Configuration for LDAP vendor-specific attributes. If not specified, the default values will be used: + // - `dnAttributeName`: `entryDN` + // - `uuidAttributeName`: `entryUUID` + vendor: VendorConfig; }; /** @@ -161,6 +165,26 @@ export type GroupConfig = { }; }; +/** + * Configuration for LDAP vendor-specific attributes. + * + * Allows custom attribute names for distinguished names (DN) and + * universally unique identifiers (UUID) in LDAP directories. + * + * @public + */ +export type VendorConfig = { + /** + * Attribute name for the distinguished name (DN) of an entry, + */ + dnAttributeName: string; + + /** + * Attribute name for the unique identifier (UUID) of an entry, + */ + uuidAttributeName: string; +}; + const defaultUserConfig = { options: { scope: 'one', @@ -225,6 +249,21 @@ function readBindConfig( }; } +function readVendorConfig( + c: Config | undefined, +): LdapProviderConfig['vendor'] | undefined { + if (!c) { + return { + dnAttributeName: `entryDN`, + uuidAttributeName: `entryUUID`, + }; + } + return { + dnAttributeName: c.getString('dn'), + uuidAttributeName: c.getString('uuidAttributeName'), + }; +} + function readOptionsConfig(c: Config | undefined): SearchOptions { if (!c) { return {}; @@ -375,6 +414,7 @@ export function readLdapLegacyConfig(config: Config): LdapProviderConfig[] { groups: readGroupConfig(c.getConfig('groups')).map(it => { return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent); }), + vendor: readVendorConfig(c.getOptionalConfig('vendor')), }; return freeze(newConfig) as LdapProviderConfig; @@ -426,6 +466,7 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] { return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent); }), schedule, + vendor: readVendorConfig(c.getOptionalConfig('vendor')), }; return freeze(newConfig) as LdapProviderConfig; 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 1c3dfb179a..53f1dab090 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -18,7 +18,7 @@ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { SearchEntry } from 'ldapjs'; import merge from 'lodash/merge'; import { LdapClient } from './client'; -import { GroupConfig, UserConfig } from './config'; +import { GroupConfig, UserConfig, VendorConfig } from './config'; import { LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, @@ -32,11 +32,8 @@ import { resolveRelations, } from './read'; import { RecursivePartial } from './util'; -import { - ActiveDirectoryVendor, - DefaultLdapVendor, - FreeIpaVendor, -} from './vendors'; + +import { CreateLdapVendor } from './vendors'; function user(data: RecursivePartial): UserEntity { return merge( @@ -84,7 +81,11 @@ describe('readLdapUsers', () => { afterEach(() => jest.resetAllMocks()); it('transfers all attributes from a default ldap vendor', async () => { - client.getVendor.mockResolvedValue(DefaultLdapVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -99,7 +100,7 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig[] = [ + const userConfig: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -114,7 +115,11 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers(client, config); + const { users, userMemberOf } = await readLdapUsers( + client, + userConfig, + vendorConfig, + ); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -142,7 +147,11 @@ describe('readLdapUsers', () => { }); it('transfers all attributes from Microsoft Active Directory', async () => { - client.getVendor.mockResolvedValue(ActiveDirectoryVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'distinguishedName', + uuidAttributeName: 'objectGUID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -162,7 +171,7 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig[] = [ + const userConfig: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -177,7 +186,11 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers(client, config); + const { users, userMemberOf } = await readLdapUsers( + client, + userConfig, + vendorConfig, + ); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -205,7 +218,11 @@ describe('readLdapUsers', () => { }); it('transfers all attributes from FreeIPA', async () => { - client.getVendor.mockResolvedValue(FreeIpaVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'dn', + uuidAttributeName: 'ipaUniqueID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -220,7 +237,7 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig[] = [ + const userConfig: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -235,7 +252,11 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers(client, config); + const { users, userMemberOf } = await readLdapUsers( + client, + userConfig, + vendorConfig, + ); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -262,7 +283,11 @@ describe('readLdapUsers', () => { ); }); it('can process a list of UserConfigs', async () => { - client.getVendor.mockResolvedValue(DefaultLdapVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -277,7 +302,7 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig[] = [ + const userConfig: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -305,12 +330,16 @@ describe('readLdapUsers', () => { }, }, ]; - const { users } = await readLdapUsers(client, config); + const { users } = await readLdapUsers(client, userConfig, vendorConfig); expect(users).toHaveLength(2); }); it('can process no UserConfigs', async () => { - const config: UserConfig[] = []; - const { users } = await readLdapUsers(client, config); + const userConfig: UserConfig[] = []; + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + const { users } = await readLdapUsers(client, userConfig, vendorConfig); expect(users).toHaveLength(0); }); }); @@ -324,7 +353,11 @@ describe('readLdapGroups', () => { afterEach(() => jest.resetAllMocks()); it('transfers all attributes from a default ldap vendor', async () => { - client.getVendor.mockResolvedValue(DefaultLdapVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -340,7 +373,7 @@ describe('readLdapGroups', () => { }), ); }); - const config: GroupConfig[] = [ + const groupConfig: GroupConfig[] = [ { dn: 'ddd', options: {}, @@ -359,7 +392,8 @@ describe('readLdapGroups', () => { ]; const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, - config, + groupConfig, + vendorConfig, ); expect(groups).toEqual([ expect.objectContaining({ @@ -392,7 +426,11 @@ describe('readLdapGroups', () => { }); it('transfers all attributes from Microsoft Active Directory', async () => { - client.getVendor.mockResolvedValue(ActiveDirectoryVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'distinguishedName', + uuidAttributeName: 'objectGUID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -413,7 +451,7 @@ describe('readLdapGroups', () => { }), ); }); - const config: GroupConfig[] = [ + const groupConfig: GroupConfig[] = [ { dn: 'ddd', options: {}, @@ -432,7 +470,8 @@ describe('readLdapGroups', () => { ]; const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, - config, + groupConfig, + vendorConfig, ); expect(groups).toEqual([ expect.objectContaining({ @@ -465,7 +504,11 @@ describe('readLdapGroups', () => { }); it('can process a list of GroupConfigs', async () => { - client.getVendor.mockResolvedValue(DefaultLdapVendor); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -481,7 +524,7 @@ describe('readLdapGroups', () => { }), ); }); - const config: GroupConfig[] = [ + const groupConfig: GroupConfig[] = [ { dn: 'ddd', options: {}, @@ -513,13 +556,17 @@ describe('readLdapGroups', () => { }, }, ]; - const { groups } = await readLdapGroups(client, config); + const { groups } = await readLdapGroups(client, groupConfig, vendorConfig); expect(groups).toHaveLength(2); }); it('can process no GroupConfigs', async () => { - const config: GroupConfig[] = []; - const { groups } = await readLdapGroups(client, config); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + const groupConfig: GroupConfig[] = []; + const { groups } = await readLdapGroups(client, groupConfig, vendorConfig); expect(groups).toHaveLength(0); }); }); @@ -679,8 +726,12 @@ describe('defaultUserTransformer', () => { entryDN: ['dn-value'], entryUUID: ['uuid-value'], }); - - let output = await defaultUserTransformer(DefaultLdapVendor, config, entry); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + const defaultLdapVendor = CreateLdapVendor(vendorConfig, false); + let output = await defaultUserTransformer(defaultLdapVendor, config, entry); expect(output).toEqual({ apiVersion: 'backstage.io/v1beta1', kind: 'User', @@ -703,7 +754,7 @@ describe('defaultUserTransformer', () => { (output!.metadata.annotations as any).c = 7; // exact same inputs again - output = await defaultUserTransformer(DefaultLdapVendor, config, entry); + output = await defaultUserTransformer(defaultLdapVendor, config, entry); expect(output).toEqual({ apiVersion: 'backstage.io/v1beta1', kind: 'User', @@ -757,8 +808,13 @@ describe('defaultGroupTransformer', () => { entryUUID: ['uuid-value'], }); + const vendorConfig: VendorConfig = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }; + const defaultLdapVendor = CreateLdapVendor(vendorConfig, false); let output = await defaultGroupTransformer( - DefaultLdapVendor, + defaultLdapVendor, config, entry, ); @@ -786,7 +842,7 @@ describe('defaultGroupTransformer', () => { (output!.metadata.annotations as any).c = 7; // exact same inputs again - output = await defaultGroupTransformer(DefaultLdapVendor, config, entry); + output = await defaultGroupTransformer(defaultLdapVendor, config, entry); expect(output).toEqual({ apiVersion: 'backstage.io/v1beta1', kind: 'Group', diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index 9e6e11a0bc..28cca0ad72 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -24,7 +24,7 @@ import lodashSet from 'lodash/set'; import cloneDeep from 'lodash/cloneDeep'; import { buildOrgHierarchy } from './org'; import { LdapClient } from './client'; -import { GroupConfig, UserConfig } from './config'; +import { GroupConfig, UserConfig, VendorConfig } from './config'; import { LDAP_DN_ANNOTATION, LDAP_RDN_ANNOTATION, @@ -104,22 +104,23 @@ export async function defaultUserTransformer( */ export async function readLdapUsers( client: LdapClient, - config: UserConfig[], + userConfig: UserConfig[], + vendorConfig: VendorConfig, opts?: { transformer?: UserTransformer }, ): Promise<{ users: UserEntity[]; // With all relations empty userMemberOf: Map>; // DN -> DN or UUID of groups }> { - if (config.length === 0) { + if (userConfig.length === 0) { return { users: [], userMemberOf: new Map() }; } const entities: UserEntity[] = []; const userMemberOf: Map> = new Map(); - const vendor = await client.getVendor(); + const vendor = await client.getVendor(vendorConfig); const transformer = opts?.transformer ?? defaultUserTransformer; - for (const cfg of config) { + for (const cfg of userConfig) { const { dn, options, map } = cfg; await client.searchStreaming(dn, options, async user => { const entity = await transformer(vendor, cfg, user); @@ -210,7 +211,8 @@ export async function defaultGroupTransformer( */ export async function readLdapGroups( client: LdapClient, - config: GroupConfig[], + groupConfig: GroupConfig[], + vendorConfig: VendorConfig, opts?: { transformer?: GroupTransformer; }, @@ -219,17 +221,17 @@ export async function readLdapGroups( groupMemberOf: Map>; // DN -> DN or UUID of groups groupMember: Map>; // DN -> DN or UUID of groups & users }> { - if (config.length === 0) { + if (groupConfig.length === 0) { return { groups: [], groupMemberOf: new Map(), groupMember: new Map() }; } const groups: GroupEntity[] = []; const groupMemberOf: Map> = new Map(); const groupMember: Map> = new Map(); - const vendor = await client.getVendor(); + const vendor = await client.getVendor(vendorConfig); const transformer = opts?.transformer ?? defaultGroupTransformer; - for (const cfg of config) { + for (const cfg of groupConfig) { const { dn, map, options } = cfg; await client.searchStreaming(dn, options, async entry => { @@ -275,6 +277,7 @@ export async function readLdapOrg( client: LdapClient, userConfig: UserConfig[], groupConfig: GroupConfig[], + vendorConfig: VendorConfig, options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; @@ -287,12 +290,18 @@ export async function readLdapOrg( // Invokes the above "raw" read functions and stitches together the results // with all relations etc filled in. - const { users, userMemberOf } = await readLdapUsers(client, userConfig, { - transformer: options?.userTransformer, - }); + const { users, userMemberOf } = await readLdapUsers( + client, + userConfig, + vendorConfig, + { + transformer: options?.userTransformer, + }, + ); const { groups, groupMemberOf, groupMember } = await readLdapGroups( client, groupConfig, + vendorConfig, { transformer: options?.groupTransformer }, ); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts index 9f4e365c38..d939811e90 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts @@ -15,6 +15,7 @@ */ import { SearchEntry } from 'ldapjs'; +import { VendorConfig } from './config'; /** * An LDAP Vendor handles unique nuances between different vendors. @@ -39,48 +40,26 @@ export type LdapVendor = { decodeStringAttribute: (entry: SearchEntry, name: string) => string[]; }; -export const DefaultLdapVendor: LdapVendor = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - decodeStringAttribute: (entry, name) => { - return decode(entry, name, value => { - return value.toString(); - }); - }, -}; - -export const ActiveDirectoryVendor: LdapVendor = { - dnAttributeName: 'distinguishedName', - uuidAttributeName: 'objectGUID', - decodeStringAttribute: (entry, name) => { - const decoder = (value: string | Buffer) => { - if (name === ActiveDirectoryVendor.uuidAttributeName) { - return formatGUID(value); - } - return value.toString(); - }; - return decode(entry, name, decoder); - }, -}; - -export const FreeIpaVendor: LdapVendor = { - dnAttributeName: 'dn', - uuidAttributeName: 'ipaUniqueID', - decodeStringAttribute: (entry, name) => { - return decode(entry, name, value => { - return value.toString(); - }); - }, -}; - -export const AEDirVendor: LdapVendor = { - dnAttributeName: 'dn', - uuidAttributeName: 'entryUUID', - decodeStringAttribute: (entry, name) => { - return decode(entry, name, value => { - return value.toString(); - }); - }, +export const CreateLdapVendor = ( + vendorConfig: VendorConfig, + isActiveDirectoryVendor: boolean, +): LdapVendor => { + return { + dnAttributeName: vendorConfig.dnAttributeName, + uuidAttributeName: vendorConfig.uuidAttributeName, + decodeStringAttribute: (entry, name) => { + const decoder = (value: string | Buffer) => { + if ( + isActiveDirectoryVendor && + name === vendorConfig.uuidAttributeName + ) { + return formatGUID(value); + } + return value.toString(); + }; + return decode(entry, name, decoder); + }, + }; }; // Decode an attribute to a consumer diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index 0e5fae55bb..a8d27855dc 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -294,6 +294,7 @@ export class LdapOrgEntityProvider implements EntityProvider { client, this.options.provider.users, this.options.provider.groups, + this.options.provider.vendor, { groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts index acbe128e0b..56f984dabc 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts @@ -109,6 +109,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { client, provider.users, provider.groups, + provider.vendor, { groupTransformer: this.groupTransformer, userTransformer: this.userTransformer, From b50e4a822f963e12e86d4f9b4e4145aaa9e19a11 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Thu, 5 Sep 2024 16:40:49 -0400 Subject: [PATCH 02/12] made vendor config optional Signed-off-by: Kashish Mittal --- .changeset/short-pumpkins-prove.md | 5 +++ .../src/ldap/client.ts | 37 ++++++++++++++++++- .../src/ldap/config.test.ts | 12 ------ .../src/ldap/config.ts | 7 +--- .../src/ldap/read.ts | 6 +-- 5 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 .changeset/short-pumpkins-prove.md diff --git a/.changeset/short-pumpkins-prove.md b/.changeset/short-pumpkins-prove.md new file mode 100644 index 0000000000..be5f6ce078 --- /dev/null +++ b/.changeset/short-pumpkins-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +Add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in LDAP vendor settings diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index 867a49b16a..8703eee1ef 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -226,12 +226,47 @@ export class LdapClient { * * @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor */ - async getVendor(vendorConfig: VendorConfig): Promise { + async getVendor(vendorConfig: VendorConfig | undefined): Promise { if (this.vendor) { return this.vendor; } this.vendor = this.getRootDSE() .then(root => { + if (!vendorConfig) { + if (root && root.raw?.forestFunctionality) { + // ActiveDirectoryVendor + return CreateLdapVendor( + { + dnAttributeName: 'distinguishedName', + uuidAttributeName: 'objectGUID', + }, + true, + ); + } else if (root && root.raw?.ipaDomainLevel) { + // FreeIpaVendor + return CreateLdapVendor( + { + dnAttributeName: 'dn', + uuidAttributeName: 'ipaUniqueID', + }, + false, + ); + } else if (root && 'aeRoot' in root.raw) { + // AEDirVendor + return CreateLdapVendor( + { dnAttributeName: 'dn', uuidAttributeName: 'entryUUID' }, + false, + ); + } + // DefaultLdapVendor + return CreateLdapVendor( + { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + }, + !!(root && root.raw?.forestFunctionality), + ); + } return CreateLdapVendor( vendorConfig, !!(root && root.raw?.forestFunctionality), 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 229ad9bd2a..598bcc8839 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts @@ -78,10 +78,6 @@ describe('readLdapConfig', () => { }, }, ], - vendor: { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }, }, ]; expect(actual).toEqual(expected); @@ -155,10 +151,6 @@ describe('readLdapConfig', () => { }, }, ], - vendor: { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }, }, ]; expect(actual).toEqual(expected); @@ -299,10 +291,6 @@ describe('readLdapConfig', () => { }, }, ], - vendor: { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }, }, ]; expect(actual).toEqual(expected); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index ec6a944eb9..1fbe93c533 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -50,7 +50,7 @@ export type LdapProviderConfig = { // Configuration for LDAP vendor-specific attributes. If not specified, the default values will be used: // - `dnAttributeName`: `entryDN` // - `uuidAttributeName`: `entryUUID` - vendor: VendorConfig; + vendor?: VendorConfig; }; /** @@ -253,10 +253,7 @@ function readVendorConfig( c: Config | undefined, ): LdapProviderConfig['vendor'] | undefined { if (!c) { - return { - dnAttributeName: `entryDN`, - uuidAttributeName: `entryUUID`, - }; + return undefined; } return { dnAttributeName: c.getString('dn'), diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index 28cca0ad72..3b59ac35ac 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -105,7 +105,7 @@ export async function defaultUserTransformer( export async function readLdapUsers( client: LdapClient, userConfig: UserConfig[], - vendorConfig: VendorConfig, + vendorConfig: VendorConfig | undefined, opts?: { transformer?: UserTransformer }, ): Promise<{ users: UserEntity[]; // With all relations empty @@ -212,7 +212,7 @@ export async function defaultGroupTransformer( export async function readLdapGroups( client: LdapClient, groupConfig: GroupConfig[], - vendorConfig: VendorConfig, + vendorConfig: VendorConfig | undefined, opts?: { transformer?: GroupTransformer; }, @@ -277,7 +277,7 @@ export async function readLdapOrg( client: LdapClient, userConfig: UserConfig[], groupConfig: GroupConfig[], - vendorConfig: VendorConfig, + vendorConfig: VendorConfig | undefined, options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; From c3c73289aa122d01d6185a1b48673a2f75da3819 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Thu, 5 Sep 2024 16:51:47 -0400 Subject: [PATCH 03/12] add changeset Signed-off-by: Kashish Mittal --- .changeset/short-pumpkins-prove.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/short-pumpkins-prove.md b/.changeset/short-pumpkins-prove.md index be5f6ce078..bc273ac3ad 100644 --- a/.changeset/short-pumpkins-prove.md +++ b/.changeset/short-pumpkins-prove.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-ldap': minor --- -Add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in LDAP vendor settings +Add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in LDAP vendor From d7dadaac6ac6ebae0023fc690752f6e6be1eccc9 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Fri, 6 Sep 2024 09:34:37 -0400 Subject: [PATCH 04/12] update api reports Signed-off-by: Kashish Mittal --- plugins/catalog-backend-module-ldap/api-report.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 40bb9d81ae..a025055a11 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -95,7 +95,8 @@ export class LdapClient { tls?: TLSConfig, ): Promise; getRootDSE(): Promise; - getVendor(): Promise; + // Warning: (ae-forgotten-export) The symbol "VendorConfig" needs to be exported by the entry point index.d.ts + getVendor(vendorConfig: VendorConfig | undefined): Promise; search(dn: string, options: SearchOptions): Promise; searchStreaming( dn: string, @@ -200,6 +201,7 @@ export type LdapProviderConfig = { users: UserConfig[]; groups: GroupConfig[]; schedule?: SchedulerServiceTaskScheduleDefinition; + vendor?: VendorConfig; }; // @public @@ -225,6 +227,7 @@ export function readLdapOrg( client: LdapClient, userConfig: UserConfig[], groupConfig: GroupConfig[], + vendorConfig: VendorConfig | undefined, options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; From 79ca3b0cbef7a7e92d417dd66a1642c113c750fe Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Fri, 6 Sep 2024 09:53:08 -0400 Subject: [PATCH 05/12] update api reports Signed-off-by: Kashish Mittal --- plugins/catalog-backend-module-ldap/api-report.md | 7 ++++++- plugins/catalog-backend-module-ldap/src/ldap/index.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index a025055a11..8e45a99a70 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -95,7 +95,6 @@ export class LdapClient { tls?: TLSConfig, ): Promise; getRootDSE(): Promise; - // Warning: (ae-forgotten-export) The symbol "VendorConfig" needs to be exported by the entry point index.d.ts getVendor(vendorConfig: VendorConfig | undefined): Promise; search(dn: string, options: SearchOptions): Promise; searchStreaming( @@ -272,4 +271,10 @@ export type UserTransformer = ( config: UserConfig, user: SearchEntry, ) => Promise; + +// @public +export type VendorConfig = { + dnAttributeName: string; + uuidAttributeName: string; +}; ``` diff --git a/plugins/catalog-backend-module-ldap/src/ldap/index.ts b/plugins/catalog-backend-module-ldap/src/ldap/index.ts index cfab5a4444..6d8fbd06d6 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/index.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/index.ts @@ -23,6 +23,7 @@ export type { UserConfig, BindConfig, TLSConfig, + VendorConfig, } from './config'; export type { LdapVendor } from './vendors'; export { From 516c76cf839ba63d440a6d6a02fb732aa120e936 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Fri, 6 Sep 2024 15:05:51 -0400 Subject: [PATCH 06/12] updated readVendorConfig Signed-off-by: Kashish Mittal --- plugins/catalog-backend-module-ldap/src/ldap/config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 1fbe93c533..097e0be02e 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -256,7 +256,7 @@ function readVendorConfig( return undefined; } return { - dnAttributeName: c.getString('dn'), + dnAttributeName: c.getString('dnAttributeName'), uuidAttributeName: c.getString('uuidAttributeName'), }; } @@ -433,7 +433,6 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] { return providersConfig.keys().map(id => { const c = providersConfig.getConfig(id); - const schedule = c.has('schedule') ? readSchedulerServiceTaskScheduleDefinitionFromConfig( c.getConfig('schedule'), From 01b112d625e8e8888cf3caa86ad55a2fd5b4c368 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Mon, 9 Sep 2024 12:23:00 -0400 Subject: [PATCH 07/12] make dnAttributeName and uuidAttributeName configs optional Signed-off-by: Kashish Mittal --- .../src/ldap/client.ts | 64 +++++++++---------- .../src/ldap/config.ts | 8 +-- .../src/ldap/vendors.ts | 4 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index 8703eee1ef..a04ca125a8 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -232,44 +232,44 @@ export class LdapClient { } this.vendor = this.getRootDSE() .then(root => { - if (!vendorConfig) { - if (root && root.raw?.forestFunctionality) { - // ActiveDirectoryVendor - return CreateLdapVendor( - { - dnAttributeName: 'distinguishedName', - uuidAttributeName: 'objectGUID', - }, - true, - ); - } else if (root && root.raw?.ipaDomainLevel) { - // FreeIpaVendor - return CreateLdapVendor( - { - dnAttributeName: 'dn', - uuidAttributeName: 'ipaUniqueID', - }, - false, - ); - } else if (root && 'aeRoot' in root.raw) { - // AEDirVendor - return CreateLdapVendor( - { dnAttributeName: 'dn', uuidAttributeName: 'entryUUID' }, - false, - ); - } - // DefaultLdapVendor + if (root && root.raw?.forestFunctionality) { + // ActiveDirectoryVendor return CreateLdapVendor( { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', + dnAttributeName: + vendorConfig?.dnAttributeName || 'distinguishedName', + uuidAttributeName: + vendorConfig?.uuidAttributeName || 'objectGUID', }, - !!(root && root.raw?.forestFunctionality), + true, + ); + } else if (root && root.raw?.ipaDomainLevel) { + // FreeIpaVendor + return CreateLdapVendor( + { + dnAttributeName: vendorConfig?.dnAttributeName || 'dn', + uuidAttributeName: + vendorConfig?.uuidAttributeName || 'ipaUniqueID', + }, + false, + ); + } else if (root && 'aeRoot' in root.raw) { + // AEDirVendor + return CreateLdapVendor( + { + dnAttributeName: vendorConfig?.dnAttributeName || 'dn', + uuidAttributeName: vendorConfig?.uuidAttributeName || 'entryUUID', + }, + false, ); } + // DefaultLdapVendor return CreateLdapVendor( - vendorConfig, - !!(root && root.raw?.forestFunctionality), + { + dnAttributeName: vendorConfig?.dnAttributeName, + uuidAttributeName: vendorConfig?.uuidAttributeName, + }, + false, ); }) .catch(err => { diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 097e0be02e..674451a0f8 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -177,12 +177,12 @@ export type VendorConfig = { /** * Attribute name for the distinguished name (DN) of an entry, */ - dnAttributeName: string; + dnAttributeName?: string; /** * Attribute name for the unique identifier (UUID) of an entry, */ - uuidAttributeName: string; + uuidAttributeName?: string; }; const defaultUserConfig = { @@ -256,8 +256,8 @@ function readVendorConfig( return undefined; } return { - dnAttributeName: c.getString('dnAttributeName'), - uuidAttributeName: c.getString('uuidAttributeName'), + dnAttributeName: c.getOptionalString('dnAttributeName'), + uuidAttributeName: c.getOptionalString('uuidAttributeName'), }; } diff --git a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts index d939811e90..f9cbcb7fa0 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts @@ -45,8 +45,8 @@ export const CreateLdapVendor = ( isActiveDirectoryVendor: boolean, ): LdapVendor => { return { - dnAttributeName: vendorConfig.dnAttributeName, - uuidAttributeName: vendorConfig.uuidAttributeName, + dnAttributeName: vendorConfig.dnAttributeName || `entryDN`, + uuidAttributeName: vendorConfig.uuidAttributeName || `entryUUID`, decodeStringAttribute: (entry, name) => { const decoder = (value: string | Buffer) => { if ( From 1cc08f134fb1c30bdeae7efb0c093701254a1a9c Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Mon, 9 Sep 2024 13:04:49 -0400 Subject: [PATCH 08/12] update api report Signed-off-by: Kashish Mittal --- plugins/catalog-backend-module-ldap/api-report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 8e45a99a70..b07b57055e 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -274,7 +274,7 @@ export type UserTransformer = ( // @public export type VendorConfig = { - dnAttributeName: string; - uuidAttributeName: string; + dnAttributeName?: string; + uuidAttributeName?: string; }; ``` From 32c785b4e77e86519eb9d0f0ff5591576dc7a080 Mon Sep 17 00:00:00 2001 From: Kashish Mittal <113269381+04kash@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:18:51 -0400 Subject: [PATCH 09/12] Update .changeset/short-pumpkins-prove.md Co-authored-by: Ben Lambert Signed-off-by: Kashish Mittal <113269381+04kash@users.noreply.github.com> --- .changeset/short-pumpkins-prove.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/short-pumpkins-prove.md b/.changeset/short-pumpkins-prove.md index bc273ac3ad..f54d3ee45d 100644 --- a/.changeset/short-pumpkins-prove.md +++ b/.changeset/short-pumpkins-prove.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-module-ldap': minor +'@backstage/plugin-catalog-backend-module-ldap': patch --- Add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in LDAP vendor From 9f0f05d75b5199279a39005fa370f57fb77ce7c2 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Tue, 10 Sep 2024 16:12:51 -0400 Subject: [PATCH 10/12] update config.ts and config.d.ts Signed-off-by: Kashish Mittal --- .../catalog-backend-module-ldap/config.d.ts | 42 +++++++++++++++++++ .../src/ldap/config.ts | 4 +- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend-module-ldap/config.d.ts b/plugins/catalog-backend-module-ldap/config.d.ts index 65914fdf74..e30aaab5d5 100644 --- a/plugins/catalog-backend-module-ldap/config.d.ts +++ b/plugins/catalog-backend-module-ldap/config.d.ts @@ -409,6 +409,20 @@ export interface Config { members?: string; }; }>; + /** + * Configuration for overriding the vendor-specific default attribute names. + */ + vendor?: { + /** + * Attribute name for the distinguished name (DN) of an entry, + */ + dnAttributeName?: string; + + /** + * Attribute name for the unique identifier (UUID) of an entry, + */ + uuidAttributeName?: string; + }; }>; }; @@ -638,6 +652,20 @@ export interface Config { members?: string; }; }; + /** + * Configuration for overriding the vendor-specific default attribute names. + */ + vendor?: { + /** + * Attribute name for the distinguished name (DN) of an entry, + */ + dnAttributeName?: string; + + /** + * Attribute name for the unique identifier (UUID) of an entry, + */ + uuidAttributeName?: string; + }; }; }; }; @@ -865,6 +893,20 @@ export interface Config { members?: string; }; }; + /** + * Configuration for overriding the vendor-specific default attribute names. + */ + vendor?: { + /** + * Attribute name for the distinguished name (DN) of an entry, + */ + dnAttributeName?: string; + + /** + * Attribute name for the unique identifier (UUID) of an entry, + */ + uuidAttributeName?: string; + }; }>; }; }; diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 674451a0f8..6fdf682b42 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -47,9 +47,7 @@ export type LdapProviderConfig = { groups: GroupConfig[]; // Schedule configuration for refresh tasks. schedule?: SchedulerServiceTaskScheduleDefinition; - // Configuration for LDAP vendor-specific attributes. If not specified, the default values will be used: - // - `dnAttributeName`: `entryDN` - // - `uuidAttributeName`: `entryUUID` + // Configuration for overriding the vendor-specific default attribute names. vendor?: VendorConfig; }; From 4bbf54497eac7cbcb267a0b5f25dcdc13c619320 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Thu, 12 Sep 2024 16:55:44 -0400 Subject: [PATCH 11/12] fixup based on feedback Signed-off-by: Kashish Mittal --- .../src/ldap/client.ts | 51 +--- .../src/ldap/read.test.ts | 268 ++++++++++++------ .../src/ldap/read.ts | 20 +- .../src/ldap/vendors.ts | 63 ++-- 4 files changed, 251 insertions(+), 151 deletions(-) diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index a04ca125a8..48a3eedfb8 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -19,9 +19,15 @@ import { readFile } from 'fs/promises'; import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs'; import { cloneDeep } from 'lodash'; import tlsLib from 'tls'; -import { BindConfig, TLSConfig, VendorConfig } from './config'; +import { BindConfig, TLSConfig } from './config'; import { createOptions, errorString } from './util'; -import { CreateLdapVendor, LdapVendor } from './vendors'; +import { + AEDirVendor, + ActiveDirectoryVendor, + DefaultLdapVendor, + FreeIpaVendor, + LdapVendor, +} from './vendors'; import { LoggerService } from '@backstage/backend-plugin-api'; /** @@ -226,51 +232,20 @@ export class LdapClient { * * @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor */ - async getVendor(vendorConfig: VendorConfig | undefined): Promise { + async getVendor(): Promise { if (this.vendor) { return this.vendor; } this.vendor = this.getRootDSE() .then(root => { if (root && root.raw?.forestFunctionality) { - // ActiveDirectoryVendor - return CreateLdapVendor( - { - dnAttributeName: - vendorConfig?.dnAttributeName || 'distinguishedName', - uuidAttributeName: - vendorConfig?.uuidAttributeName || 'objectGUID', - }, - true, - ); + return ActiveDirectoryVendor; } else if (root && root.raw?.ipaDomainLevel) { - // FreeIpaVendor - return CreateLdapVendor( - { - dnAttributeName: vendorConfig?.dnAttributeName || 'dn', - uuidAttributeName: - vendorConfig?.uuidAttributeName || 'ipaUniqueID', - }, - false, - ); + return FreeIpaVendor; } else if (root && 'aeRoot' in root.raw) { - // AEDirVendor - return CreateLdapVendor( - { - dnAttributeName: vendorConfig?.dnAttributeName || 'dn', - uuidAttributeName: vendorConfig?.uuidAttributeName || 'entryUUID', - }, - false, - ); + return AEDirVendor; } - // DefaultLdapVendor - return CreateLdapVendor( - { - dnAttributeName: vendorConfig?.dnAttributeName, - uuidAttributeName: vendorConfig?.uuidAttributeName, - }, - false, - ); + return DefaultLdapVendor; }) .catch(err => { this.vendor = 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 53f1dab090..5a7e625577 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -32,8 +32,11 @@ import { resolveRelations, } from './read'; import { RecursivePartial } from './util'; - -import { CreateLdapVendor } from './vendors'; +import { + ActiveDirectoryVendor, + DefaultLdapVendor, + FreeIpaVendor, +} from './vendors'; function user(data: RecursivePartial): UserEntity { return merge( @@ -81,11 +84,7 @@ describe('readLdapUsers', () => { afterEach(() => jest.resetAllMocks()); it('transfers all attributes from a default ldap vendor', async () => { - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); + client.getVendor.mockResolvedValue(DefaultLdapVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -100,7 +99,7 @@ describe('readLdapUsers', () => { }), ); }); - const userConfig: UserConfig[] = [ + const config: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -115,11 +114,7 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers( - client, - userConfig, - vendorConfig, - ); + const { users, userMemberOf } = await readLdapUsers(client, config, {}); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -146,12 +141,77 @@ describe('readLdapUsers', () => { ); }); - it('transfers all attributes from Microsoft Active Directory', async () => { + it('override default vendor configs', 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'], + customDN: ['dn-value'], + customUUID: ['uuid-value'], + }), + ); + }); + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, + }, + ]; + const vendorConfig: VendorConfig = { - dnAttributeName: 'distinguishedName', - uuidAttributeName: 'objectGUID', + dnAttributeName: 'customDN', + uuidAttributeName: 'customUUID', }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true)); + + const { users, userMemberOf } = await readLdapUsers( + client, + config, + vendorConfig, + ); + 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]: 'uuid-value', + }, + }, + spec: { + profile: { + displayName: 'cn-value', + email: 'mail-value', + picture: 'avatarUrl-value', + }, + memberOf: [], + }, + }), + ]); + + expect(userMemberOf).toEqual( + new Map([['dn-value', new Set(['x', 'y', 'z'])]]), + ); + }); + + it('transfers all attributes from Microsoft Active Directory', async () => { + client.getVendor.mockResolvedValue(ActiveDirectoryVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -171,7 +231,7 @@ describe('readLdapUsers', () => { }), ); }); - const userConfig: UserConfig[] = [ + const config: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -186,11 +246,7 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers( - client, - userConfig, - vendorConfig, - ); + const { users, userMemberOf } = await readLdapUsers(client, config, {}); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -218,11 +274,7 @@ describe('readLdapUsers', () => { }); it('transfers all attributes from FreeIPA', async () => { - const vendorConfig: VendorConfig = { - dnAttributeName: 'dn', - uuidAttributeName: 'ipaUniqueID', - }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); + client.getVendor.mockResolvedValue(FreeIpaVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -237,7 +289,7 @@ describe('readLdapUsers', () => { }), ); }); - const userConfig: UserConfig[] = [ + const config: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -252,11 +304,7 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers( - client, - userConfig, - vendorConfig, - ); + const { users, userMemberOf } = await readLdapUsers(client, config, {}); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -283,11 +331,7 @@ describe('readLdapUsers', () => { ); }); it('can process a list of UserConfigs', async () => { - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); + client.getVendor.mockResolvedValue(DefaultLdapVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -302,7 +346,7 @@ describe('readLdapUsers', () => { }), ); }); - const userConfig: UserConfig[] = [ + const config: UserConfig[] = [ { dn: 'ddd', options: {}, @@ -330,16 +374,12 @@ describe('readLdapUsers', () => { }, }, ]; - const { users } = await readLdapUsers(client, userConfig, vendorConfig); + const { users } = await readLdapUsers(client, config, {}); expect(users).toHaveLength(2); }); it('can process no UserConfigs', async () => { - const userConfig: UserConfig[] = []; - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - const { users } = await readLdapUsers(client, userConfig, vendorConfig); + const config: UserConfig[] = []; + const { users } = await readLdapUsers(client, config, {}); expect(users).toHaveLength(0); }); }); @@ -353,11 +393,7 @@ describe('readLdapGroups', () => { afterEach(() => jest.resetAllMocks()); it('transfers all attributes from a default ldap vendor', async () => { - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); + client.getVendor.mockResolvedValue(DefaultLdapVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -373,7 +409,7 @@ describe('readLdapGroups', () => { }), ); }); - const groupConfig: GroupConfig[] = [ + const config: GroupConfig[] = [ { dn: 'ddd', options: {}, @@ -392,8 +428,8 @@ describe('readLdapGroups', () => { ]; const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, - groupConfig, - vendorConfig, + config, + {}, ); expect(groups).toEqual([ expect.objectContaining({ @@ -426,11 +462,7 @@ describe('readLdapGroups', () => { }); it('transfers all attributes from Microsoft Active Directory', async () => { - const vendorConfig: VendorConfig = { - dnAttributeName: 'distinguishedName', - uuidAttributeName: 'objectGUID', - }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true)); + client.getVendor.mockResolvedValue(ActiveDirectoryVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -451,7 +483,7 @@ describe('readLdapGroups', () => { }), ); }); - const groupConfig: GroupConfig[] = [ + const config: GroupConfig[] = [ { dn: 'ddd', options: {}, @@ -470,8 +502,8 @@ describe('readLdapGroups', () => { ]; const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, - groupConfig, - vendorConfig, + config, + {}, ); expect(groups).toEqual([ expect.objectContaining({ @@ -503,12 +535,83 @@ describe('readLdapGroups', () => { ); }); - it('can process a list of GroupConfigs', async () => { + 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 config: GroupConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + type: 'tt', + memberOf: 'memberOf', + members: 'member', + }, + }, + ]; + const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', + dnAttributeName: 'customDN', + uuidAttributeName: 'customUUID', }; - client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false)); + + const { groups, groupMember, groupMemberOf } = await readLdapGroups( + client, + config, + vendorConfig, + ); + expect(groups).toEqual([ + expect.objectContaining({ + metadata: { + name: 'cn-value', + description: 'description-value', + annotations: { + [LDAP_DN_ANNOTATION]: 'dn-value', + [LDAP_RDN_ANNOTATION]: 'cn-value', + [LDAP_UUID_ANNOTATION]: 'uuid-value', + }, + }, + spec: { + type: 'type-value', + profile: { + displayName: 'cn-value', + email: 'mail-value', + picture: 'avatarUrl-value', + }, + children: [], + }, + }), + ]); + expect(groupMember).toEqual( + new Map([['dn-value', new Set(['e', 'f', 'g'])]]), + ); + expect(groupMemberOf).toEqual( + new Map([['dn-value', new Set(['x', 'y', 'z'])]]), + ); + }); + + it('can process a list of GroupConfigs', async () => { + client.getVendor.mockResolvedValue(DefaultLdapVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { await fn( searchEntry({ @@ -524,7 +627,7 @@ describe('readLdapGroups', () => { }), ); }); - const groupConfig: GroupConfig[] = [ + const config: GroupConfig[] = [ { dn: 'ddd', options: {}, @@ -556,17 +659,13 @@ describe('readLdapGroups', () => { }, }, ]; - const { groups } = await readLdapGroups(client, groupConfig, vendorConfig); + const { groups } = await readLdapGroups(client, config, {}); expect(groups).toHaveLength(2); }); it('can process no GroupConfigs', async () => { - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - const groupConfig: GroupConfig[] = []; - const { groups } = await readLdapGroups(client, groupConfig, vendorConfig); + const config: GroupConfig[] = []; + const { groups } = await readLdapGroups(client, config, {}); expect(groups).toHaveLength(0); }); }); @@ -726,12 +825,8 @@ describe('defaultUserTransformer', () => { entryDN: ['dn-value'], entryUUID: ['uuid-value'], }); - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - const defaultLdapVendor = CreateLdapVendor(vendorConfig, false); - let output = await defaultUserTransformer(defaultLdapVendor, config, entry); + + let output = await defaultUserTransformer(DefaultLdapVendor, config, entry); expect(output).toEqual({ apiVersion: 'backstage.io/v1beta1', kind: 'User', @@ -754,7 +849,7 @@ describe('defaultUserTransformer', () => { (output!.metadata.annotations as any).c = 7; // exact same inputs again - output = await defaultUserTransformer(defaultLdapVendor, config, entry); + output = await defaultUserTransformer(DefaultLdapVendor, config, entry); expect(output).toEqual({ apiVersion: 'backstage.io/v1beta1', kind: 'User', @@ -808,13 +903,8 @@ describe('defaultGroupTransformer', () => { entryUUID: ['uuid-value'], }); - const vendorConfig: VendorConfig = { - dnAttributeName: 'entryDN', - uuidAttributeName: 'entryUUID', - }; - const defaultLdapVendor = CreateLdapVendor(vendorConfig, false); let output = await defaultGroupTransformer( - defaultLdapVendor, + DefaultLdapVendor, config, entry, ); @@ -842,7 +932,7 @@ describe('defaultGroupTransformer', () => { (output!.metadata.annotations as any).c = 7; // exact same inputs again - output = await defaultGroupTransformer(defaultLdapVendor, config, entry); + output = await defaultGroupTransformer(DefaultLdapVendor, config, entry); expect(output).toEqual({ apiVersion: 'backstage.io/v1beta1', kind: 'Group', diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index 3b59ac35ac..e1cfc0730e 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -116,8 +116,14 @@ export async function readLdapUsers( } const entities: UserEntity[] = []; const userMemberOf: Map> = new Map(); - - const vendor = await client.getVendor(vendorConfig); + const vendorDefaults = await client.getVendor(); + const vendor: LdapVendor = { + dnAttributeName: + vendorConfig?.dnAttributeName ?? vendorDefaults.dnAttributeName, + uuidAttributeName: + vendorConfig?.uuidAttributeName ?? vendorDefaults.uuidAttributeName, + decodeStringAttribute: vendorDefaults.decodeStringAttribute, + }; const transformer = opts?.transformer ?? defaultUserTransformer; for (const cfg of userConfig) { @@ -228,7 +234,15 @@ export async function readLdapGroups( const groupMemberOf: Map> = new Map(); const groupMember: Map> = new Map(); - const vendor = await client.getVendor(vendorConfig); + const vendorDefaults = await client.getVendor(); + const vendor: LdapVendor = { + dnAttributeName: + vendorConfig?.dnAttributeName ?? vendorDefaults.dnAttributeName, + uuidAttributeName: + vendorConfig?.uuidAttributeName ?? vendorDefaults.uuidAttributeName, + decodeStringAttribute: vendorDefaults.decodeStringAttribute, + }; + const transformer = opts?.transformer ?? defaultGroupTransformer; for (const cfg of groupConfig) { diff --git a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts index f9cbcb7fa0..9f4e365c38 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts @@ -15,7 +15,6 @@ */ import { SearchEntry } from 'ldapjs'; -import { VendorConfig } from './config'; /** * An LDAP Vendor handles unique nuances between different vendors. @@ -40,26 +39,48 @@ export type LdapVendor = { decodeStringAttribute: (entry: SearchEntry, name: string) => string[]; }; -export const CreateLdapVendor = ( - vendorConfig: VendorConfig, - isActiveDirectoryVendor: boolean, -): LdapVendor => { - return { - dnAttributeName: vendorConfig.dnAttributeName || `entryDN`, - uuidAttributeName: vendorConfig.uuidAttributeName || `entryUUID`, - decodeStringAttribute: (entry, name) => { - const decoder = (value: string | Buffer) => { - if ( - isActiveDirectoryVendor && - name === vendorConfig.uuidAttributeName - ) { - return formatGUID(value); - } - return value.toString(); - }; - return decode(entry, name, decoder); - }, - }; +export const DefaultLdapVendor: LdapVendor = { + dnAttributeName: 'entryDN', + uuidAttributeName: 'entryUUID', + decodeStringAttribute: (entry, name) => { + return decode(entry, name, value => { + return value.toString(); + }); + }, +}; + +export const ActiveDirectoryVendor: LdapVendor = { + dnAttributeName: 'distinguishedName', + uuidAttributeName: 'objectGUID', + decodeStringAttribute: (entry, name) => { + const decoder = (value: string | Buffer) => { + if (name === ActiveDirectoryVendor.uuidAttributeName) { + return formatGUID(value); + } + return value.toString(); + }; + return decode(entry, name, decoder); + }, +}; + +export const FreeIpaVendor: LdapVendor = { + dnAttributeName: 'dn', + uuidAttributeName: 'ipaUniqueID', + decodeStringAttribute: (entry, name) => { + return decode(entry, name, value => { + return value.toString(); + }); + }, +}; + +export const AEDirVendor: LdapVendor = { + dnAttributeName: 'dn', + uuidAttributeName: 'entryUUID', + decodeStringAttribute: (entry, name) => { + return decode(entry, name, value => { + return value.toString(); + }); + }, }; // Decode an attribute to a consumer From b7bca5fcd524d4fb6f1a3590b65969d8e8316ca0 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Thu, 12 Sep 2024 17:11:10 -0400 Subject: [PATCH 12/12] update api report Signed-off-by: Kashish Mittal --- plugins/catalog-backend-module-ldap/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index b07b57055e..a786d48fdb 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -95,7 +95,7 @@ export class LdapClient { tls?: TLSConfig, ): Promise; getRootDSE(): Promise; - getVendor(vendorConfig: VendorConfig | undefined): Promise; + getVendor(): Promise; search(dn: string, options: SearchOptions): Promise; searchStreaming( dn: string,