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