From a956e4473edfab43aa20b47e10e4a1795f7a23f6 Mon Sep 17 00:00:00 2001 From: Kashish Mittal Date: Thu, 5 Sep 2024 15:47:38 -0400 Subject: [PATCH] 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,