fix: ldap allow case sensitivity settings to force dn and memberOf to be lowercase

Signed-off-by: John Redwood <john.r.k.redwood@gmail.com>
This commit is contained in:
John Redwood
2024-10-01 21:37:31 +10:00
parent 9d47beed8a
commit 884a86c89f
6 changed files with 335 additions and 3 deletions
+5
View File
@@ -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
@@ -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:
@@ -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'),
};
}
@@ -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' },
},
});
});
});
@@ -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<string, Set<string>>,
@@ -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.
*