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:
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user