diff --git a/.changeset/short-pumpkins-prove.md b/.changeset/short-pumpkins-prove.md new file mode 100644 index 0000000000..f54d3ee45d --- /dev/null +++ b/.changeset/short-pumpkins-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in LDAP vendor diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 40bb9d81ae..a786d48fdb 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -200,6 +200,7 @@ export type LdapProviderConfig = { users: UserConfig[]; groups: GroupConfig[]; schedule?: SchedulerServiceTaskScheduleDefinition; + vendor?: VendorConfig; }; // @public @@ -225,6 +226,7 @@ export function readLdapOrg( client: LdapClient, userConfig: UserConfig[], groupConfig: GroupConfig[], + vendorConfig: VendorConfig | undefined, options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; @@ -269,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/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 d7e1035545..6fdf682b42 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -47,6 +47,8 @@ export type LdapProviderConfig = { groups: GroupConfig[]; // Schedule configuration for refresh tasks. schedule?: SchedulerServiceTaskScheduleDefinition; + // Configuration for overriding the vendor-specific default attribute names. + vendor?: VendorConfig; }; /** @@ -161,6 +163,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 +247,18 @@ function readBindConfig( }; } +function readVendorConfig( + c: Config | undefined, +): LdapProviderConfig['vendor'] | undefined { + if (!c) { + return undefined; + } + return { + dnAttributeName: c.getOptionalString('dnAttributeName'), + uuidAttributeName: c.getOptionalString('uuidAttributeName'), + }; +} + function readOptionsConfig(c: Config | undefined): SearchOptions { if (!c) { return {}; @@ -375,6 +409,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; @@ -396,7 +431,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'), @@ -426,6 +460,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/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 { 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..5a7e625577 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, @@ -114,7 +114,7 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers(client, config); + const { users, userMemberOf } = await readLdapUsers(client, config, {}); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -141,6 +141,75 @@ describe('readLdapUsers', () => { ); }); + 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: 'customDN', + uuidAttributeName: 'customUUID', + }; + + 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) => { @@ -177,7 +246,7 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers(client, config); + const { users, userMemberOf } = await readLdapUsers(client, config, {}); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -235,7 +304,7 @@ describe('readLdapUsers', () => { }, }, ]; - const { users, userMemberOf } = await readLdapUsers(client, config); + const { users, userMemberOf } = await readLdapUsers(client, config, {}); expect(users).toEqual([ expect.objectContaining({ metadata: { @@ -305,12 +374,12 @@ describe('readLdapUsers', () => { }, }, ]; - const { users } = await readLdapUsers(client, config); + const { users } = await readLdapUsers(client, config, {}); expect(users).toHaveLength(2); }); it('can process no UserConfigs', async () => { const config: UserConfig[] = []; - const { users } = await readLdapUsers(client, config); + const { users } = await readLdapUsers(client, config, {}); expect(users).toHaveLength(0); }); }); @@ -360,6 +429,7 @@ describe('readLdapGroups', () => { const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, config, + {}, ); expect(groups).toEqual([ expect.objectContaining({ @@ -433,6 +503,7 @@ describe('readLdapGroups', () => { const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, config, + {}, ); expect(groups).toEqual([ expect.objectContaining({ @@ -464,6 +535,81 @@ 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 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: 'customDN', + uuidAttributeName: 'customUUID', + }; + + 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) => { @@ -513,13 +659,13 @@ describe('readLdapGroups', () => { }, }, ]; - const { groups } = await readLdapGroups(client, config); + const { groups } = await readLdapGroups(client, config, {}); expect(groups).toHaveLength(2); }); it('can process no GroupConfigs', async () => { const config: GroupConfig[] = []; - const { groups } = await readLdapGroups(client, config); + const { groups } = await readLdapGroups(client, config, {}); expect(groups).toHaveLength(0); }); }); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index 9e6e11a0bc..e1cfc0730e 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,29 @@ export async function defaultUserTransformer( */ export async function readLdapUsers( client: LdapClient, - config: UserConfig[], + userConfig: UserConfig[], + vendorConfig: VendorConfig | undefined, 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 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 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 +217,8 @@ export async function defaultGroupTransformer( */ export async function readLdapGroups( client: LdapClient, - config: GroupConfig[], + groupConfig: GroupConfig[], + vendorConfig: VendorConfig | undefined, opts?: { transformer?: GroupTransformer; }, @@ -219,17 +227,25 @@ 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 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 config) { + for (const cfg of groupConfig) { const { dn, map, options } = cfg; await client.searchStreaming(dn, options, async entry => { @@ -275,6 +291,7 @@ export async function readLdapOrg( client: LdapClient, userConfig: UserConfig[], groupConfig: GroupConfig[], + vendorConfig: VendorConfig | undefined, options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; @@ -287,12 +304,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/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,