Added support for only users or only groups and multiple users or multiple groups bindings
Closes: #25256 Signed-off-by: Jente Sondervorst <jentesondervorst@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-ldap': minor
|
||||
---
|
||||
|
||||
Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one.
|
||||
@@ -86,6 +86,14 @@ catalog:
|
||||
These config blocks have a lot of options in them, so we will describe each
|
||||
"root" key within the block separately.
|
||||
|
||||
> NOTE:
|
||||
>
|
||||
> If you want to import users and groups from different LDAP servers, you can define multiple providers with different names.
|
||||
> If they should come from the same server, you can define multiple users and groups blocks within the same provider using an array of users / groups.
|
||||
> Entries coming from the same block will be able to detect group memberships based on the `memberOf` attribute.
|
||||
>
|
||||
> If you want only to import users or groups, you can omit the groups or users block.
|
||||
|
||||
### target
|
||||
|
||||
This is the URL of the targeted server, typically on the form
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { readProviderConfigs } from './config';
|
||||
import { readProviderConfigs, UserConfig } from './config';
|
||||
|
||||
describe('readLdapConfig', () => {
|
||||
it('applies all of the defaults', () => {
|
||||
@@ -247,7 +247,7 @@ describe('readLdapConfig', () => {
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
const expected = '(|(cn=foo bar)(cn=bar))';
|
||||
expect(actual[0].users.options.filter).toEqual(expected);
|
||||
expect((actual[0].users!! as UserConfig).options.filter).toEqual(expected);
|
||||
});
|
||||
|
||||
it('supports a dot nested set structure', () => {
|
||||
@@ -284,7 +284,9 @@ describe('readLdapConfig', () => {
|
||||
};
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
expect(actual[0].users.set).toEqual({ 'metadata.annotations': { a: 'b' } });
|
||||
expect((actual[0].users!! as UserConfig).set).toEqual({
|
||||
'metadata.annotations': { a: 'b' },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on attempts to modify the set structure', () => {
|
||||
@@ -320,25 +322,77 @@ describe('readLdapConfig', () => {
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
expect(() => {
|
||||
(actual[0].users.set as any).y = 2;
|
||||
((actual[0].users!! as UserConfig).set as any).y = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property y, object is not extensible"`,
|
||||
);
|
||||
expect(() => {
|
||||
(actual[0].users.set as any).x.b = 2;
|
||||
((actual[0].users!! as UserConfig).set as any).x.b = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property b, object is not extensible"`,
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
(actual[0].groups.set as any).y = 2;
|
||||
((actual[0].users!! as UserConfig).set as any).y = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property y, object is not extensible"`,
|
||||
);
|
||||
expect(() => {
|
||||
(actual[0].groups.set as any).x.b = 2;
|
||||
((actual[0].users!! as UserConfig).set as any).x.b = 2;
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot add property b, object is not extensible"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports users/groups config as list', () => {
|
||||
const config = {
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
users: [
|
||||
{
|
||||
dn: 'udn1',
|
||||
},
|
||||
{
|
||||
dn: 'udn2',
|
||||
},
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
dn: 'gdn1',
|
||||
},
|
||||
{
|
||||
dn: 'gdn2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
expect(actual[0].users).toHaveLength(2);
|
||||
expect(actual[0].groups).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('supports users/groups config as undefined', () => {
|
||||
const config = {
|
||||
catalog: {
|
||||
providers: {
|
||||
ldapOrg: {
|
||||
default: {
|
||||
target: 'target',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const actual = readProviderConfigs(new ConfigReader(config));
|
||||
|
||||
expect(actual[0].users).toBeUndefined();
|
||||
expect(actual[0].groups).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,9 +42,9 @@ export type LdapProviderConfig = {
|
||||
// command is not issued.
|
||||
bind?: BindConfig;
|
||||
// The settings that govern the reading and interpretation of users
|
||||
users: UserConfig;
|
||||
users: UserConfigList;
|
||||
// The settings that govern the reading and interpretation of groups
|
||||
groups: GroupConfig;
|
||||
groups: GroupConfigList;
|
||||
// Schedule configuration for refresh tasks.
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
@@ -81,6 +81,8 @@ export type BindConfig = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type UserConfigList = UserConfig | UserConfig[] | undefined;
|
||||
|
||||
export type UserConfig = {
|
||||
// The DN under which users are stored.
|
||||
dn: string;
|
||||
@@ -121,6 +123,8 @@ export type UserConfig = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type GroupConfigList = GroupConfig | GroupConfig[] | undefined;
|
||||
|
||||
export type GroupConfig = {
|
||||
// The DN under which groups are stored.
|
||||
dn: string;
|
||||
@@ -272,9 +276,7 @@ function readSetConfig(
|
||||
return c.get();
|
||||
}
|
||||
|
||||
function readUserMapConfig(
|
||||
c: Config | undefined,
|
||||
): Partial<LdapProviderConfig['users']['map']> {
|
||||
function readUserMapConfig(c: Config | undefined): Partial<UserConfig['map']> {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
@@ -292,7 +294,7 @@ function readUserMapConfig(
|
||||
|
||||
function readGroupMapConfig(
|
||||
c: Config | undefined,
|
||||
): Partial<LdapProviderConfig['groups']['map']> {
|
||||
): Partial<GroupConfig['map']> {
|
||||
if (!c) {
|
||||
return {};
|
||||
}
|
||||
@@ -311,8 +313,21 @@ function readGroupMapConfig(
|
||||
}
|
||||
|
||||
function readUserConfig(
|
||||
c: Config,
|
||||
c: Config | Config[] | undefined,
|
||||
): RecursivePartial<LdapProviderConfig['users']> {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(c)) {
|
||||
return c.map(it => {
|
||||
return {
|
||||
dn: it.getString('dn'),
|
||||
options: readOptionsConfig(it.getOptionalConfig('options')),
|
||||
set: readSetConfig(it.getOptionalConfig('set')),
|
||||
map: readUserMapConfig(it.getOptionalConfig('map')),
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
options: readOptionsConfig(c.getOptionalConfig('options')),
|
||||
@@ -322,8 +337,21 @@ function readUserConfig(
|
||||
}
|
||||
|
||||
function readGroupConfig(
|
||||
c: Config,
|
||||
c: Config | Config[] | undefined,
|
||||
): RecursivePartial<LdapProviderConfig['groups']> {
|
||||
if (!c) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(c)) {
|
||||
return c.map(it => {
|
||||
return {
|
||||
dn: it.getString('dn'),
|
||||
options: readOptionsConfig(it.getOptionalConfig('options')),
|
||||
set: readSetConfig(it.getOptionalConfig('set')),
|
||||
map: readGroupMapConfig(it.getOptionalConfig('map')),
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
dn: c.getString('dn'),
|
||||
options: readOptionsConfig(c.getOptionalConfig('options')),
|
||||
@@ -383,19 +411,41 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] {
|
||||
? readTaskScheduleDefinitionFromConfig(c.getConfig('schedule'))
|
||||
: undefined;
|
||||
|
||||
const isUserList = Array.isArray(c.getOptional('users'));
|
||||
const isGroupList = Array.isArray(c.getOptional('groups'));
|
||||
|
||||
const newConfig = {
|
||||
id,
|
||||
target: trimEnd(c.getString('target'), '/'),
|
||||
tls: readTlsConfig(c.getOptionalConfig('tls')),
|
||||
bind: readBindConfig(c.getOptionalConfig('bind')),
|
||||
users: readUserConfig(c.getConfig('users')),
|
||||
groups: readGroupConfig(c.getConfig('groups')),
|
||||
users: readUserConfig(
|
||||
isUserList
|
||||
? c.getOptionalConfigArray('users')
|
||||
: c.getOptionalConfig('users'),
|
||||
),
|
||||
groups: readGroupConfig(
|
||||
isGroupList
|
||||
? c.getOptionalConfigArray('groups')
|
||||
: c.getOptionalConfig('groups'),
|
||||
),
|
||||
schedule,
|
||||
};
|
||||
const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => {
|
||||
// Replace arrays instead of merging, otherwise default behavior
|
||||
return Array.isArray(from) ? from : undefined;
|
||||
});
|
||||
|
||||
const merged = mergeWith(
|
||||
{},
|
||||
defaultConfig,
|
||||
newConfig,
|
||||
(_value, srcValue, key, object, _source) => {
|
||||
// Remove users and groups from default if they are not set in the new config
|
||||
if ((key === 'users' || key === 'groups') && !srcValue) {
|
||||
return (object[key] = srcValue);
|
||||
}
|
||||
// Replace arrays instead of merging, otherwise default behavior
|
||||
return Array.isArray(srcValue) ? srcValue : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
return freeze(merged) as LdapProviderConfig;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ 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,
|
||||
GroupConfigList,
|
||||
UserConfig,
|
||||
UserConfigList,
|
||||
} from './config';
|
||||
import {
|
||||
LDAP_DN_ANNOTATION,
|
||||
LDAP_RDN_ANNOTATION,
|
||||
@@ -255,6 +260,58 @@ describe('readLdapUsers', () => {
|
||||
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) => {
|
||||
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: UserConfigList = [
|
||||
{
|
||||
dn: 'ddd',
|
||||
options: {},
|
||||
map: {
|
||||
rdn: 'uid',
|
||||
name: 'uid',
|
||||
description: 'description',
|
||||
displayName: 'cn',
|
||||
email: 'mail',
|
||||
picture: 'avatarUrl',
|
||||
memberOf: 'memberOf',
|
||||
},
|
||||
},
|
||||
{
|
||||
dn: 'ddd',
|
||||
options: {},
|
||||
map: {
|
||||
rdn: 'uid',
|
||||
name: 'uid',
|
||||
description: 'description',
|
||||
displayName: 'cn',
|
||||
email: 'mail',
|
||||
picture: 'avatarUrl',
|
||||
memberOf: 'memberOf',
|
||||
},
|
||||
},
|
||||
];
|
||||
const { users } = await readLdapUsers(client, config);
|
||||
expect(users).toHaveLength(2);
|
||||
});
|
||||
it('can process no UserConfigs', async () => {
|
||||
const config: UserConfigList = undefined;
|
||||
const { users } = await readLdapUsers(client, config);
|
||||
expect(users).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readLdapGroups', () => {
|
||||
@@ -401,6 +458,65 @@ describe('readLdapGroups', () => {
|
||||
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({
|
||||
cn: ['cn-value'],
|
||||
description: ['description-value'],
|
||||
tt: ['type-value'],
|
||||
mail: ['mail-value'],
|
||||
avatarUrl: ['avatarUrl-value'],
|
||||
memberOf: ['x', 'y', 'z'],
|
||||
member: ['e', 'f', 'g'],
|
||||
entryDN: ['dn-value'],
|
||||
entryUUID: ['uuid-value'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
const config: GroupConfigList = [
|
||||
{
|
||||
dn: 'ddd',
|
||||
options: {},
|
||||
map: {
|
||||
rdn: 'cn',
|
||||
name: 'cn',
|
||||
description: 'description',
|
||||
displayName: 'cn',
|
||||
email: 'mail',
|
||||
picture: 'avatarUrl',
|
||||
type: 'tt',
|
||||
memberOf: 'memberOf',
|
||||
members: 'member',
|
||||
},
|
||||
},
|
||||
{
|
||||
dn: 'ddd',
|
||||
options: {},
|
||||
map: {
|
||||
rdn: 'cn',
|
||||
name: 'cn',
|
||||
description: 'description',
|
||||
displayName: 'cn',
|
||||
email: 'mail',
|
||||
picture: 'avatarUrl',
|
||||
type: 'tt',
|
||||
memberOf: 'memberOf',
|
||||
members: 'member',
|
||||
},
|
||||
},
|
||||
];
|
||||
const { groups } = await readLdapGroups(client, config);
|
||||
expect(groups).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('can process no GroupConfigs', async () => {
|
||||
const config: GroupConfigList = undefined;
|
||||
const { groups } = await readLdapGroups(client, config);
|
||||
expect(groups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRelations', () => {
|
||||
|
||||
@@ -24,7 +24,12 @@ 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,
|
||||
GroupConfigList,
|
||||
UserConfig,
|
||||
UserConfigList,
|
||||
} from './config';
|
||||
import {
|
||||
LDAP_DN_ANNOTATION,
|
||||
LDAP_RDN_ANNOTATION,
|
||||
@@ -104,32 +109,37 @@ export async function defaultUserTransformer(
|
||||
*/
|
||||
export async function readLdapUsers(
|
||||
client: LdapClient,
|
||||
config: UserConfig,
|
||||
config: UserConfigList,
|
||||
opts?: { transformer?: UserTransformer },
|
||||
): Promise<{
|
||||
users: UserEntity[]; // With all relations empty
|
||||
userMemberOf: Map<string, Set<string>>; // DN -> DN or UUID of groups
|
||||
}> {
|
||||
const { dn, options, map } = config;
|
||||
const vendor = await client.getVendor();
|
||||
|
||||
if (!config) {
|
||||
return { users: [], userMemberOf: new Map() };
|
||||
}
|
||||
const configs = Array.isArray(config) ? config : [config];
|
||||
const entities: UserEntity[] = [];
|
||||
const userMemberOf: Map<string, Set<string>> = new Map();
|
||||
|
||||
const vendor = await client.getVendor();
|
||||
const transformer = opts?.transformer ?? defaultUserTransformer;
|
||||
|
||||
await client.searchStreaming(dn, options, async user => {
|
||||
const entity = await transformer(vendor, config, user);
|
||||
for (const cfg of configs) {
|
||||
const { dn, options, map } = cfg;
|
||||
await client.searchStreaming(dn, options, async user => {
|
||||
const entity = await transformer(vendor, cfg, user);
|
||||
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(userMemberOf, myDn, vs);
|
||||
mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(userMemberOf, myDn, vs);
|
||||
});
|
||||
entities.push(entity);
|
||||
});
|
||||
entities.push(entity);
|
||||
});
|
||||
}
|
||||
|
||||
return { users: entities, userMemberOf };
|
||||
}
|
||||
@@ -206,7 +216,7 @@ export async function defaultGroupTransformer(
|
||||
*/
|
||||
export async function readLdapGroups(
|
||||
client: LdapClient,
|
||||
config: GroupConfig,
|
||||
config: GroupConfigList,
|
||||
opts?: {
|
||||
transformer?: GroupTransformer;
|
||||
},
|
||||
@@ -215,35 +225,41 @@ 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) {
|
||||
return { groups: [], groupMemberOf: new Map(), groupMember: new Map() };
|
||||
}
|
||||
const configs = Array.isArray(config) ? config : [config];
|
||||
const groups: GroupEntity[] = [];
|
||||
const groupMemberOf: Map<string, Set<string>> = new Map();
|
||||
const groupMember: Map<string, Set<string>> = new Map();
|
||||
|
||||
const { dn, map, options } = config;
|
||||
const vendor = await client.getVendor();
|
||||
|
||||
const transformer = opts?.transformer ?? defaultGroupTransformer;
|
||||
|
||||
await client.searchStreaming(dn, options, async entry => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
for (const cfg of configs) {
|
||||
const { dn, map, options } = cfg;
|
||||
|
||||
const entity = await transformer(vendor, config, entry);
|
||||
await client.searchStreaming(dn, options, async entry => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
const entity = await transformer(vendor, cfg, entry);
|
||||
|
||||
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(groupMemberOf, myDn, vs);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(groupMemberOf, myDn, vs);
|
||||
});
|
||||
mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => {
|
||||
ensureItems(groupMember, myDn, vs);
|
||||
});
|
||||
|
||||
groups.push(entity);
|
||||
});
|
||||
mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => {
|
||||
ensureItems(groupMember, myDn, vs);
|
||||
});
|
||||
|
||||
groups.push(entity);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
groups,
|
||||
@@ -264,8 +280,8 @@ export async function readLdapGroups(
|
||||
*/
|
||||
export async function readLdapOrg(
|
||||
client: LdapClient,
|
||||
userConfig: UserConfig,
|
||||
groupConfig: GroupConfig,
|
||||
userConfig: UserConfigList,
|
||||
groupConfig: GroupConfigList,
|
||||
options: {
|
||||
groupTransformer?: GroupTransformer;
|
||||
userTransformer?: UserTransformer;
|
||||
|
||||
Reference in New Issue
Block a user