fixup based on feedback

Signed-off-by: Kashish Mittal <kmittal@redhat.com>
This commit is contained in:
Kashish Mittal
2024-09-12 16:55:44 -04:00
parent 8e45d03520
commit 4bbf54497e
4 changed files with 251 additions and 151 deletions
@@ -19,9 +19,15 @@ import { readFile } from 'fs/promises';
import ldap, { Client, SearchEntry, SearchOptions } from 'ldapjs';
import { cloneDeep } from 'lodash';
import tlsLib from 'tls';
import { BindConfig, TLSConfig, VendorConfig } from './config';
import { BindConfig, TLSConfig } from './config';
import { createOptions, errorString } from './util';
import { CreateLdapVendor, LdapVendor } from './vendors';
import {
AEDirVendor,
ActiveDirectoryVendor,
DefaultLdapVendor,
FreeIpaVendor,
LdapVendor,
} from './vendors';
import { LoggerService } from '@backstage/backend-plugin-api';
/**
@@ -226,51 +232,20 @@ export class LdapClient {
*
* @see https://ldapwiki.com/wiki/Determine%20LDAP%20Server%20Vendor
*/
async getVendor(vendorConfig: VendorConfig | undefined): Promise<LdapVendor> {
async getVendor(): Promise<LdapVendor> {
if (this.vendor) {
return this.vendor;
}
this.vendor = this.getRootDSE()
.then(root => {
if (root && root.raw?.forestFunctionality) {
// ActiveDirectoryVendor
return CreateLdapVendor(
{
dnAttributeName:
vendorConfig?.dnAttributeName || 'distinguishedName',
uuidAttributeName:
vendorConfig?.uuidAttributeName || 'objectGUID',
},
true,
);
return ActiveDirectoryVendor;
} else if (root && root.raw?.ipaDomainLevel) {
// FreeIpaVendor
return CreateLdapVendor(
{
dnAttributeName: vendorConfig?.dnAttributeName || 'dn',
uuidAttributeName:
vendorConfig?.uuidAttributeName || 'ipaUniqueID',
},
false,
);
return FreeIpaVendor;
} else if (root && 'aeRoot' in root.raw) {
// AEDirVendor
return CreateLdapVendor(
{
dnAttributeName: vendorConfig?.dnAttributeName || 'dn',
uuidAttributeName: vendorConfig?.uuidAttributeName || 'entryUUID',
},
false,
);
return AEDirVendor;
}
// DefaultLdapVendor
return CreateLdapVendor(
{
dnAttributeName: vendorConfig?.dnAttributeName,
uuidAttributeName: vendorConfig?.uuidAttributeName,
},
false,
);
return DefaultLdapVendor;
})
.catch(err => {
this.vendor = undefined;
@@ -32,8 +32,11 @@ import {
resolveRelations,
} from './read';
import { RecursivePartial } from './util';
import { CreateLdapVendor } from './vendors';
import {
ActiveDirectoryVendor,
DefaultLdapVendor,
FreeIpaVendor,
} from './vendors';
function user(data: RecursivePartial<UserEntity>): UserEntity {
return merge(
@@ -81,11 +84,7 @@ describe('readLdapUsers', () => {
afterEach(() => jest.resetAllMocks());
it('transfers all attributes from a default ldap vendor', async () => {
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -100,7 +99,7 @@ describe('readLdapUsers', () => {
}),
);
});
const userConfig: UserConfig[] = [
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -115,11 +114,7 @@ describe('readLdapUsers', () => {
},
},
];
const { users, userMemberOf } = await readLdapUsers(
client,
userConfig,
vendorConfig,
);
const { users, userMemberOf } = await readLdapUsers(client, config, {});
expect(users).toEqual([
expect.objectContaining({
metadata: {
@@ -146,12 +141,77 @@ describe('readLdapUsers', () => {
);
});
it('transfers all attributes from Microsoft Active Directory', async () => {
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: 'distinguishedName',
uuidAttributeName: 'objectGUID',
dnAttributeName: 'customDN',
uuidAttributeName: 'customUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true));
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) => {
await fn(
searchEntry({
@@ -171,7 +231,7 @@ describe('readLdapUsers', () => {
}),
);
});
const userConfig: UserConfig[] = [
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -186,11 +246,7 @@ describe('readLdapUsers', () => {
},
},
];
const { users, userMemberOf } = await readLdapUsers(
client,
userConfig,
vendorConfig,
);
const { users, userMemberOf } = await readLdapUsers(client, config, {});
expect(users).toEqual([
expect.objectContaining({
metadata: {
@@ -218,11 +274,7 @@ describe('readLdapUsers', () => {
});
it('transfers all attributes from FreeIPA', async () => {
const vendorConfig: VendorConfig = {
dnAttributeName: 'dn',
uuidAttributeName: 'ipaUniqueID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.getVendor.mockResolvedValue(FreeIpaVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -237,7 +289,7 @@ describe('readLdapUsers', () => {
}),
);
});
const userConfig: UserConfig[] = [
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -252,11 +304,7 @@ describe('readLdapUsers', () => {
},
},
];
const { users, userMemberOf } = await readLdapUsers(
client,
userConfig,
vendorConfig,
);
const { users, userMemberOf } = await readLdapUsers(client, config, {});
expect(users).toEqual([
expect.objectContaining({
metadata: {
@@ -283,11 +331,7 @@ describe('readLdapUsers', () => {
);
});
it('can process a list of UserConfigs', async () => {
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -302,7 +346,7 @@ describe('readLdapUsers', () => {
}),
);
});
const userConfig: UserConfig[] = [
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
@@ -330,16 +374,12 @@ describe('readLdapUsers', () => {
},
},
];
const { users } = await readLdapUsers(client, userConfig, vendorConfig);
const { users } = await readLdapUsers(client, config, {});
expect(users).toHaveLength(2);
});
it('can process no UserConfigs', async () => {
const userConfig: UserConfig[] = [];
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const { users } = await readLdapUsers(client, userConfig, vendorConfig);
const config: UserConfig[] = [];
const { users } = await readLdapUsers(client, config, {});
expect(users).toHaveLength(0);
});
});
@@ -353,11 +393,7 @@ describe('readLdapGroups', () => {
afterEach(() => jest.resetAllMocks());
it('transfers all attributes from a default ldap vendor', async () => {
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
client.getVendor.mockResolvedValue(DefaultLdapVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -373,7 +409,7 @@ describe('readLdapGroups', () => {
}),
);
});
const groupConfig: GroupConfig[] = [
const config: GroupConfig[] = [
{
dn: 'ddd',
options: {},
@@ -392,8 +428,8 @@ describe('readLdapGroups', () => {
];
const { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
groupConfig,
vendorConfig,
config,
{},
);
expect(groups).toEqual([
expect.objectContaining({
@@ -426,11 +462,7 @@ describe('readLdapGroups', () => {
});
it('transfers all attributes from Microsoft Active Directory', async () => {
const vendorConfig: VendorConfig = {
dnAttributeName: 'distinguishedName',
uuidAttributeName: 'objectGUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, true));
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
await fn(
searchEntry({
@@ -451,7 +483,7 @@ describe('readLdapGroups', () => {
}),
);
});
const groupConfig: GroupConfig[] = [
const config: GroupConfig[] = [
{
dn: 'ddd',
options: {},
@@ -470,8 +502,8 @@ describe('readLdapGroups', () => {
];
const { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
groupConfig,
vendorConfig,
config,
{},
);
expect(groups).toEqual([
expect.objectContaining({
@@ -503,12 +535,83 @@ describe('readLdapGroups', () => {
);
});
it('can process a list of GroupConfigs', async () => {
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: 'entryDN',
uuidAttributeName: 'entryUUID',
dnAttributeName: 'customDN',
uuidAttributeName: 'customUUID',
};
client.getVendor.mockResolvedValue(CreateLdapVendor(vendorConfig, false));
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) => {
await fn(
searchEntry({
@@ -524,7 +627,7 @@ describe('readLdapGroups', () => {
}),
);
});
const groupConfig: GroupConfig[] = [
const config: GroupConfig[] = [
{
dn: 'ddd',
options: {},
@@ -556,17 +659,13 @@ describe('readLdapGroups', () => {
},
},
];
const { groups } = await readLdapGroups(client, groupConfig, vendorConfig);
const { groups } = await readLdapGroups(client, config, {});
expect(groups).toHaveLength(2);
});
it('can process no GroupConfigs', async () => {
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const groupConfig: GroupConfig[] = [];
const { groups } = await readLdapGroups(client, groupConfig, vendorConfig);
const config: GroupConfig[] = [];
const { groups } = await readLdapGroups(client, config, {});
expect(groups).toHaveLength(0);
});
});
@@ -726,12 +825,8 @@ describe('defaultUserTransformer', () => {
entryDN: ['dn-value'],
entryUUID: ['uuid-value'],
});
const vendorConfig: VendorConfig = {
dnAttributeName: 'entryDN',
uuidAttributeName: 'entryUUID',
};
const defaultLdapVendor = CreateLdapVendor(vendorConfig, false);
let output = await defaultUserTransformer(defaultLdapVendor, config, entry);
let output = await defaultUserTransformer(DefaultLdapVendor, config, entry);
expect(output).toEqual({
apiVersion: 'backstage.io/v1beta1',
kind: 'User',
@@ -754,7 +849,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',
@@ -808,13 +903,8 @@ 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,
);
@@ -842,7 +932,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',
@@ -116,8 +116,14 @@ export async function readLdapUsers(
}
const entities: UserEntity[] = [];
const userMemberOf: Map<string, Set<string>> = new Map();
const vendor = await client.getVendor(vendorConfig);
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 userConfig) {
@@ -228,7 +234,15 @@ export async function readLdapGroups(
const groupMemberOf: Map<string, Set<string>> = new Map();
const groupMember: Map<string, Set<string>> = new Map();
const vendor = await client.getVendor(vendorConfig);
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 groupConfig) {
@@ -15,7 +15,6 @@
*/
import { SearchEntry } from 'ldapjs';
import { VendorConfig } from './config';
/**
* An LDAP Vendor handles unique nuances between different vendors.
@@ -40,26 +39,48 @@ export type LdapVendor = {
decodeStringAttribute: (entry: SearchEntry, name: string) => string[];
};
export const CreateLdapVendor = (
vendorConfig: VendorConfig,
isActiveDirectoryVendor: boolean,
): LdapVendor => {
return {
dnAttributeName: vendorConfig.dnAttributeName || `entryDN`,
uuidAttributeName: vendorConfig.uuidAttributeName || `entryUUID`,
decodeStringAttribute: (entry, name) => {
const decoder = (value: string | Buffer) => {
if (
isActiveDirectoryVendor &&
name === vendorConfig.uuidAttributeName
) {
return formatGUID(value);
}
return value.toString();
};
return decode(entry, name, decoder);
},
};
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();
});
},
};
// Decode an attribute to a consumer