Merge pull request #26923 from veenarm/fix-ldap-module-user-mapping
fix: ldap allow case sensitivity settings to force dn and memberOf to…
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-ldap': patch
|
||||
---
|
||||
|
||||
Added a `dnCaseSensitive` flag to support LDAP servers with mixed-case attributes.
|
||||
@@ -55,6 +55,8 @@ backend.add(import('@backstage/plugin-catalog-backend-module-ldap'));
|
||||
The following configuration is a small example of how a setup could look for
|
||||
importing groups and users from a corporate LDAP server.
|
||||
|
||||
Users and Groups can be configured for multiple `dn` entries as an array.
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
providers:
|
||||
@@ -65,25 +67,24 @@ catalog:
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
users:
|
||||
dn: ou=people,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (uid=*)
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
- dn: ou=people,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (uid=*)
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
groups:
|
||||
dn: ou=access,ou=groups,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
- dn: ou=access,ou=groups,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
```
|
||||
|
||||
These config blocks have a lot of options in them, so we will describe each
|
||||
"root" key within the block separately.
|
||||
These config blocks have a lot of options in them, so we will describe each "root" key within the block separately.
|
||||
|
||||
> NOTE:
|
||||
>
|
||||
@@ -303,6 +304,33 @@ map:
|
||||
members: member
|
||||
```
|
||||
|
||||
## Optional Vendor Configuration
|
||||
|
||||
In case the LDAP vendor isn't automatically detected by the module, an optional `vendor` configuration section is available which allows overriding the location for `dn` and `uuid` settings, and a case sensitivity setting.
|
||||
|
||||
#### vendor.dnAttributeName
|
||||
|
||||
Allows explicitly defining the name of the attribute that stores each entry's DN.
|
||||
|
||||
#### vendor.uuidAttributeName
|
||||
|
||||
Allows explicitly defining the name of the attribute that stores each entry's UUID.
|
||||
|
||||
#### vendor.dnCaseSensitive
|
||||
|
||||
Provides the ability to ignore case sensitivity issues with user/group mappings. If set to true, the ingestion will link user/members to groups whether their `dn`, `member`, or `memberOf` values have the right case or not.
|
||||
|
||||
```yaml
|
||||
vendor:
|
||||
# Attribute name override for the distinguished name (DN) of an entry.
|
||||
dnAttributeName: dn
|
||||
# Attribute name override for the unique identifier (UUID) of an entry.
|
||||
uuidAttributeName: uuid
|
||||
# Attribute to force values provided from dn and members/memberOf values all to lowercase.
|
||||
# This is to resolve potential user/group mapping issues if case differences on dn strings.
|
||||
dnCaseSensitive: true
|
||||
```
|
||||
|
||||
## Customize the Provider
|
||||
|
||||
In case you want to customize the ingested entities, the provider allows to pass
|
||||
|
||||
@@ -205,6 +205,7 @@ export type LdapProviderConfig = {
|
||||
export type LdapVendor = {
|
||||
dnAttributeName: string;
|
||||
uuidAttributeName: string;
|
||||
dnCaseSensitive?: boolean;
|
||||
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
|
||||
};
|
||||
|
||||
@@ -274,5 +275,6 @@ export type UserTransformer = (
|
||||
export type VendorConfig = {
|
||||
dnAttributeName?: string;
|
||||
uuidAttributeName?: string;
|
||||
dnCaseSensitive?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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,25 @@ 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].toLocaleLowerCase('en-US'),
|
||||
values.map(v => v.toLocaleLowerCase('en-US')),
|
||||
);
|
||||
} 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.toLocaleLowerCase('en-US')
|
||||
: 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