diff --git a/.changeset/early-cobras-compare.md b/.changeset/early-cobras-compare.md new file mode 100644 index 0000000000..5a107fedaa --- /dev/null +++ b/.changeset/early-cobras-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Fixed user/group membership mapping for case insensitive returned dn results for catalog-ldap diff --git a/plugins/catalog-backend-module-ldap/report.api.md b/plugins/catalog-backend-module-ldap/report.api.md index e8b20073a3..551800d9b4 100644 --- a/plugins/catalog-backend-module-ldap/report.api.md +++ b/plugins/catalog-backend-module-ldap/report.api.md @@ -205,6 +205,7 @@ export type LdapProviderConfig = { export type LdapVendor = { dnAttributeName: string; uuidAttributeName: string; + dnCaseSensitive?: boolean; decodeStringAttribute: (entry: SearchEntry, name: string) => string[]; }; @@ -274,6 +275,7 @@ export type UserTransformer = ( export type VendorConfig = { dnAttributeName?: string; uuidAttributeName?: string; + dnCaseSensitive?: boolean; }; // Warnings were encountered during analysis: diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 6fdf682b42..4b53034d36 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -181,6 +181,13 @@ export type VendorConfig = { * Attribute name for the unique identifier (UUID) of an entry, */ uuidAttributeName?: string; + + /** + * Attribute to determine if we need to force the DN and members/memberOf values to be forced to same case. + * Some providers may provide lowercase members but multicase DN names which causes the group filtering to break. + * The default is off, but turning this on forces the inbound DN values and all member values to lowercase. + */ + dnCaseSensitive?: boolean; }; const defaultUserConfig = { @@ -256,6 +263,7 @@ function readVendorConfig( return { dnAttributeName: c.getOptionalString('dnAttributeName'), uuidAttributeName: c.getOptionalString('uuidAttributeName'), + dnCaseSensitive: c.getOptionalBoolean('dnCaseSensitive'), }; } 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 5a7e625577..20917a0fcc 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -330,6 +330,128 @@ describe('readLdapUsers', () => { new Map([['dn-value', new Set(['x', 'y', 'z'])]]), ); }); + + it('transfers all attributes from for vendorDN case sensitivity', async () => { + const vendor = DefaultLdapVendor; + vendor.dnCaseSensitive = true; + client.getVendor.mockResolvedValue(vendor); + client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { + await fn( + searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'Y', 'z'], + entryDN: ['dn-VALUE'], + entryUUID: ['uuid-value'], + }), + ); + }); + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, + }, + ]; + const { users, userMemberOf } = await readLdapUsers(client, config, vendor); + // reset dnCaseSensitivity + vendor.dnCaseSensitive = false; + 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('fails to transfer all attributes from for due case sensitivity', async () => { + const vendor = DefaultLdapVendor; + client.getVendor.mockResolvedValue(vendor); + client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { + await fn( + searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'Y', 'z'], + entryDN: ['dn-VALUE'], + entryUUID: ['uuid-value'], + }), + ); + }); + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, + }, + ]; + const { users, userMemberOf } = await readLdapUsers(client, config, vendor); + 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('can process a list of UserConfigs', async () => { client.getVendor.mockResolvedValue(DefaultLdapVendor); client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { @@ -955,3 +1077,170 @@ describe('defaultGroupTransformer', () => { }); }); }); + +/** + * Case Insensitivity Tests + */ +describe('defaultUserTransformerWithCaseSensitiveDNs', () => { + it('can set things safely', async () => { + const config: UserConfig = { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + displayName: 'cn', + email: 'mail', + memberOf: 'memberOf', + }, + set: { + 'metadata.annotations.a': 1, + 'metadata.annotations': { a: 2, b: 3 }, + }, + }; + + const entry = searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + entryDN: ['dn-VALUE'], + entryUUID: ['uuid-value'], + }); + + const vendor = DefaultLdapVendor; + vendor.dnCaseSensitive = true; + let output = await defaultUserTransformer(vendor, config, entry); + expect(output).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/ldap-dn': 'dn-value', + 'backstage.io/ldap-rdn': 'uid-value', + 'backstage.io/ldap-uuid': 'uuid-value', + a: 2, + b: 3, + }, + name: 'uid-value', + }, + spec: { + memberOf: [], + profile: { displayName: 'cn-value', email: 'mail-value' }, + }, + }); + + (output!.metadata.annotations as any).c = 7; + + // exact same inputs again + output = await defaultUserTransformer(vendor, config, entry); + // reset dnCaseSensitivity + vendor.dnCaseSensitive = false; + expect(output).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/ldap-dn': 'dn-value', + 'backstage.io/ldap-rdn': 'uid-value', + 'backstage.io/ldap-uuid': 'uuid-value', + a: 2, + b: 3, + }, + name: 'uid-value', + }, + spec: { + memberOf: [], + profile: { displayName: 'cn-value', email: 'mail-value' }, + }, + }); + }); +}); + +describe('defaultGroupTransformerWithCaseSensitiveDNs', () => { + it('can set things safely', async () => { + const config: GroupConfig = { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + displayName: 'cn', + email: 'mail', + description: 'description', + type: 'type', + members: 'members', + memberOf: 'memberOf', + }, + set: { + 'metadata.annotations.a': 1, + 'metadata.annotations': { a: 2, b: 3 }, + }, + }; + + const entry = searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + entryDN: ['dn-VALUE'], + entryUUID: ['uuid-value'], + }); + + const vendor = DefaultLdapVendor; + vendor.dnCaseSensitive = true; + + let output = await defaultGroupTransformer(vendor, config, entry); + expect(output).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/ldap-dn': 'dn-value', + 'backstage.io/ldap-rdn': 'uid-value', + 'backstage.io/ldap-uuid': 'uuid-value', + a: 2, + b: 3, + }, + description: 'description-value', + name: 'uid-value', + }, + spec: { + type: 'unknown', + children: [], + profile: { displayName: 'cn-value', email: 'mail-value' }, + }, + }); + + (output!.metadata.annotations as any).c = 7; + + // exact same inputs again + output = await defaultGroupTransformer(vendor, config, entry); + // reset dnCaseSensitivity + vendor.dnCaseSensitive = false; + expect(output).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/ldap-dn': 'dn-value', + 'backstage.io/ldap-rdn': 'uid-value', + 'backstage.io/ldap-uuid': 'uuid-value', + a: 2, + b: 3, + }, + description: 'description-value', + name: 'uid-value', + }, + spec: { + type: 'unknown', + children: [], + profile: { displayName: 'cn-value', email: 'mail-value' }, + }, + }); + }); +}); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index e1cfc0730e..6fbd2a7e86 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -80,7 +80,10 @@ export async function defaultUserTransformer( entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v; }); mapStringAttr(entry, vendor, vendor.dnAttributeName, v => { - entity.metadata.annotations![LDAP_DN_ANNOTATION] = v; + entity.metadata.annotations![LDAP_DN_ANNOTATION] = getCaseSensitivityValue( + v, + vendor, + ); }); mapStringAttr(entry, vendor, map.displayName, v => { entity.spec.profile!.displayName = v; @@ -122,6 +125,8 @@ export async function readLdapUsers( vendorConfig?.dnAttributeName ?? vendorDefaults.dnAttributeName, uuidAttributeName: vendorConfig?.uuidAttributeName ?? vendorDefaults.uuidAttributeName, + dnCaseSensitive: + vendorConfig?.dnCaseSensitive ?? vendorDefaults.dnCaseSensitive, decodeStringAttribute: vendorDefaults.decodeStringAttribute, }; const transformer = opts?.transformer ?? defaultUserTransformer; @@ -190,7 +195,10 @@ export async function defaultGroupTransformer( entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v; }); mapStringAttr(entry, vendor, vendor.dnAttributeName, v => { - entity.metadata.annotations![LDAP_DN_ANNOTATION] = v; + entity.metadata.annotations![LDAP_DN_ANNOTATION] = getCaseSensitivityValue( + v, + vendor, + ); }); mapStringAttr(entry, vendor, map.type, v => { entity.spec.type = v; @@ -240,6 +248,8 @@ export async function readLdapGroups( vendorConfig?.dnAttributeName ?? vendorDefaults.dnAttributeName, uuidAttributeName: vendorConfig?.uuidAttributeName ?? vendorDefaults.uuidAttributeName, + dnCaseSensitive: + vendorConfig?.dnCaseSensitive ?? vendorDefaults.dnCaseSensitive, decodeStringAttribute: vendorDefaults.decodeStringAttribute, }; @@ -341,11 +351,23 @@ function mapReferencesAttr( const values = vendor.decodeStringAttribute(entry, attributeName); const dn = vendor.decodeStringAttribute(entry, vendor.dnAttributeName); if (values && dn && dn.length === 1) { - setter(dn[0], values); + if (vendor.dnCaseSensitive) { + setter( + dn[0].toLowerCase(), + values.map(v => v.toLowerCase()), + ); + } else { + setter(dn[0], values); + } } } } +/** Validates value exists and if required forced sensitivty value to lowercase */ +function getCaseSensitivityValue(value: string, vendor: LdapVendor) { + return value && vendor.dnCaseSensitive ? value.toLowerCase() : value; +} + // Inserts a number of values in a key-values mapping function ensureItems( target: Map>, diff --git a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts index 9f4e365c38..8b84002c20 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/vendors.ts @@ -30,6 +30,12 @@ export type LdapVendor = { * The attribute name that holds a universal unique identifier for an entry. */ uuidAttributeName: string; + + /** + * The attribute that determines behaviour of the (dn,members,memberOf) for entries. + */ + dnCaseSensitive?: boolean; + /** * Decode ldap entry values for a given attribute name to their string representation. *