Merge pull request #29821 from backstage/blam/ldap
[catalog]: Support disabling one form of relationships from `ldap` module
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-ldap': patch
|
||||
---
|
||||
|
||||
Added the ability to configure disabling one side of the relations tree with LDAP.
|
||||
|
||||
Groups have a `member` attribute and users have a `memberOf` attribute, however these can drift out of sync in some LDAP installations, leaving weird states in the Catalog as we collate these results together and deduplicate them.
|
||||
|
||||
You can chose to optionally disable one side of these relationships, or even both by setting the respective mapping to `null` in your `app-config.yaml` for your groups and/or users:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
providers:
|
||||
ldapOrg:
|
||||
default:
|
||||
target: ldaps://ds.example.net
|
||||
bind:
|
||||
dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net
|
||||
secret: ${LDAP_SECRET}
|
||||
users:
|
||||
- dn: ou=people,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (uid=*)
|
||||
map:
|
||||
# this ensures that outgoing memberships from users is ignored
|
||||
memberOf: null
|
||||
groups:
|
||||
- dn: ou=access,ou=groups,ou=example,dc=example,dc=net
|
||||
options:
|
||||
filter: (&(objectClass=some-group-class)(!(groupType=email)))
|
||||
map:
|
||||
description: l
|
||||
set:
|
||||
metadata.customField: 'hello'
|
||||
map:
|
||||
# this ensures that outgoing memberships from groups is ignored
|
||||
members: null
|
||||
```
|
||||
+17
-16
@@ -141,7 +141,7 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
};
|
||||
}
|
||||
| Array<{
|
||||
@@ -221,7 +221,7 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -311,12 +311,12 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
/**
|
||||
* 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;
|
||||
members?: string | null;
|
||||
};
|
||||
}
|
||||
| Array<{
|
||||
@@ -401,12 +401,12 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
/**
|
||||
* 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;
|
||||
members?: string | null;
|
||||
};
|
||||
}>;
|
||||
/**
|
||||
@@ -556,7 +556,7 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
};
|
||||
}
|
||||
| Array<{
|
||||
@@ -588,6 +588,7 @@ export interface Config {
|
||||
pagePause?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* JSON paths (on a.b.c form) and hard coded values to set on those
|
||||
* paths.
|
||||
@@ -636,7 +637,7 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -726,12 +727,12 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
/**
|
||||
* 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;
|
||||
members?: string | null;
|
||||
};
|
||||
}
|
||||
| Array<{
|
||||
@@ -816,12 +817,12 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
/**
|
||||
* 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;
|
||||
members?: string | null;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -926,7 +927,6 @@ export interface Config {
|
||||
* 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 };
|
||||
/**
|
||||
@@ -969,7 +969,7 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1006,6 +1006,7 @@ export interface Config {
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @default false
|
||||
* JSON paths (on a.b.c form) and hard coded values to set on those
|
||||
* paths.
|
||||
*
|
||||
@@ -1058,12 +1059,12 @@ export interface Config {
|
||||
* 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;
|
||||
memberOf?: string | null;
|
||||
/**
|
||||
* 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;
|
||||
members?: string | null;
|
||||
};
|
||||
};
|
||||
/**
|
||||
|
||||
@@ -63,8 +63,8 @@ export type GroupConfig = {
|
||||
displayName: string;
|
||||
email?: string;
|
||||
picture?: string;
|
||||
memberOf: string;
|
||||
members: string;
|
||||
memberOf: string | null;
|
||||
members: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -259,7 +259,7 @@ export type UserConfig = {
|
||||
displayName: string;
|
||||
email: string;
|
||||
picture?: string;
|
||||
memberOf: string;
|
||||
memberOf: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export type UserConfig = {
|
||||
// Only the scope, filter, attributes, and paged fields are supported. The
|
||||
// default is scope "one" and attributes "*" and "+".
|
||||
options: SearchOptions;
|
||||
|
||||
// JSON paths (on a.b.c form) and hard coded values to set on those paths
|
||||
set?: { [path: string]: JsonValue };
|
||||
// Mappings from well known entity fields, to LDAP attribute names
|
||||
@@ -114,7 +115,7 @@ export type UserConfig = {
|
||||
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;
|
||||
memberOf: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -129,6 +130,7 @@ export type GroupConfig = {
|
||||
// The search options to use.
|
||||
// Only the scope, filter, attributes, and paged fields are supported.
|
||||
options: SearchOptions;
|
||||
|
||||
// JSON paths (on a.b.c form) and hard coded values to set on those paths
|
||||
set?: { [path: string]: JsonValue };
|
||||
// Mappings from well known entity fields, to LDAP attribute names
|
||||
@@ -156,10 +158,10 @@ export type GroupConfig = {
|
||||
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;
|
||||
memberOf: string | null;
|
||||
// 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;
|
||||
members: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -210,6 +210,53 @@ describe('readLdapUsers', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow skipping memberOf', async () => {
|
||||
client.getVendor.mockResolvedValue(DefaultLdapVendor);
|
||||
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
|
||||
await fn(searchEntry({ memberOf: ['x', 'y', 'z'] }));
|
||||
});
|
||||
|
||||
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: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const vendorConfig: VendorConfig = {
|
||||
dnAttributeName: 'customDN',
|
||||
uuidAttributeName: 'customUUID',
|
||||
};
|
||||
|
||||
const { userMemberOf } = await readLdapUsers(client, config, vendorConfig);
|
||||
expect(userMemberOf.size).toBe(0);
|
||||
});
|
||||
|
||||
it('transfers all attributes from Microsoft Active Directory', async () => {
|
||||
client.getVendor.mockResolvedValue(ActiveDirectoryVendor);
|
||||
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
|
||||
@@ -729,6 +776,51 @@ describe('readLdapGroups', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow skipping members', 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: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const vendorConfig: VendorConfig = {
|
||||
dnAttributeName: 'customDN',
|
||||
uuidAttributeName: 'customUUID',
|
||||
};
|
||||
|
||||
const { groupMember } = await readLdapGroups(client, config, vendorConfig);
|
||||
|
||||
expect(groupMember.size).toBe(0);
|
||||
});
|
||||
|
||||
it('can process a list of GroupConfigs', async () => {
|
||||
client.getVendor.mockResolvedValue(DefaultLdapVendor);
|
||||
client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => {
|
||||
|
||||
@@ -146,6 +146,7 @@ export async function readLdapUsers(
|
||||
mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(userMemberOf, myDn, vs);
|
||||
});
|
||||
|
||||
entities.push(entity);
|
||||
});
|
||||
}
|
||||
@@ -277,6 +278,7 @@ export async function readLdapGroups(
|
||||
mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => {
|
||||
ensureItems(groupMemberOf, myDn, vs);
|
||||
});
|
||||
|
||||
mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => {
|
||||
ensureItems(groupMember, myDn, vs);
|
||||
});
|
||||
@@ -349,7 +351,7 @@ export async function readLdapOrg(
|
||||
function mapReferencesAttr(
|
||||
entry: SearchEntry,
|
||||
vendor: LdapVendor,
|
||||
attributeName: string | undefined,
|
||||
attributeName: string | undefined | null,
|
||||
setter: (sourceDn: string, targets: string[]) => void,
|
||||
) {
|
||||
if (attributeName) {
|
||||
|
||||
Reference in New Issue
Block a user