diff --git a/openldap/group-fixtures.ts b/openldap/group-fixtures.ts index 518ec6d5..f02351de 100644 --- a/openldap/group-fixtures.ts +++ b/openldap/group-fixtures.ts @@ -35,6 +35,29 @@ const data: Jsonify[] = [ externalId: "cn=Cleaners,ou=Janitorial,dc=bitwarden,dc=com", name: "Cleaners", }, + { + userMemberExternalIds: [ + "cn=Benjamin Chen,ou=Product Development,dc=bitwarden,dc=com", + "cn=Karen Smith,ou=Product Development,dc=bitwarden,dc=com", + "cn=Robert Johnson,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=Thomas Williams,ou=Management,dc=bitwarden,dc=com", + "cn=Michelle Brown,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)); diff --git a/openldap/ldifs/directory-20.ldif b/openldap/ldifs/directory-20.ldif index 62be217a..81655718 100644 --- a/openldap/ldifs/directory-20.ldif +++ b/openldap/ldifs/directory-20.ldif @@ -688,4 +688,173 @@ 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 + +dn: cn=DevOps Team,dc=bitwarden,dc=com +changetype: add +cn: DevOps Team +gidnumber: 800 +memberuid: ChenB +memberuid: SmithK +memberuid: JohnsonR +objectclass: posixGroup +objectclass: top + +dn: cn=Security Team,dc=bitwarden,dc=com +changetype: add +cn: Security Team +gidnumber: 900 +memberuid: WilliamsT +memberuid: BrownM +objectclass: posixGroup +objectclass: top + +dn: cn=Benjamin Chen,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Benjamin Chen +sn: Chen +description: This is Benjamin Chen's description +facsimileTelephoneNumber: +1 408 555-1234 +l: San Jose +ou: Product Development +postalAddress: Product Development$San Jose +telephoneNumber: +1 408 555-5678 +title: Senior DevOps Engineer +userPassword: Password1 +uid: ChenB +givenName: Benjamin +mail: ChenB@9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3.bitwarden.com +carLicense: 2K8N9L +departmentNumber: 1001 +employeeType: Employee +homePhone: +1 408 555-9876 +initials: B. C. +mobile: +1 408 555-4321 +pager: +1 408 555-7890 +roomNumber: 7125 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Karen Smith,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Karen Smith +sn: Smith +description: This is Karen Smith's description +facsimileTelephoneNumber: +1 415 555-2345 +l: San Francisco +ou: Product Development +postalAddress: Product Development$San Francisco +telephoneNumber: +1 415 555-6789 +title: Senior Systems Administrator +userPassword: Password1 +uid: SmithK +givenName: Karen +mail: SmithK@3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9.bitwarden.com +carLicense: 5M3P7Q +departmentNumber: 1002 +employeeType: Employee +homePhone: +1 415 555-0987 +initials: K. S. +mobile: +1 415 555-5432 +pager: +1 415 555-8901 +roomNumber: 7126 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Robert Johnson,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Robert Johnson +sn: Johnson +description: This is Robert Johnson's description +facsimileTelephoneNumber: +1 510 555-3456 +l: Oakland +ou: Product Development +postalAddress: Product Development$Oakland +telephoneNumber: +1 510 555-7890 +title: Cloud Infrastructure Engineer +userPassword: Password1 +uid: JohnsonR +givenName: Robert +mail: JohnsonR@7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3.bitwarden.com +carLicense: 8X2Y4Z +departmentNumber: 1003 +employeeType: Employee +homePhone: +1 510 555-1098 +initials: R. J. +mobile: +1 510 555-6543 +pager: +1 510 555-9012 +roomNumber: 7127 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Thomas Williams,ou=Management,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Thomas Williams +sn: Williams +description: This is Thomas Williams's description +facsimileTelephoneNumber: +1 650 555-4567 +l: Palo Alto +ou: Management +postalAddress: Management$Palo Alto +telephoneNumber: +1 650 555-8901 +title: Chief Security Officer +userPassword: Password1 +uid: WilliamsT +givenName: Thomas +mail: WilliamsT@1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7.bitwarden.com +carLicense: 6H9J2K +departmentNumber: 1004 +employeeType: Employee +homePhone: +1 650 555-2109 +initials: T. W. +mobile: +1 650 555-7654 +pager: +1 650 555-0123 +roomNumber: 7128 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Michelle Brown,ou=Management,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Michelle Brown +sn: Brown +description: This is Michelle Brown's description +facsimileTelephoneNumber: +1 408 555-5678 +l: Santa Clara +ou: Management +postalAddress: Management$Santa Clara +telephoneNumber: +1 408 555-9012 +title: Security Analyst +userPassword: Password1 +uid: BrownM +givenName: Michelle +mail: BrownM@5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1.bitwarden.com +carLicense: 4T6U8V +departmentNumber: 1005 +employeeType: Employee +homePhone: +1 408 555-3210 +initials: M. B. +mobile: +1 408 555-8765 +pager: +1 408 555-1234 +roomNumber: 7129 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com diff --git a/openldap/user-fixtures.ts b/openldap/user-fixtures.ts index d34a2eec..168d81f4 100644 --- a/openldap/user-fixtures.ts +++ b/openldap/user-fixtures.ts @@ -144,6 +144,41 @@ const data: Jsonify[] = [ externalId: "cn=Loella Mak,ou=Payroll,dc=bitwarden,dc=com", email: "makl@6ab3e25ca49d4d64aaf44844288a8ef7.bitwarden.com", }, + { + disabled: false, + deleted: false, + referenceId: "cn=Benjamin Chen,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Benjamin Chen,ou=Product Development,dc=bitwarden,dc=com", + email: "chenb@9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Karen Smith,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Karen Smith,ou=Product Development,dc=bitwarden,dc=com", + email: "smithk@3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Robert Johnson,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Robert Johnson,ou=Product Development,dc=bitwarden,dc=com", + email: "johnsonr@7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Thomas Williams,ou=Management,dc=bitwarden,dc=com", + externalId: "cn=Thomas Williams,ou=Management,dc=bitwarden,dc=com", + email: "williamst@1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Michelle Brown,ou=Management,dc=bitwarden,dc=com", + externalId: "cn=Michelle Brown,ou=Management,dc=bitwarden,dc=com", + email: "brownm@5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1.bitwarden.com", + }, ]; export const userFixtures = data.map((v) => UserEntry.fromJSON(v)); diff --git a/src/services/ldap-directory.service.integration.spec.ts b/src/services/ldap-directory.service.integration.spec.ts index 7cbf9252..0ec25074 100644 --- a/src/services/ldap-directory.service.integration.spec.ts +++ b/src/services/ldap-directory.service.integration.spec.ts @@ -152,4 +152,88 @@ describe("ldapDirectoryService", () => { expect(result).toEqual([[redTeam], undefined]); }); }); + + describe("new groups and users", () => { + it("fetches DevOps Team and Security Team groups", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ + groups: true, + groupFilter: "(|(cn=DevOps Team)(cn=Security Team))", + }), + ); + + const devOpsTeam = groupFixtures.find( + (g) => g.referenceId === "cn=DevOps Team,dc=bitwarden,dc=com", + ); + const securityTeam = groupFixtures.find( + (g) => g.referenceId === "cn=Security Team,dc=bitwarden,dc=com", + ); + + const result = await directoryService.getEntries(true, true); + expect(result[0]).toEqual(expect.arrayContaining([devOpsTeam, securityTeam])); + expect(result[0].length).toEqual(2); + }); + + it("fetches new users with correct group memberships", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ + users: true, + groups: true, + userFilter: "(|(uid=ChenB)(uid=SmithK)(uid=JohnsonR)(uid=WilliamsT)(uid=BrownM))", + }), + ); + + const newUsers = userFixtures.filter( + (u) => + u.referenceId === "cn=Benjamin Chen,ou=Product Development,dc=bitwarden,dc=com" || + u.referenceId === "cn=Karen Smith,ou=Product Development,dc=bitwarden,dc=com" || + u.referenceId === "cn=Robert Johnson,ou=Product Development,dc=bitwarden,dc=com" || + u.referenceId === "cn=Thomas Williams,ou=Management,dc=bitwarden,dc=com" || + u.referenceId === "cn=Michelle Brown,ou=Management,dc=bitwarden,dc=com", + ); + + const devOpsTeam = groupFixtures.find( + (g) => g.referenceId === "cn=DevOps Team,dc=bitwarden,dc=com", + ); + const securityTeam = groupFixtures.find( + (g) => g.referenceId === "cn=Security Team,dc=bitwarden,dc=com", + ); + + const result = await directoryService.getEntries(true, true); + + // Verify users are fetched + expect(result[1]).toEqual(expect.arrayContaining(newUsers)); + expect(result[1].length).toEqual(newUsers.length); + + // Verify groups are fetched with correct membership + expect(result[0]).toEqual(expect.arrayContaining([devOpsTeam, securityTeam])); + + // Verify DevOps Team has 3 members + const fetchedDevOpsTeam = result[0].find((g) => g.name === "DevOps Team"); + expect(fetchedDevOpsTeam.userMemberExternalIds.size).toEqual(3); + expect(Array.from(fetchedDevOpsTeam.userMemberExternalIds)).toEqual( + expect.arrayContaining([ + "cn=Benjamin Chen,ou=Product Development,dc=bitwarden,dc=com", + "cn=Karen Smith,ou=Product Development,dc=bitwarden,dc=com", + "cn=Robert Johnson,ou=Product Development,dc=bitwarden,dc=com", + ]), + ); + + // Verify Security Team has 2 members + const fetchedSecurityTeam = result[0].find((g) => g.name === "Security Team"); + expect(fetchedSecurityTeam.userMemberExternalIds.size).toEqual(2); + expect(Array.from(fetchedSecurityTeam.userMemberExternalIds)).toEqual( + expect.arrayContaining([ + "cn=Thomas Williams,ou=Management,dc=bitwarden,dc=com", + "cn=Michelle Brown,ou=Management,dc=bitwarden,dc=com", + ]), + ); + }); + }); }); diff --git a/src/services/ldap-directory.service.ts b/src/services/ldap-directory.service.ts index c7f24b55..47e21640 100644 --- a/src/services/ldap-directory.service.ts +++ b/src/services/ldap-directory.service.ts @@ -192,13 +192,13 @@ export class LdapDirectoryService implements IDirectoryService { this.syncConfig.userFilter, ); const userPath = this.makeSearchPath(this.syncConfig.userPath); - const userIdMap = new Map(); + const userDnMap = new Map(); const userUidMap = new Map(); await this.search(userPath, userFilter, (se: any) => { const dn = this.getReferenceId(se); const uid = this.getAttr(se, "uid"); const externalId = this.getExternalId(se, dn); - userIdMap.set(dn, externalId); + userDnMap.set(dn, externalId); if (uid != null) { userUidMap.set(uid, externalId); } @@ -206,7 +206,7 @@ export class LdapDirectoryService implements IDirectoryService { }); for (const se of groupSearchEntries) { - const group = this.buildGroup(se, userIdMap, userUidMap); + const group = this.buildGroup(se, userDnMap, userUidMap); if (group != null) { entries.push(group); } @@ -217,7 +217,7 @@ export class LdapDirectoryService implements IDirectoryService { private buildGroup( searchEntry: any, - userMap: Map, + userDnMap: Map, userUidMap: Map, ) { const group = new GroupEntry(); @@ -239,24 +239,33 @@ export class LdapDirectoryService implements IDirectoryService { const members = this.getAttrVals(searchEntry, this.syncConfig.memberAttribute); if (members != null) { - for (const member of members) { - // Check if member is a DN (contains '=' and ',') - const isDn = member.includes("=") && member.includes(","); + const getMemberAttributeType = (member: string): "memberDn" | "memberUid" | "groupDn" => { + const isDnLike = member.includes("=") && member.includes(","); + if (isDnLike) { + return userDnMap.has(member) ? "memberDn" : "groupDn"; + } + return "memberUid"; + }; - if (isDn) { - // Member is a DN - if (userMap.has(member) && !group.userMemberExternalIds.has(userMap.get(member))) { - group.userMemberExternalIds.add(userMap.get(member)); - } else if (!group.groupMemberReferenceIds.has(member)) { - group.groupMemberReferenceIds.add(member); + for (const member of members) { + switch (getMemberAttributeType(member)) { + case "memberDn": { + const externalId = userDnMap.get(member); + if (externalId != null) { + group.userMemberExternalIds.add(externalId); + } + break; } - } else { - // Member is likely a UID - if (userUidMap.has(member) && !group.userMemberExternalIds.has(userUidMap.get(member))) { - group.userMemberExternalIds.add(userUidMap.get(member)); - } else if (!group.groupMemberReferenceIds.has(member)) { - group.groupMemberReferenceIds.add(member); + case "memberUid": { + const externalId = userUidMap.get(member); + if (externalId != null) { + group.userMemberExternalIds.add(externalId); + } + break; } + case "groupDn": + group.groupMemberReferenceIds.add(member); + break; } } } diff --git a/src/services/sync.service.integration.spec.ts b/src/services/sync.service.integration.spec.ts index 39cd6ac2..f58bb841 100644 --- a/src/services/sync.service.integration.spec.ts +++ b/src/services/sync.service.integration.spec.ts @@ -123,7 +123,7 @@ describe("SyncService", () => { expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith( expect.objectContaining({ overwriteExisting: false }), ); - expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(6); + expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(9); // @ts-expect-error Reset batch size to original state. constants.batchSize = originalBatchSize;