Merge pull request #26511 from 04kash/ldap-vendor-config

add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in the ldapOrg processor
This commit is contained in:
Fredrik Adelöw
2024-09-17 08:51:05 +02:00
committed by GitHub
9 changed files with 284 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-ldap': patch
---
Add support for optional configuration of `dnAttributeName` and `uuidAttributeName` in LDAP vendor
@@ -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<UserEntity | undefined>;
// @public
export type VendorConfig = {
dnAttributeName?: string;
uuidAttributeName?: string;
};
```
+42
View File
@@ -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;
};
}>;
};
};
@@ -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;
@@ -23,6 +23,7 @@ export type {
UserConfig,
BindConfig,
TLSConfig,
VendorConfig,
} from './config';
export type { LdapVendor } from './vendors';
export {
@@ -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);
});
});
@@ -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<string, Set<string>>; // 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<string, Set<string>> = 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<string, Set<string>>; // DN -> DN or UUID of groups
groupMember: Map<string, Set<string>>; // 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<string, Set<string>> = new Map();
const groupMember: Map<string, Set<string>> = 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 },
);
@@ -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,
@@ -109,6 +109,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
client,
provider.users,
provider.groups,
provider.vendor,
{
groupTransformer: this.groupTransformer,
userTransformer: this.userTransformer,