Merge pull request #25261 from Jenson3210/feature/multiple_ldap_users_groups

Added support for only users or only groups and multiple users or multiple groups bindings
This commit is contained in:
Fredrik Adelöw
2024-06-29 23:47:28 +02:00
committed by GitHub
8 changed files with 853 additions and 435 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-backend-module-ldap': minor
---
**BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items.
Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one.
+8
View File
@@ -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
@@ -197,8 +197,8 @@ export type LdapProviderConfig = {
target: string;
tls?: TLSConfig;
bind?: BindConfig;
users: UserConfig;
groups: GroupConfig;
users: UserConfig[];
groups: GroupConfig[];
schedule?: SchedulerServiceTaskScheduleDefinition;
};
@@ -223,8 +223,8 @@ export function readLdapLegacyConfig(config: Config): LdapProviderConfig[];
// @public
export function readLdapOrg(
client: LdapClient,
userConfig: UserConfig,
groupConfig: GroupConfig,
userConfig: UserConfig[],
groupConfig: GroupConfig[],
options: {
groupTransformer?: GroupTransformer;
userTransformer?: UserTransformer;
+342 -170
View File
@@ -63,180 +63,352 @@ export interface Config {
/**
* The settings that govern the reading and interpretation of users.
*/
users: {
/**
* The DN under which users are stored.
*
* E.g. "ou=people,ou=example,dc=example,dc=net"
*/
dn: string;
/**
* The search options to use. The default is scope "one" and
* attributes "*" and "+".
*
* It is common to want to specify a filter, to narrow down the set
* of matching items.
*/
options: {
scope?: 'base' | 'one' | 'sub';
filter?: string;
attributes?: string | string[];
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
paged?:
| boolean
| {
pageSize?: number;
pagePause?: boolean;
};
};
/**
* JSON paths (on a.b.c form) and hard coded values to set on those
* paths.
*
* This can be useful for example if you want to hard code a
* namespace or similar on the generated entities.
*/
set?: { [key: string]: JsonValue };
/**
* Mappings from well known entity fields, to LDAP attribute names
*/
map?: {
/**
* The name of the attribute that holds the relative
* distinguished name of each entry. Defaults to "uid".
*/
rdn?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.name field of the entity. Defaults to "uid".
*/
name?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.description field of the entity.
*/
description?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.displayName field of the entity. Defaults to
* "cn".
*/
displayName?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.email field of the entity. Defaults to
* "mail".
*/
email?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.picture field of the entity.
*/
picture?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.memberOf field of the entity. Defaults to "memberOf".
*/
memberOf?: string;
};
};
users?:
| {
/**
* The DN under which users are stored.
*
* E.g. "ou=people,ou=example,dc=example,dc=net"
*/
dn: string;
/**
* The search options to use. The default is scope "one" and
* attributes "*" and "+".
*
* It is common to want to specify a filter, to narrow down the set
* of matching items.
*/
options: {
scope?: 'base' | 'one' | 'sub';
filter?: string;
attributes?: string | string[];
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
paged?:
| boolean
| {
pageSize?: number;
pagePause?: boolean;
};
};
/**
* JSON paths (on a.b.c form) and hard coded values to set on those
* paths.
*
* This can be useful for example if you want to hard code a
* namespace or similar on the generated entities.
*/
set?: { [key: string]: JsonValue };
/**
* Mappings from well known entity fields, to LDAP attribute names
*/
map?: {
/**
* The name of the attribute that holds the relative
* distinguished name of each entry. Defaults to "uid".
*/
rdn?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.name field of the entity. Defaults to "uid".
*/
name?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.description field of the entity.
*/
description?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.displayName field of the entity. Defaults to
* "cn".
*/
displayName?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.email field of the entity. Defaults to
* "mail".
*/
email?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.picture field of the entity.
*/
picture?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.memberOf field of the entity. Defaults to "memberOf".
*/
memberOf?: string;
};
}
| Array<{
/**
* The DN under which users are stored.
*
* E.g. "ou=people,ou=example,dc=example,dc=net"
*/
dn: string;
/**
* The search options to use. The default is scope "one" and
* attributes "*" and "+".
*
* It is common to want to specify a filter, to narrow down the set
* of matching items.
*/
options: {
scope?: 'base' | 'one' | 'sub';
filter?: string;
attributes?: string | string[];
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
paged?:
| boolean
| {
pageSize?: number;
pagePause?: boolean;
};
};
/**
* JSON paths (on a.b.c form) and hard coded values to set on those
* paths.
*
* This can be useful for example if you want to hard code a
* namespace or similar on the generated entities.
*/
set?: { [key: string]: JsonValue };
/**
* Mappings from well known entity fields, to LDAP attribute names
*/
map?: {
/**
* The name of the attribute that holds the relative
* distinguished name of each entry. Defaults to "uid".
*/
rdn?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.name field of the entity. Defaults to "uid".
*/
name?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.description field of the entity.
*/
description?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.displayName field of the entity. Defaults to
* "cn".
*/
displayName?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.email field of the entity. Defaults to
* "mail".
*/
email?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.picture field of the entity.
*/
picture?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.memberOf field of the entity. Defaults to "memberOf".
*/
memberOf?: string;
};
}>;
/**
* The settings that govern the reading and interpretation of groups.
*/
groups: {
/**
* The DN under which groups are stored.
*
* E.g. "ou=people,ou=example,dc=example,dc=net"
*/
dn: string;
/**
* The search options to use. The default is scope "one" and
* attributes "*" and "+".
*
* It is common to want to specify a filter, to narrow down the set
* of matching items.
*/
options: {
scope?: 'base' | 'one' | 'sub';
filter?: string;
attributes?: string | string[];
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
paged?:
| boolean
| {
pageSize?: number;
pagePause?: boolean;
};
};
/**
* JSON paths (on a.b.c form) and hard coded values to set on those
* paths.
*
* This can be useful for example if you want to hard code a
* namespace or similar on the generated entities.
*/
set?: { [key: string]: JsonValue };
/**
* Mappings from well known entity fields, to LDAP attribute names
*/
map?: {
/**
* The name of the attribute that holds the relative
* distinguished name of each entry. Defaults to "cn".
*/
rdn?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.name field of the entity. Defaults to "cn".
*/
name?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.description field of the entity. Defaults to
* "description".
*/
description?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.type field of the entity. Defaults to "groupType".
*/
type?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.displayName field of the entity. Defaults to
* "cn".
*/
displayName?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.email field of the entity.
*/
email?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.picture field of the entity.
*/
picture?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.parent field of the entity. Defaults to "memberOf".
*/
memberOf?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.children field of the entity. Defaults to "member".
*/
members?: string;
};
};
groups?:
| {
/**
* The DN under which groups are stored.
*
* E.g. "ou=people,ou=example,dc=example,dc=net"
*/
dn: string;
/**
* The search options to use. The default is scope "one" and
* attributes "*" and "+".
*
* It is common to want to specify a filter, to narrow down the set
* of matching items.
*/
options: {
scope?: 'base' | 'one' | 'sub';
filter?: string;
attributes?: string | string[];
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
paged?:
| boolean
| {
pageSize?: number;
pagePause?: boolean;
};
};
/**
* JSON paths (on a.b.c form) and hard coded values to set on those
* paths.
*
* This can be useful for example if you want to hard code a
* namespace or similar on the generated entities.
*/
set?: { [key: string]: JsonValue };
/**
* Mappings from well known entity fields, to LDAP attribute names
*/
map?: {
/**
* The name of the attribute that holds the relative
* distinguished name of each entry. Defaults to "cn".
*/
rdn?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.name field of the entity. Defaults to "cn".
*/
name?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.description field of the entity. Defaults to
* "description".
*/
description?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.type field of the entity. Defaults to "groupType".
*/
type?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.displayName field of the entity. Defaults to
* "cn".
*/
displayName?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.email field of the entity.
*/
email?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.picture field of the entity.
*/
picture?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.parent field of the entity. Defaults to "memberOf".
*/
memberOf?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.children field of the entity. Defaults to "member".
*/
members?: string;
};
}
| Array<{
/**
* The DN under which groups are stored.
*
* E.g. "ou=people,ou=example,dc=example,dc=net"
*/
dn: string;
/**
* The search options to use. The default is scope "one" and
* attributes "*" and "+".
*
* It is common to want to specify a filter, to narrow down the set
* of matching items.
*/
options: {
scope?: 'base' | 'one' | 'sub';
filter?: string;
attributes?: string | string[];
sizeLimit?: number;
timeLimit?: number;
derefAliases?: number;
typesOnly?: boolean;
paged?:
| boolean
| {
pageSize?: number;
pagePause?: boolean;
};
};
/**
* JSON paths (on a.b.c form) and hard coded values to set on those
* paths.
*
* This can be useful for example if you want to hard code a
* namespace or similar on the generated entities.
*/
set?: { [key: string]: JsonValue };
/**
* Mappings from well known entity fields, to LDAP attribute names
*/
map?: {
/**
* The name of the attribute that holds the relative
* distinguished name of each entry. Defaults to "cn".
*/
rdn?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.name field of the entity. Defaults to "cn".
*/
name?: string;
/**
* The name of the attribute that shall be used for the value of
* the metadata.description field of the entity. Defaults to
* "description".
*/
description?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.type field of the entity. Defaults to "groupType".
*/
type?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.displayName field of the entity. Defaults to
* "cn".
*/
displayName?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.email field of the entity.
*/
email?: string;
/**
* The name of the attribute that shall be used for the value of
* the spec.profile.picture field of the entity.
*/
picture?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.parent field of the entity. Defaults to "memberOf".
*/
memberOf?: string;
/**
* The name of the attribute that shall be used for the values of
* the spec.children field of the entity. Defaults to "member".
*/
members?: string;
};
}>;
}>;
};
@@ -42,38 +42,42 @@ describe('readLdapConfig', () => {
id: 'default',
target: 'target',
bind: undefined,
users: {
dn: 'udn',
options: {
scope: 'one',
attributes: ['*', '+'],
users: [
{
dn: 'udn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
},
},
set: undefined,
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
],
groups: [
{
dn: 'gdn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
type: 'groupType',
displayName: 'cn',
memberOf: 'memberOf',
members: 'member',
},
},
},
groups: {
dn: 'gdn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
type: 'groupType',
displayName: 'cn',
memberOf: 'memberOf',
members: 'member',
},
},
],
},
];
expect(actual).toEqual(expected);
@@ -111,38 +115,42 @@ describe('readLdapConfig', () => {
frequency: { minutes: 3 },
timeout: { minutes: 1 },
},
users: {
dn: 'udn',
options: {
scope: 'one',
attributes: ['*', '+'],
users: [
{
dn: 'udn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
},
},
set: undefined,
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
],
groups: [
{
dn: 'gdn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
type: 'groupType',
displayName: 'cn',
memberOf: 'memberOf',
members: 'member',
},
},
},
groups: {
dn: 'gdn',
options: {
scope: 'one',
attributes: ['*', '+'],
},
set: undefined,
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
type: 'groupType',
displayName: 'cn',
memberOf: 'memberOf',
members: 'member',
},
},
],
},
];
expect(actual).toEqual(expected);
@@ -228,57 +236,61 @@ describe('readLdapConfig', () => {
keys: '/tmp/keys.pem',
certs: '/tmp/certs.pem',
},
users: {
dn: 'udn',
options: {
scope: 'base',
attributes: ['*'],
filter: 'f',
paged: true,
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
},
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
description: 'd',
displayName: 'c',
email: 'm',
picture: 'p',
memberOf: 'm',
},
},
groups: {
dn: 'gdn',
options: {
scope: 'base',
attributes: ['*'],
filter: 'f',
paged: {
pageSize: 7,
pagePause: true,
users: [
{
dn: 'udn',
options: {
scope: 'base',
attributes: ['*'],
filter: 'f',
paged: true,
timeLimit: 42,
sizeLimit: 100,
derefAliases: 0,
typesOnly: false,
},
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
description: 'd',
displayName: 'c',
email: 'm',
picture: 'p',
memberOf: 'm',
},
timeLimit: 42,
sizeLimit: 100,
derefAliases: 1,
typesOnly: true,
},
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
description: 'd',
type: 't',
displayName: 'c',
email: 'm',
picture: 'p',
memberOf: 'm',
members: 'n',
],
groups: [
{
dn: 'gdn',
options: {
scope: 'base',
attributes: ['*'],
filter: 'f',
paged: {
pageSize: 7,
pagePause: true,
},
timeLimit: 42,
sizeLimit: 100,
derefAliases: 1,
typesOnly: true,
},
set: { p: 'v' },
map: {
rdn: 'u',
name: 'v',
description: 'd',
type: 't',
displayName: 'c',
email: 'm',
picture: 'p',
memberOf: 'm',
members: 'n',
},
},
},
],
},
];
expect(actual).toEqual(expected);
@@ -316,7 +328,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[0].options.filter).toEqual(expected);
});
it('supports a dot nested set structure', () => {
@@ -353,7 +365,9 @@ describe('readLdapConfig', () => {
};
const actual = readProviderConfigs(new ConfigReader(config));
expect(actual[0].users.set).toEqual({ 'metadata.annotations': { a: 'b' } });
expect(actual[0].users[0].set).toEqual({
'metadata.annotations': { a: 'b' },
});
});
it('throws on attempts to modify the set structure', () => {
@@ -389,25 +403,77 @@ describe('readLdapConfig', () => {
const actual = readProviderConfigs(new ConfigReader(config));
expect(() => {
(actual[0].users.set as any).y = 2;
(actual[0].users[0].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[0].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].groups[0].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].groups[0].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).toHaveLength(0);
expect(actual[0].groups).toHaveLength(0);
});
});
@@ -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: UserConfig[];
// The settings that govern the reading and interpretation of groups
groups: GroupConfig;
groups: GroupConfig[];
// Schedule configuration for refresh tasks.
schedule?: SchedulerServiceTaskScheduleDefinition;
};
@@ -161,34 +161,33 @@ export type GroupConfig = {
};
};
const defaultConfig = {
users: {
options: {
scope: 'one',
attributes: ['*', '+'],
},
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
},
const defaultUserConfig = {
options: {
scope: 'one',
attributes: ['*', '+'],
},
groups: {
options: {
scope: 'one',
attributes: ['*', '+'],
},
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
displayName: 'cn',
type: 'groupType',
memberOf: 'memberOf',
members: 'member',
},
map: {
rdn: 'uid',
name: 'uid',
displayName: 'cn',
email: 'mail',
memberOf: 'memberOf',
},
};
const defaultGroupConfig = {
options: {
scope: 'one',
attributes: ['*', '+'],
},
map: {
rdn: 'cn',
name: 'cn',
description: 'description',
displayName: 'cn',
type: 'groupType',
memberOf: 'memberOf',
members: 'member',
},
};
@@ -272,9 +271,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 +289,7 @@ function readUserMapConfig(
function readGroupMapConfig(
c: Config | undefined,
): Partial<LdapProviderConfig['groups']['map']> {
): Partial<GroupConfig['map']> {
if (!c) {
return {};
}
@@ -311,8 +308,18 @@ function readGroupMapConfig(
}
function readUserConfig(
c: Config,
c: Config | Config[] | undefined,
): RecursivePartial<LdapProviderConfig['users']> {
if (!c) {
return [];
}
if (Array.isArray(c)) {
return c.map(it => readSingleUserConfig(it));
}
return [readSingleUserConfig(c)];
}
function readSingleUserConfig(c: Config): RecursivePartial<UserConfig> {
return {
dn: c.getString('dn'),
options: readOptionsConfig(c.getOptionalConfig('options')),
@@ -322,8 +329,18 @@ function readUserConfig(
}
function readGroupConfig(
c: Config,
c: Config | Config[] | undefined,
): RecursivePartial<LdapProviderConfig['groups']> {
if (!c) {
return [];
}
if (Array.isArray(c)) {
return c.map(it => readSingleGroupConfig(it));
}
return [readSingleGroupConfig(c)];
}
function readSingleGroupConfig(c: Config): RecursivePartial<GroupConfig> {
return {
dn: c.getString('dn'),
options: readOptionsConfig(c.getOptionalConfig('options')),
@@ -352,14 +369,15 @@ export function readLdapLegacyConfig(config: Config): LdapProviderConfig[] {
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(c.getConfig('users')).map(it => {
return mergeWith({}, defaultUserConfig, it, replaceArraysIfPresent);
}),
groups: readGroupConfig(c.getConfig('groups')).map(it => {
return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent);
}),
};
const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => {
// Replace arrays instead of merging, otherwise default behavior
return Array.isArray(from) ? from : undefined;
});
return freeze(merged) as LdapProviderConfig;
return freeze(newConfig) as LdapProviderConfig;
});
}
@@ -385,19 +403,36 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] {
)
: 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'),
).map(it => {
return mergeWith({}, defaultUserConfig, it, replaceArraysIfPresent);
}),
groups: readGroupConfig(
isGroupList
? c.getOptionalConfigArray('groups')
: c.getOptionalConfig('groups'),
).map(it => {
return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent);
}),
schedule,
};
const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => {
// Replace arrays instead of merging, otherwise default behavior
return Array.isArray(from) ? from : undefined;
});
return freeze(merged) as LdapProviderConfig;
return freeze(newConfig) as LdapProviderConfig;
});
}
function replaceArraysIfPresent(_into: any, from: any) {
// Replace arrays instead of merging, otherwise default behavior
return Array.isArray(from) ? from : undefined;
}
@@ -99,19 +99,21 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig = {
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
picture: 'avatarUrl',
memberOf: 'memberOf',
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
picture: 'avatarUrl',
memberOf: 'memberOf',
},
},
};
];
const { users, userMemberOf } = await readLdapUsers(client, config);
expect(users).toEqual([
expect.objectContaining({
@@ -160,19 +162,21 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig = {
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
picture: 'avatarUrl',
memberOf: 'memberOf',
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
picture: 'avatarUrl',
memberOf: 'memberOf',
},
},
};
];
const { users, userMemberOf } = await readLdapUsers(client, config);
expect(users).toEqual([
expect.objectContaining({
@@ -216,19 +220,21 @@ describe('readLdapUsers', () => {
}),
);
});
const config: UserConfig = {
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
picture: 'avatarUrl',
memberOf: 'memberOf',
const config: UserConfig[] = [
{
dn: 'ddd',
options: {},
map: {
rdn: 'uid',
name: 'uid',
description: 'description',
displayName: 'cn',
email: 'mail',
picture: 'avatarUrl',
memberOf: 'memberOf',
},
},
};
];
const { users, userMemberOf } = await readLdapUsers(client, config);
expect(users).toEqual([
expect.objectContaining({
@@ -255,6 +261,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: UserConfig[] = [
{
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: UserConfig[] = [];
const { users } = await readLdapUsers(client, config);
expect(users).toHaveLength(0);
});
});
describe('readLdapGroups', () => {
@@ -282,21 +340,23 @@ describe('readLdapGroups', () => {
}),
);
});
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 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 { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
config,
@@ -353,21 +413,23 @@ describe('readLdapGroups', () => {
}),
);
});
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 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 { groups, groupMember, groupMemberOf } = await readLdapGroups(
client,
config,
@@ -401,6 +463,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: GroupConfig[] = [
{
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: GroupConfig[] = [];
const { groups } = await readLdapGroups(client, config);
expect(groups).toHaveLength(0);
});
});
describe('resolveRelations', () => {
@@ -104,32 +104,36 @@ export async function defaultUserTransformer(
*/
export async function readLdapUsers(
client: LdapClient,
config: UserConfig,
config: UserConfig[],
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.length === 0) {
return { users: [], userMemberOf: new Map() };
}
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 config) {
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 +210,7 @@ export async function defaultGroupTransformer(
*/
export async function readLdapGroups(
client: LdapClient,
config: GroupConfig,
config: GroupConfig[],
opts?: {
transformer?: GroupTransformer;
},
@@ -215,35 +219,40 @@ 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) {
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 { 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 config) {
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 +273,8 @@ export async function readLdapGroups(
*/
export async function readLdapOrg(
client: LdapClient,
userConfig: UserConfig,
groupConfig: GroupConfig,
userConfig: UserConfig[],
groupConfig: GroupConfig[],
options: {
groupTransformer?: GroupTransformer;
userTransformer?: UserTransformer;