make dnAttributeName and uuidAttributeName for ldap vendors configurable

Signed-off-by: Kashish Mittal <kmittal@redhat.com>
This commit is contained in:
Kashish Mittal
2024-09-05 15:47:38 -04:00
parent cd105809cb
commit a956e4473e
8 changed files with 196 additions and 107 deletions
@@ -19,15 +19,9 @@ import { readFile } from 'fs/promises';
import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs';
import { cloneDeep } from 'lodash';
import tlsLib from 'tls';
import { BindConfig, TLSConfig } from './config';
import { BindConfig, TLSConfig, VendorConfig } from './config';
import { createOptions, errorString } from './util';
import {
AEDirVendor,
ActiveDirectoryVendor,
DefaultLdapVendor,
FreeIpaVendor,
LdapVendor,
} from './vendors';
import { CreateLdapVendor, LdapVendor } from './vendors';
import { LoggerService } from '@backstage/backend-plugin-api';
/**
@@ -232,20 +226,16 @@ export class LdapClient {
*
* @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor
*/
async getVendor(): Promise<LdapVendor> {
async getVendor(vendorConfig: VendorConfig): Promise<LdapVendor> {
if (this.vendor) {
return this.vendor;
}
this.vendor = this.getRootDSE()
.then(root => {
if (root && root.raw?.forestFunctionality) {
return ActiveDirectoryVendor;
} else if (root && root.raw?.ipaDomainLevel) {
return FreeIpaVendor;
} else if (root && 'aeRoot' in root.raw) {
return AEDirVendor;
}
return DefaultLdapVendor;
return CreateLdapVendor(
vendorConfig,
!!(root && root.raw?.forestFunctionality),
);
})
.catch(err => {
this.vendor = undefined;
@@ -78,6 +78,10 @@ describe('readLdapConfig', () => {
},
},
],
vendor: {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
},
},
];
expect(actual).toEqual(expected);
@@ -151,6 +155,10 @@ describe('readLdapConfig', () => {
},
},
],
vendor: {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
},
},
];
expect(actual).toEqual(expected);
@@ -291,6 +299,10 @@ describe('readLdapConfig', () => {
},
},
],
vendor: {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
},
},
];
expect(actual).toEqual(expected);
@@ -47,6 +47,10 @@ export type LdapProviderConfig = {
groups: GroupConfig[];
// Schedule configuration for refresh tasks.
schedule?: SchedulerServiceTaskScheduleDefinition;
// Configuration for LDAP vendor-specific attributes. If not specified, the default values will be used:
// - `dnAttributeName`: `entryDN`
// - `uuidAttributeName`: `entryUUID`
vendor: VendorConfig;
};
/**
@@ -161,6 +165,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 +249,21 @@ function readBindConfig(
};
}
function readVendorConfig(
c: Config | undefined,
): LdapProviderConfig['vendor'] | undefined {
if (!c) {
return {
dnAttributeName: `entryDN`,
uuidAttributeName: `entryUUID`,
};
}
return {
dnAttributeName: c.getString('dn'),
uuidAttributeName: c.getString('uuidAttributeName'),
};
}
function readOptionsConfig(c: Config | undefined): SearchOptions {
if (!c) {
return {};
@@ -375,6 +414,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;
@@ -426,6 +466,7 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] {
return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent);
}),
schedule,
vendor: readVendorConfig(c.getOptionalConfig('vendor')),
};
return freeze(newConfig) as LdapProviderConfig;
@@ -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,
@@ -32,11 +32,8 @@ import {
resolveRelations,
} from './read';
import { RecursivePartial } from './util';
import {
ActiveDirectoryVendor,
DefaultLdapVendor,
FreeIpaVendor,
} from './vendors';
import { CreateLdapVendor } from './vendors';
function user(data: RecursivePartial<UserEntity>): UserEntity {
return merge(
@@ -84,7 +81,11 @@ describe('readLdapUsers', () => {
afterEach(() => jest.resetAllMocks());
it('transfers all attributes from a default ldap vendor', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -99,7 +100,7 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig[] = [
const userConfig: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -114,7 +115,11 @@ describe('readLdapUsers', () => {
},
},
];
const { users, userMemberOf } = await readLdapUsers(client, config);
const { users, userMemberOf } = await readLdapUsers(
client,
userConfig,
vendorConfig,
);
expect(users).toEqual([
expect.objectContaining({
metadata: {
@@ -142,7 +147,11 @@ describe('readLdapUsers', () => {
});
it('transfers all attributes from Microsoft Active Directory', async () => {
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'distinguishedName',
uuidAttributeName: 'objectGUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -162,7 +171,7 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig[] = [
const userConfig: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -177,7 +186,11 @@ describe('readLdapUsers', () => {
},
},
];
const { users, userMemberOf } = await readLdapUsers(client, config);
const { users, userMemberOf } = await readLdapUsers(
client,
userConfig,
vendorConfig,
);
expect(users).toEqual([
expect.objectContaining({
metadata: {
@@ -205,7 +218,11 @@ describe('readLdapUsers', () => {
});
it('transfers all attributes from FreeIPA', async () => {
client.getVendor.mockResolvedValue(FreeIpaVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'dn',
uuidAttributeName: 'ipaUniqueID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -220,7 +237,7 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig[] = [
const userConfig: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -235,7 +252,11 @@ describe('readLdapUsers', () => {
},
},
];
const { users, userMemberOf } = await readLdapUsers(client, config);
const { users, userMemberOf } = await readLdapUsers(
client,
userConfig,
vendorConfig,
);
expect(users).toEqual([
expect.objectContaining({
metadata: {
@@ -262,7 +283,11 @@ describe('readLdapUsers', () => {
);
});
it('can process a list of UserConfigs', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -277,7 +302,7 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig[] = [
const userConfig: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -305,12 +330,16 @@ describe('readLdapUsers', () => {
},
},
];
const { users } = await readLdapUsers(client, config);
const { users } = await readLdapUsers(client, userConfig, vendorConfig);
expect(users).toHaveLength(2);
});
it('can process no UserConfigs', async () => {
const config: UserConfig[] = [];
const { users } = await readLdapUsers(client, config);
const userConfig: UserConfig[] = [];
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const { users } = await readLdapUsers(client, userConfig, vendorConfig);
expect(users).toHaveLength(0);
});
});
@@ -324,7 +353,11 @@ describe('readLdapGroups', () => {
afterEach(() => jest.resetAllMocks());
it('transfers all attributes from a default ldap vendor', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -340,7 +373,7 @@ describe('readLdapGroups', () => {
}),
);
});
const config: GroupConfig[] = [
const groupConfig: GroupConfig[] = [
{
dn: 'ddd',
options: {},
@@ -359,7 +392,8 @@ describe('readLdapGroups', () => {
];
const { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
config,
groupConfig,
vendorConfig,
);
expect(groups).toEqual([
expect.objectContaining({
@@ -392,7 +426,11 @@ describe('readLdapGroups', () => {
});
it('transfers all attributes from Microsoft Active Directory', async () => {
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'distinguishedName',
uuidAttributeName: 'objectGUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -413,7 +451,7 @@ describe('readLdapGroups', () => {
}),
);
});
const config: GroupConfig[] = [
const groupConfig: GroupConfig[] = [
{
dn: 'ddd',
options: {},
@@ -432,7 +470,8 @@ describe('readLdapGroups', () => {
];
const { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
config,
groupConfig,
vendorConfig,
);
expect(groups).toEqual([
expect.objectContaining({
@@ -465,7 +504,11 @@ describe('readLdapGroups', () => {
});
it('can process a list of GroupConfigs', async () => {
client.getVendor.mockResolvedValue(DefaultLdapVendor);
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -481,7 +524,7 @@ describe('readLdapGroups', () => {
}),
);
});
const config: GroupConfig[] = [
const groupConfig: GroupConfig[] = [
{
dn: 'ddd',
options: {},
@@ -513,13 +556,17 @@ describe('readLdapGroups', () => {
},
},
];
const { groups } = await readLdapGroups(client, config);
const { groups } = await readLdapGroups(client, groupConfig, vendorConfig);
expect(groups).toHaveLength(2);
});
it('can process no GroupConfigs', async () => {
const config: GroupConfig[] = [];
const { groups } = await readLdapGroups(client, config);
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const groupConfig: GroupConfig[] = [];
const { groups } = await readLdapGroups(client, groupConfig, vendorConfig);
expect(groups).toHaveLength(0);
});
});
@@ -679,8 +726,12 @@ describe('defaultUserTransformer', () => {
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
});
let output = await defaultUserTransformer(DefaultLdapVendor, config, entry);
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const defaultLdapVendor = CreateLdapVendor(vendorConfig, false);
let output = await defaultUserTransformer(defaultLdapVendor, config, entry);
expect(output).toEqual({
apiVersion: 'backstage.io/v1beta1',
kind: 'User',
@@ -703,7 +754,7 @@ describe('defaultUserTransformer', () => {
(output!.metadata.annotations as any).c = 7;
// exact same inputs again
output = await defaultUserTransformer(DefaultLdapVendor, config, entry);
output = await defaultUserTransformer(defaultLdapVendor, config, entry);
expect(output).toEqual({
apiVersion: 'backstage.io/v1beta1',
kind: 'User',
@@ -757,8 +808,13 @@ describe('defaultGroupTransformer', () => {
entryUUID: ['uuid-value'],
});
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const defaultLdapVendor = CreateLdapVendor(vendorConfig, false);
let output = await defaultGroupTransformer(
DefaultLdapVendor,
defaultLdapVendor,
config,
entry,
);
@@ -786,7 +842,7 @@ describe('defaultGroupTransformer', () => {
(output!.metadata.annotations as any).c = 7;
// exact same inputs again
output = await defaultGroupTransformer(DefaultLdapVendor, config, entry);
output = await defaultGroupTransformer(defaultLdapVendor, config, entry);
expect(output).toEqual({
apiVersion: 'backstage.io/v1beta1',
kind: 'Group',
@@ -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,23 @@ export async function defaultUserTransformer(
*/
export async function readLdapUsers(
client: LdapClient,
config: UserConfig[],
userConfig: UserConfig[],
vendorConfig: VendorConfig,
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 vendor = await client.getVendor(vendorConfig);
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 +211,8 @@ export async function defaultGroupTransformer(
*/
export async function readLdapGroups(
client: LdapClient,
config: GroupConfig[],
groupConfig: GroupConfig[],
vendorConfig: VendorConfig,
opts?: {
transformer?: GroupTransformer;
},
@@ -219,17 +221,17 @@ 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 vendor = await client.getVendor(vendorConfig);
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 +277,7 @@ export async function readLdapOrg(
client: LdapClient,
userConfig: UserConfig[],
groupConfig: GroupConfig[],
vendorConfig: VendorConfig,
options: {
groupTransformer?: GroupTransformer;
userTransformer?: UserTransformer;
@@ -287,12 +290,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 },
);
@@ -15,6 +15,7 @@
*/
import { SearchEntry } from 'ldapjs';
import { VendorConfig } from './config';
/**
* An LDAP Vendor handles unique nuances between different vendors.
@@ -39,48 +40,26 @@ export type LdapVendor = {
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
};
export const DefaultLdapVendor: LdapVendor = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
decodeStringAttribute: (entry, name) => {
return decode(entry, name, value => {
return value.toString();
});
},
};
export const ActiveDirectoryVendor: LdapVendor = {
dnAttributeName: 'distinguishedName',
uuidAttributeName: 'objectGUID',
decodeStringAttribute: (entry, name) => {
const decoder = (value: string | Buffer) => {
if (name === ActiveDirectoryVendor.uuidAttributeName) {
return formatGUID(value);
}
return value.toString();
};
return decode(entry, name, decoder);
},
};
export const FreeIpaVendor: LdapVendor = {
dnAttributeName: 'dn',
uuidAttributeName: 'ipaUniqueID',
decodeStringAttribute: (entry, name) => {
return decode(entry, name, value => {
return value.toString();
});
},
};
export const AEDirVendor: LdapVendor = {
dnAttributeName: 'dn',
uuidAttributeName: 'entryUUID',
decodeStringAttribute: (entry, name) => {
return decode(entry, name, value => {
return value.toString();
});
},
export const CreateLdapVendor = (
vendorConfig: VendorConfig,
isActiveDirectoryVendor: boolean,
): LdapVendor => {
return {
dnAttributeName: vendorConfig.dnAttributeName,
uuidAttributeName: vendorConfig.uuidAttributeName,
decodeStringAttribute: (entry, name) => {
const decoder = (value: string | Buffer) => {
if (
isActiveDirectoryVendor &&
name === vendorConfig.uuidAttributeName
) {
return formatGUID(value);
}
return value.toString();
};
return decode(entry, name, decoder);
},
};
};
// Decode an attribute to a consumer
@@ -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,