mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
[PM-11981] Support LDAP membership with UID (#841)
--------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -35,6 +35,29 @@ const data: Jsonify<GroupEntry>[] = [
|
||||
externalId: "cn=Cleaners,ou=Janitorial,dc=bitwarden,dc=com",
|
||||
name: "Cleaners",
|
||||
},
|
||||
{
|
||||
userMemberExternalIds: [
|
||||
"cn=Painterson Miki,ou=Product Development,dc=bitwarden,dc=com",
|
||||
"cn=Virgina Pichocki,ou=Product Development,dc=bitwarden,dc=com",
|
||||
"cn=Steffen Carsten,ou=Product Development,dc=bitwarden,dc=com",
|
||||
],
|
||||
groupMemberReferenceIds: [],
|
||||
users: [],
|
||||
referenceId: "cn=DevOps Team,dc=bitwarden,dc=com",
|
||||
externalId: "cn=DevOps Team,dc=bitwarden,dc=com",
|
||||
name: "DevOps Team",
|
||||
},
|
||||
{
|
||||
userMemberExternalIds: [
|
||||
"cn=Angus Merizzi,ou=Management,dc=bitwarden,dc=com",
|
||||
"cn=Grissel Currer,ou=Management,dc=bitwarden,dc=com",
|
||||
],
|
||||
groupMemberReferenceIds: [],
|
||||
users: [],
|
||||
referenceId: "cn=Security Team,dc=bitwarden,dc=com",
|
||||
externalId: "cn=Security Team,dc=bitwarden,dc=com",
|
||||
name: "Security Team",
|
||||
},
|
||||
];
|
||||
|
||||
export const groupFixtures = data.map((g) => GroupEntry.fromJSON(g));
|
||||
|
||||
@@ -688,4 +688,27 @@ mobile: +1 804 319-5569
|
||||
pager: +1 804 815-3661
|
||||
roomNumber: 9273
|
||||
manager: cn=Inga Schnirer,ou=Product Testing,dc=bitwarden, dc=com
|
||||
secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com
|
||||
secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com
|
||||
|
||||
# DevOps Team and Security Team identify their members by the member uid attribute,
|
||||
# instead of the member Dn attribute.
|
||||
# These test that group membership by uid works correctly.
|
||||
|
||||
dn: cn=DevOps Team,dc=bitwarden,dc=com
|
||||
changetype: add
|
||||
cn: DevOps Team
|
||||
gidnumber: 800
|
||||
memberuid: mikip
|
||||
memberuid: pichockv
|
||||
memberuid: carstens
|
||||
objectclass: posixGroup
|
||||
objectclass: top
|
||||
|
||||
dn: cn=Security Team,dc=bitwarden,dc=com
|
||||
changetype: add
|
||||
cn: Security Team
|
||||
gidnumber: 900
|
||||
memberuid: merizzia
|
||||
memberuid: currerg
|
||||
objectclass: posixGroup
|
||||
objectclass: top
|
||||
@@ -118,7 +118,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
[delControl],
|
||||
);
|
||||
return regularUsers.concat(deletedUsers);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.logService.warning("Cannot query deleted users.");
|
||||
return regularUsers;
|
||||
}
|
||||
@@ -192,14 +192,21 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
this.syncConfig.userFilter,
|
||||
);
|
||||
const userPath = this.makeSearchPath(this.syncConfig.userPath);
|
||||
const userIdMap = new Map<string, string>();
|
||||
const userDnMap = new Map<string, string>();
|
||||
const userUidMap = new Map<string, string>();
|
||||
await this.search<string>(userPath, userFilter, (se: any) => {
|
||||
userIdMap.set(this.getReferenceId(se), this.getExternalId(se, this.getReferenceId(se)));
|
||||
const dn = this.getReferenceId(se);
|
||||
const uid = this.getAttr<string>(se, "uid");
|
||||
const externalId = this.getExternalId(se, dn);
|
||||
userDnMap.set(dn, externalId);
|
||||
if (uid != null) {
|
||||
userUidMap.set(uid.toLowerCase(), externalId);
|
||||
}
|
||||
return se;
|
||||
});
|
||||
|
||||
for (const se of groupSearchEntries) {
|
||||
const group = this.buildGroup(se, userIdMap);
|
||||
const group = this.buildGroup(se, userDnMap, userUidMap);
|
||||
if (group != null) {
|
||||
entries.push(group);
|
||||
}
|
||||
@@ -208,7 +215,20 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
return entries;
|
||||
}
|
||||
|
||||
private buildGroup(searchEntry: any, userMap: Map<string, string>) {
|
||||
/**
|
||||
* Builds a GroupEntry from LDAP search results, including membership.
|
||||
* Supports user membership by DN or UID and nested group membership by DN.
|
||||
*
|
||||
* @param searchEntry - The LDAP search entry containing group data
|
||||
* @param userDnMap - Map of user DNs to their external IDs
|
||||
* @param userUidMap - Map of user UIDs to their external IDs
|
||||
* @returns A populated GroupEntry object, or null if the group lacks required properties
|
||||
*/
|
||||
private buildGroup(
|
||||
searchEntry: any,
|
||||
userDnMap: Map<string, string>,
|
||||
userUidMap: Map<string, string>,
|
||||
) {
|
||||
const group = new GroupEntry();
|
||||
group.referenceId = this.getReferenceId(searchEntry);
|
||||
if (group.referenceId == null) {
|
||||
@@ -228,11 +248,34 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
|
||||
const members = this.getAttrVals<string>(searchEntry, this.syncConfig.memberAttribute);
|
||||
if (members != null) {
|
||||
for (const memDn of members) {
|
||||
if (userMap.has(memDn) && !group.userMemberExternalIds.has(userMap.get(memDn))) {
|
||||
group.userMemberExternalIds.add(userMap.get(memDn));
|
||||
} else if (!group.groupMemberReferenceIds.has(memDn)) {
|
||||
group.groupMemberReferenceIds.add(memDn);
|
||||
// Parses a group member attribute and identifies it as a member DN, member Uid, or a group Dn
|
||||
const getMemberAttributeType = (member: string): "memberDn" | "memberUid" | "groupDn" => {
|
||||
const isDnLike = member.includes("=") && member.includes(",");
|
||||
if (isDnLike) {
|
||||
return userDnMap.has(member) ? "memberDn" : "groupDn";
|
||||
}
|
||||
return "memberUid";
|
||||
};
|
||||
|
||||
for (const member of members) {
|
||||
switch (getMemberAttributeType(member)) {
|
||||
case "memberDn": {
|
||||
const externalId = userDnMap.get(member);
|
||||
if (externalId != null) {
|
||||
group.userMemberExternalIds.add(externalId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "memberUid": {
|
||||
const externalId = userUidMap.get(member.toLowerCase());
|
||||
if (externalId != null) {
|
||||
group.userMemberExternalIds.add(externalId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "groupDn":
|
||||
group.groupMemberReferenceIds.add(member);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,10 @@ describe("SyncService", () => {
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ overwriteExisting: false }),
|
||||
);
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(6);
|
||||
|
||||
// The expected number of calls may change if more data is added to the ldif
|
||||
// Make sure it equals (number of users / 4) + (number of groups / 4)
|
||||
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(7);
|
||||
|
||||
// @ts-expect-error Reset batch size to original state.
|
||||
constants.batchSize = originalBatchSize;
|
||||
|
||||
Reference in New Issue
Block a user