From d65f42684e8e472370b2fb71fabfd28f34ecfce3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:17:00 +1000 Subject: [PATCH] [PM-13008] Add ldap integration tests (#637) --- .github/workflows/integration-test.yml | 96 +++ docker-compose.yml | 18 + openldap/group-fixtures.ts | 40 + openldap/ldifs/directory.ldif | 691 ++++++++++++++++++ openldap/mkcert.sh | 10 + openldap/user-fixtures.ts | 149 ++++ package.json | 9 +- src/models/groupEntry.ts | 36 + src/models/userEntry.ts | 24 + ...ldap-directory.service.integration.spec.ts | 207 ++++++ 10 files changed, 1277 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/integration-test.yml create mode 100644 docker-compose.yml create mode 100644 openldap/group-fixtures.ts create mode 100644 openldap/ldifs/directory.ldif create mode 100755 openldap/mkcert.sh create mode 100644 openldap/user-fixtures.ts create mode 100644 src/services/ldap-directory.service.integration.spec.ts diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 000000000..5174c62d7 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,96 @@ +name: Integration Testing + +on: + workflow_dispatch: + push: + branches: + - "main" + paths: + - ".github/workflows/integration-test.yml" # this file + - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment + pull_request: + paths: + - ".github/workflows/integration-test.yml" # this file + - "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment + +jobs: + check-test-secrets: + name: Check for test secrets + runs-on: ubuntu-22.04 + outputs: + available: ${{ steps.check-test-secrets.outputs.available }} + permissions: + contents: read + + steps: + - name: Check + id: check-test-secrets + run: | + if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then + echo "available=true" >> $GITHUB_OUTPUT; + else + echo "available=false" >> $GITHUB_OUTPUT; + fi + + testing: + name: Run tests + if: ${{ startsWith(github.head_ref, 'version_bump_') == false }} + runs-on: ubuntu-22.04 + needs: check-test-secrets + permissions: + checks: write + contents: read + pull-requests: write + + steps: + - name: Check out repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Get Node version + id: retrieve-node-version + run: | + NODE_NVMRC=$(cat .nvmrc) + NODE_VERSION=${NODE_NVMRC/v/''} + echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + + - name: Set up Node + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + node-version: ${{ steps.retrieve-node-version.outputs.node_version }} + + - name: Install Node dependencies + run: npm ci + + - name: Install mkcert + run: | + sudo apt-get update + sudo apt-get -y install mkcert + + - name: Setup integration tests + run: npm run test:integration:setup + + - name: Run integration tests + run: npm run test:integration --coverage + + - name: Report test results + uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 + if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }} + with: + name: Test Results + path: "junit.xml" + reporter: jest-junit + fail-on-error: true + + - name: Upload coverage to codecov.io + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + if: ${{ needs.check-test-secrets.outputs.available == 'true' }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload results to codecov.io + uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0 + if: ${{ needs.check-test-secrets.outputs.available == 'true' }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..8b836bc74 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + open-ldap: + image: bitnami/openldap:latest + hostname: openldap + environment: + - LDAP_ADMIN_USERNAME=admin + - LDAP_ADMIN_PASSWORD=admin + - LDAP_ROOT=dc=bitwarden,dc=com + - LDAP_ENABLE_TLS=yes + - LDAP_TLS_CERT_FILE=/certs/openldap.pem + - LDAP_TLS_KEY_FILE=/certs/openldap-key.pem + - LDAP_TLS_CA_FILE=/certs/rootCA.pem + volumes: + - "./openldap/ldifs:/ldifs" + - "./openldap/certs:/certs" + ports: + - "1389:1389" + - "1636:1636" diff --git a/openldap/group-fixtures.ts b/openldap/group-fixtures.ts new file mode 100644 index 000000000..518ec6d51 --- /dev/null +++ b/openldap/group-fixtures.ts @@ -0,0 +1,40 @@ +import { Jsonify } from "type-fest"; + +import { GroupEntry } from "../src/models/groupEntry"; + +// These must match the ldap server seed data in directory.ldif +const data: Jsonify[] = [ + { + userMemberExternalIds: [ + "cn=Loella Mak,ou=Payroll,dc=bitwarden,dc=com", + "cn=Painterson Miki,ou=Product Development,dc=bitwarden,dc=com", + "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com", + ], + groupMemberReferenceIds: [], + users: [], + referenceId: "cn=Blue Team,dc=bitwarden,dc=com", + externalId: "cn=Blue Team,dc=bitwarden,dc=com", + name: "Blue Team", + }, + { + userMemberExternalIds: [ + "cn=Shiela Harada,ou=Peons,dc=bitwarden,dc=com", + "cn=Micaela Doud,ou=Janitorial,dc=bitwarden,dc=com", + ], + groupMemberReferenceIds: [], + users: [], + referenceId: "cn=Red Team,dc=bitwarden,dc=com", + externalId: "cn=Red Team,dc=bitwarden,dc=com", + name: "Red Team", + }, + { + userMemberExternalIds: [], + groupMemberReferenceIds: [], + users: [], + referenceId: "cn=Cleaners,ou=Janitorial,dc=bitwarden,dc=com", + externalId: "cn=Cleaners,ou=Janitorial,dc=bitwarden,dc=com", + name: "Cleaners", + }, +]; + +export const groupFixtures = data.map((g) => GroupEntry.fromJSON(g)); diff --git a/openldap/ldifs/directory.ldif b/openldap/ldifs/directory.ldif new file mode 100644 index 000000000..62be217a0 --- /dev/null +++ b/openldap/ldifs/directory.ldif @@ -0,0 +1,691 @@ +version: 1 + +dn: dc=bitwarden,dc=com +dc: bitwarden +objectClass: dcObject +objectClass: organization +o: Bitwarden + +dn: ou=Accounting,dc=bitwarden,dc=com +changetype: add +ou: Accounting +objectClass: top +objectClass: organizationalUnit + +dn: ou=Product Development,dc=bitwarden, dc=com +changetype: add +ou: Product Development +objectClass: top +objectClass: organizationalUnit + +dn: ou=Product Testing,dc=bitwarden, dc=com +changetype: add +ou: Product Testing +objectClass: top +objectClass: organizationalUnit + +dn: ou=Human Resources,dc=bitwarden, dc=com +changetype: add +ou: Human Resources +objectClass: top +objectClass: organizationalUnit + +dn: ou=Payroll,dc=bitwarden, dc=com +changetype: add +ou: Payroll +objectClass: top +objectClass: organizationalUnit + +dn: ou=Janitorial,dc=bitwarden, dc=com +changetype: add +ou: Janitorial +objectClass: top +objectClass: organizationalUnit + +dn: ou=Management,dc=bitwarden, dc=com +changetype: add +ou: Management +objectClass: top +objectClass: organizationalUnit + +dn: ou=Administrative,dc=bitwarden, dc=com +changetype: add +ou: Administrative +objectClass: top +objectClass: organizationalUnit + +dn: ou=Peons,dc=bitwarden, dc=com +changetype: add +ou: Peons +objectClass: top +objectClass: organizationalUnit + +dn: ou=Planning,dc=bitwarden, dc=com +changetype: add +ou: Planning +objectClass: top +objectClass: organizationalUnit + +dn: cn=Blue Team,dc=bitwarden,dc=com +changetype: add +cn: Blue Team +gidnumber: 500 +memberuid: cn=Loella Mak,ou=Payroll,dc=bitwarden,dc=com +memberuid: cn=Painterson Miki,ou=Product Development,dc=bitwarden,dc=com +memberuid: cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com +objectclass: posixGroup +objectclass: top + +dn: cn=Red Team,dc=bitwarden,dc=com +changetype: add +cn: Red Team +gidnumber: 600 +memberuid: cn=Shiela Harada,ou=Peons,dc=bitwarden,dc=com +memberuid: cn=Micaela Doud,ou=Janitorial,dc=bitwarden,dc=com +objectclass: posixGroup +objectclass: top + +dn: cn=Cleaners,ou=Janitorial,dc=bitwarden,dc=com +changetype: add +cn: Cleaners +gidnumber: 700 +objectclass: posixGroup +objectclass: top + +dn: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Roland Dyke +sn: Dyke +description: This is Roland Dyke's description +facsimileTelephoneNumber: +1 804 674-5794 +l: San Francisco +ou: Human Resources +postalAddress: Human Resources$San Francisco +telephoneNumber: +1 804 831-5121 +title: Supreme Human Resources Writer +userPassword: Password1 +uid: DykeR +givenName: Roland +mail: DykeR@220af87272f04218bb8dd81d50fb19f5.bitwarden.com +carLicense: 4CMGOJ +departmentNumber: 2838 +employeeType: Contract +homePhone: +1 804 936-4965 +initials: R. D. +mobile: +1 804 592-3734 +pager: +1 804 285-2962 +roomNumber: 9890 +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com +manager: cn=Linnell Kinstley,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Charin Goulfine,ou=Human Resources,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Charin Goulfine +sn: Goulfine +description: This is Charin Goulfine's description +facsimileTelephoneNumber: +1 408 916-3906 +l: Redmond +ou: Human Resources +postalAddress: Human Resources$Redmond +telephoneNumber: +1 408 551-2393 +title: Junior Human Resources Punk +userPassword: Password1 +uid: GoulfinC +givenName: Charin +mail: GoulfinC@5fa9a69302a9422abedbd51aa69f472b.bitwarden.com +carLicense: FVUOG5 +departmentNumber: 9411 +employeeType: Contract +homePhone: +1 408 606-3616 +initials: C. G. +mobile: +1 408 369-8824 +pager: +1 408 930-6584 +roomNumber: 9793 + +dn: cn=Margit Peters,ou=Product Testing,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Margit Peters +sn: Peters +description: This is Margit Peters's description +facsimileTelephoneNumber: +1 804 822-1892 +l: Orem +ou: Product Testing +postalAddress: Product Testing$Orem +telephoneNumber: +1 804 930-5577 +title: Supreme Product Testing Dictator +userPassword: Password1 +uid: PetersM +givenName: Margit +mail: PetersM@909269ac94be4e5b9ff6809f52b1dda3.bitwarden.com +carLicense: IKKDEY +departmentNumber: 5202 +employeeType: Employee +homePhone: +1 804 405-5226 +initials: M. P. +mobile: +1 804 210-8735 +pager: +1 804 533-8191 +roomNumber: 9814 +manager: cn=Inga Schnirer,ou=Product Testing,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Shiela Harada,ou=Peons,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Shiela Harada +sn: Harada +description: This is Shiela Harada's description +facsimileTelephoneNumber: +1 510 773-7858 +l: Cambridge +ou: Peons +postalAddress: Peons$Cambridge +telephoneNumber: +1 510 358-8117 +title: Supreme Peons Technician +userPassword: Password1 +uid: HaradaS +givenName: Shiela +mail: HaradaS@2782a7fada1240d682fc754affb31519.bitwarden.com +carLicense: 9KNKLC +departmentNumber: 8729 +employeeType: Normal +homePhone: +1 510 412-1076 +initials: S. H. +mobile: +1 510 458-4453 +pager: +1 510 554-4842 +roomNumber: 9387 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Angelle Guarino,ou=Human Resources,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Angelle Guarino +sn: Guarino +description: This is Angelle Guarino's description +facsimileTelephoneNumber: +1 510 783-6849 +l: Armonk +ou: Human Resources +postalAddress: Human Resources$Armonk +telephoneNumber: +1 510 560-4384 +title: Junior Human Resources Vice President +userPassword: Password1 +uid: GuarinoA +givenName: Angelle +mail: GuarinoA@720ab4266da34c8e9ccf5ef3370b892b.bitwarden.com +carLicense: K24T4G +departmentNumber: 5102 +employeeType: Normal +homePhone: +1 510 712-7834 +initials: A. G. +mobile: +1 510 528-1331 +pager: +1 510 991-3515 +roomNumber: 8053 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Lynnea Dasilva,ou=Janitorial,dc=bitwarden, dc=com + +dn: cn=Shela Khoury,ou=Peons,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Shela Khoury +sn: Khoury +description: This is Shela Khoury's description +facsimileTelephoneNumber: +1 415 829-6000 +l: San Mateo +ou: Peons +postalAddress: Peons$San Mateo +telephoneNumber: +1 415 518-4568 +title: Supreme Peons Sales Rep +userPassword: Password1 +uid: KhouryS +givenName: Shela +mail: KhouryS@4dc1a2d970bb4c23aef0d860b6018ed6.bitwarden.com +carLicense: VX8Y9Q +departmentNumber: 5125 +employeeType: Contract +homePhone: +1 415 368-5121 +initials: S. K. +mobile: +1 415 224-9289 +pager: +1 415 722-7004 +roomNumber: 9511 +manager: cn=Tilmon Kuzbary,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Vicuong Dyba,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Micaela Doud,ou=Janitorial,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Micaela Doud +sn: Doud +description: This is Micaela Doud's description +facsimileTelephoneNumber: +1 804 989-1007 +l: Sunnyvale +ou: Janitorial +postalAddress: Janitorial$Sunnyvale +telephoneNumber: +1 804 763-6986 +title: Associate Janitorial Warrior +userPassword: Password1 +uid: DoudM +givenName: Micaela +mail: DoudM@b2de0606c7904578b184a63046aa1a59.bitwarden.com +carLicense: U51VUR +departmentNumber: 3449 +employeeType: Employee +homePhone: +1 804 833-8174 +initials: M. D. +mobile: +1 804 255-5188 +pager: +1 804 908-3999 +roomNumber: 9336 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Lynnea Dasilva,ou=Janitorial,dc=bitwarden, dc=com + +dn: cn=Marthe Kenik,ou=Product Testing,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Marthe Kenik +sn: Kenik +description: This is Marthe Kenik's description +facsimileTelephoneNumber: +1 510 622-1104 +l: San Jose +ou: Product Testing +postalAddress: Product Testing$San Jose +telephoneNumber: +1 510 752-7722 +title: Master Product Testing Director +userPassword: Password1 +uid: KenikM +givenName: Marthe +mail: KenikM@5ce7099e829941498f32f6630dda9440.bitwarden.com +carLicense: R07O7A +departmentNumber: 4904 +employeeType: Employee +homePhone: +1 510 326-5669 +initials: M. K. +mobile: +1 510 270-2177 +pager: +1 510 375-6349 +roomNumber: 9653 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Angus Merizzi,ou=Management,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Angus Merizzi +sn: Merizzi +description: This is Angus Merizzi's description +facsimileTelephoneNumber: +1 818 188-3223 +l: Palo Alto +ou: Management +postalAddress: Management$Palo Alto +telephoneNumber: +1 818 210-9559 +title: Master Management Technician +userPassword: Password1 +uid: MerizziA +givenName: Angus +mail: MerizziA@7f1912f54e7a4efa8a33a6ba82fc7102.bitwarden.com +carLicense: W89F9T +departmentNumber: 9737 +employeeType: Normal +homePhone: +1 818 529-9985 +initials: A. M. +mobile: +1 818 317-4882 +pager: +1 818 942-6439 +roomNumber: 9060 +manager: cn=Inga Schnirer,ou=Product Testing,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Edmund Kardos,ou=Product Testing,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Edmund Kardos +sn: Kardos +description: This is Edmund Kardos's description +facsimileTelephoneNumber: +1 804 374-4959 +l: Redwood Shores +ou: Product Testing +postalAddress: Product Testing$Redwood Shores +telephoneNumber: +1 804 624-9693 +title: Supreme Product Testing Janitor +userPassword: Password1 +uid: KardosE +givenName: Edmund +mail: KardosE@5f41ae90bad64d5e97b0ef849f0cfd8f.bitwarden.com +carLicense: 7N9K02 +departmentNumber: 2240 +employeeType: Normal +homePhone: +1 804 100-4518 +initials: E. K. +mobile: +1 804 604-1454 +pager: +1 804 472-8093 +roomNumber: 9668 +manager: cn=Tilmon Kuzbary,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Lynnea Dasilva,ou=Janitorial,dc=bitwarden, dc=com + +dn: cn=Joyann Frucci,ou=Administrative,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Joyann Frucci +sn: Frucci +description: This is Joyann Frucci's description +facsimileTelephoneNumber: +1 804 209-8112 +l: San Jose +ou: Administrative +postalAddress: Administrative$San Jose +telephoneNumber: +1 804 384-8334 +title: Supreme Administrative Manager +userPassword: Password1 +uid: FrucciJ +givenName: Joyann +mail: FrucciJ@55f85638346c4f81b665496e3fee8d10.bitwarden.com +carLicense: 7HRDG5 +departmentNumber: 8188 +employeeType: Contract +homePhone: +1 804 364-9627 +initials: J. F. +mobile: +1 804 578-9507 +pager: +1 804 336-2260 +roomNumber: 9547 +manager: cn=Tilmon Kuzbary,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Lynnea Dasilva,ou=Janitorial,dc=bitwarden, dc=com + +dn: cn=Painterson Miki,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Painterson Miki +sn: Miki +description: This is Painterson Miki's description +facsimileTelephoneNumber: +1 206 873-3690 +l: Alameda +ou: Product Development +postalAddress: Product Development$Alameda +telephoneNumber: +1 206 715-5086 +title: Supreme Product Development Dictator +userPassword: Password1 +uid: MikiP +givenName: Painterson +mail: MikiP@2c4fec4ef77046e1b1e4b34fd50dd6a9.bitwarden.com +carLicense: 194N6J +departmentNumber: 8374 +employeeType: Normal +homePhone: +1 206 983-4256 +initials: P. M. +mobile: +1 206 124-4934 +pager: +1 206 554-6019 +roomNumber: 9715 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Jammie De Los,ou=Peons,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Jammie De Los +sn: De Los +description: This is Jammie De Los's description +facsimileTelephoneNumber: +1 818 184-1742 +l: San Mateo +ou: Peons +postalAddress: Peons$San Mateo +telephoneNumber: +1 818 390-3544 +title: Chief Peons Visionary +userPassword: Password1 +uid: De LosJ +givenName: Jammie +mail: DeLosJ@8575686b747a4c5ab6b0a7ac30503d95.bitwarden.com +carLicense: 0GKM0H +departmentNumber: 2547 +employeeType: Normal +homePhone: +1 818 277-5492 +initials: J. D. +mobile: +1 818 560-9044 +pager: +1 818 390-1519 +roomNumber: 9392 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Vicuong Dyba,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Virgina Pichocki,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Virgina Pichocki +sn: Pichocki +description: This is Virgina Pichocki's description +facsimileTelephoneNumber: +1 415 921-7139 +l: San Francisco +ou: Product Development +postalAddress: Product Development$San Francisco +telephoneNumber: +1 415 400-5576 +title: Master Product Development Evangelist +userPassword: Password1 +uid: PichockV +givenName: Virgina +mail: PichockV@2b7d385172624c81935f26cfb5f852c0.bitwarden.com +carLicense: N25ABP +departmentNumber: 2635 +employeeType: Normal +homePhone: +1 415 736-3487 +initials: V. P. +mobile: +1 415 122-7774 +pager: +1 415 305-4401 +roomNumber: 8444 +manager: cn=Inga Schnirer,ou=Product Testing,dc=bitwarden, dc=com +secretary: cn=Vicuong Dyba,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Steffen Carsten,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Steffen Carsten +sn: Carsten +description: This is Steffen Carsten's description +facsimileTelephoneNumber: +1 206 207-4197 +l: Orem +ou: Product Development +postalAddress: Product Development$Orem +telephoneNumber: +1 206 973-4002 +title: Associate Product Development Czar +userPassword: Password1 +uid: CarstenS +givenName: Steffen +mail: CarstenS@c8b8d0d540194a31b14e399b8e0ac7ff.bitwarden.com +carLicense: URONSV +departmentNumber: 3796 +employeeType: Employee +homePhone: +1 206 494-8029 +initials: S. C. +mobile: +1 206 667-8663 +pager: +1 206 183-2075 +roomNumber: 9917 +manager: cn=Inga Schnirer,ou=Product Testing,dc=bitwarden, dc=com +secretary: cn=Keven Gilleland,ou=Administrative,dc=bitwarden, dc=com + +dn: cn=Elna Drescher,ou=Administrative,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Elna Drescher +sn: Drescher +description: This is Elna Drescher's description +facsimileTelephoneNumber: +1 408 792-9983 +l: Cambridge +ou: Administrative +postalAddress: Administrative$Cambridge +telephoneNumber: +1 408 697-1740 +title: Master Administrative Writer +userPassword: Password1 +uid: DrescheE +givenName: Elna +mail: DrescheE@4ab96258683b45799b4cb34e5e13e2ee.bitwarden.com +carLicense: NYIO0U +departmentNumber: 4493 +employeeType: Contract +homePhone: +1 408 947-7311 +initials: E. D. +mobile: +1 408 266-4623 +pager: +1 408 793-5306 +roomNumber: 8652 +manager: cn=Tilmon Kuzbary,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Lynnea Dasilva,ou=Janitorial,dc=bitwarden, dc=com + +dn: cn=Gwen Kardomateas,ou=Product Development,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Gwen Kardomateas +sn: Kardomateas +description: This is Gwen Kardomateas's description +facsimileTelephoneNumber: +1 415 281-2558 +l: Alameda +ou: Product Development +postalAddress: Product Development$Alameda +telephoneNumber: +1 415 920-9857 +title: Master Product Development Punk +userPassword: Password1 +uid: KardomaG +givenName: Gwen +mail: KardomaG@7bc2a22aa7d345f1bef866ce890bec49.bitwarden.com +carLicense: Q9JPFV +departmentNumber: 7746 +employeeType: Contract +homePhone: +1 415 256-9106 +initials: G. K. +mobile: +1 415 649-9091 +pager: +1 415 896-8495 +roomNumber: 8294 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Vicuong Dyba,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Reeta Roldan,ou=Administrative,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Reeta Roldan +sn: Roldan +description: This is Reeta Roldan's description +facsimileTelephoneNumber: +1 510 354-2580 +l: Santa Clara +ou: Administrative +postalAddress: Administrative$Santa Clara +telephoneNumber: +1 510 144-9610 +title: Chief Administrative Fellow +userPassword: Password1 +uid: RoldanR +givenName: Reeta +mail: RoldanR@108c2ffc1189457d80b27e9b862163f4.bitwarden.com +carLicense: WD7J8D +departmentNumber: 1219 +employeeType: Contract +homePhone: +1 510 749-9885 +initials: R. R. +mobile: +1 510 356-5695 +pager: +1 510 216-4863 +roomNumber: 8243 +manager: cn=Roland Dyke,ou=Human Resources,dc=bitwarden, dc=com +secretary: cn=Vicuong Dyba,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Grissel Currer,ou=Management,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Grissel Currer +sn: Currer +description: This is Grissel Currer's description +facsimileTelephoneNumber: +1 818 393-4507 +l: Sunnyvale +ou: Management +postalAddress: Management$Sunnyvale +telephoneNumber: +1 818 522-3933 +title: Junior Management Artist +userPassword: Password1 +uid: CurrerG +givenName: Grissel +mail: CurrerG@248d38bb7c664c8f9d2a64525819610e.bitwarden.com +carLicense: M8TKUS +departmentNumber: 1814 +employeeType: Normal +homePhone: +1 818 803-8000 +initials: G. C. +mobile: +1 818 714-9944 +pager: +1 818 100-5018 +roomNumber: 9199 +manager: cn=Inga Schnirer,ou=Product Testing,dc=bitwarden, dc=com +secretary: cn=Vicuong Dyba,ou=Product Development,dc=bitwarden, dc=com + +dn: cn=Loella Mak,ou=Payroll,dc=bitwarden, dc=com +changetype: add +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: Loella Mak +sn: Mak +description: This is Loella Mak's description +facsimileTelephoneNumber: +1 804 152-4298 +l: Menlo Park +ou: Payroll +postalAddress: Payroll$Menlo Park +telephoneNumber: +1 804 289-8864 +title: Junior Payroll Punk +userPassword: Password1 +uid: MakL +givenName: Loella +mail: MakL@6ab3e25ca49d4d64aaf44844288a8ef7.bitwarden.com +carLicense: YVN4UL +departmentNumber: 5904 +employeeType: Employee +homePhone: +1 804 749-7856 +initials: L. M. +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 diff --git a/openldap/mkcert.sh b/openldap/mkcert.sh new file mode 100755 index 000000000..3543bed77 --- /dev/null +++ b/openldap/mkcert.sh @@ -0,0 +1,10 @@ +if ! [ -x "$(command -v mkcert)" ]; then + echo 'Error: mkcert is not installed. Install mkcert first and then re-run this script.' + echo 'e.g. brew install mkcert' + exit 1 +fi + +mkcert -install +mkdir -p ./openldap/certs +cp $(mkcert -CAROOT)/rootCA.pem ./openldap/certs/rootCA.pem +mkcert -key-file ./openldap/certs/openldap-key.pem -cert-file ./openldap/certs/openldap.pem localhost openldap diff --git a/openldap/user-fixtures.ts b/openldap/user-fixtures.ts new file mode 100644 index 000000000..d34a2eec5 --- /dev/null +++ b/openldap/user-fixtures.ts @@ -0,0 +1,149 @@ +import { Jsonify } from "type-fest"; + +import { UserEntry } from "../src/models/userEntry"; + +// These must match the ldap server seed data in directory.ldif +const data: Jsonify[] = [ + { + disabled: false, + deleted: false, + referenceId: "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com", + externalId: "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com", + email: "dyker@220af87272f04218bb8dd81d50fb19f5.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Charin Goulfine,ou=Human Resources,dc=bitwarden,dc=com", + externalId: "cn=Charin Goulfine,ou=Human Resources,dc=bitwarden,dc=com", + email: "goulfinc@5fa9a69302a9422abedbd51aa69f472b.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Margit Peters,ou=Product Testing,dc=bitwarden,dc=com", + externalId: "cn=Margit Peters,ou=Product Testing,dc=bitwarden,dc=com", + email: "petersm@909269ac94be4e5b9ff6809f52b1dda3.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Shiela Harada,ou=Peons,dc=bitwarden,dc=com", + externalId: "cn=Shiela Harada,ou=Peons,dc=bitwarden,dc=com", + email: "haradas@2782a7fada1240d682fc754affb31519.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Angelle Guarino,ou=Human Resources,dc=bitwarden,dc=com", + externalId: "cn=Angelle Guarino,ou=Human Resources,dc=bitwarden,dc=com", + email: "guarinoa@720ab4266da34c8e9ccf5ef3370b892b.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Shela Khoury,ou=Peons,dc=bitwarden,dc=com", + externalId: "cn=Shela Khoury,ou=Peons,dc=bitwarden,dc=com", + email: "khourys@4dc1a2d970bb4c23aef0d860b6018ed6.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Micaela Doud,ou=Janitorial,dc=bitwarden,dc=com", + externalId: "cn=Micaela Doud,ou=Janitorial,dc=bitwarden,dc=com", + email: "doudm@b2de0606c7904578b184a63046aa1a59.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Marthe Kenik,ou=Product Testing,dc=bitwarden,dc=com", + externalId: "cn=Marthe Kenik,ou=Product Testing,dc=bitwarden,dc=com", + email: "kenikm@5ce7099e829941498f32f6630dda9440.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Angus Merizzi,ou=Management,dc=bitwarden,dc=com", + externalId: "cn=Angus Merizzi,ou=Management,dc=bitwarden,dc=com", + email: "merizzia@7f1912f54e7a4efa8a33a6ba82fc7102.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Edmund Kardos,ou=Product Testing,dc=bitwarden,dc=com", + externalId: "cn=Edmund Kardos,ou=Product Testing,dc=bitwarden,dc=com", + email: "kardose@5f41ae90bad64d5e97b0ef849f0cfd8f.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Joyann Frucci,ou=Administrative,dc=bitwarden,dc=com", + externalId: "cn=Joyann Frucci,ou=Administrative,dc=bitwarden,dc=com", + email: "fruccij@55f85638346c4f81b665496e3fee8d10.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Painterson Miki,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Painterson Miki,ou=Product Development,dc=bitwarden,dc=com", + email: "mikip@2c4fec4ef77046e1b1e4b34fd50dd6a9.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Jammie De Los,ou=Peons,dc=bitwarden,dc=com", + externalId: "cn=Jammie De Los,ou=Peons,dc=bitwarden,dc=com", + email: "delosj@8575686b747a4c5ab6b0a7ac30503d95.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Virgina Pichocki,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Virgina Pichocki,ou=Product Development,dc=bitwarden,dc=com", + email: "pichockv@2b7d385172624c81935f26cfb5f852c0.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Steffen Carsten,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Steffen Carsten,ou=Product Development,dc=bitwarden,dc=com", + email: "carstens@c8b8d0d540194a31b14e399b8e0ac7ff.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Elna Drescher,ou=Administrative,dc=bitwarden,dc=com", + externalId: "cn=Elna Drescher,ou=Administrative,dc=bitwarden,dc=com", + email: "dreschee@4ab96258683b45799b4cb34e5e13e2ee.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Gwen Kardomateas,ou=Product Development,dc=bitwarden,dc=com", + externalId: "cn=Gwen Kardomateas,ou=Product Development,dc=bitwarden,dc=com", + email: "kardomag@7bc2a22aa7d345f1bef866ce890bec49.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Reeta Roldan,ou=Administrative,dc=bitwarden,dc=com", + externalId: "cn=Reeta Roldan,ou=Administrative,dc=bitwarden,dc=com", + email: "roldanr@108c2ffc1189457d80b27e9b862163f4.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Grissel Currer,ou=Management,dc=bitwarden,dc=com", + externalId: "cn=Grissel Currer,ou=Management,dc=bitwarden,dc=com", + email: "currerg@248d38bb7c664c8f9d2a64525819610e.bitwarden.com", + }, + { + disabled: false, + deleted: false, + referenceId: "cn=Loella Mak,ou=Payroll,dc=bitwarden,dc=com", + externalId: "cn=Loella Mak,ou=Payroll,dc=bitwarden,dc=com", + email: "makl@6ab3e25ca49d4d64aaf44844288a8ef7.bitwarden.com", + }, +]; + +export const userFixtures = data.map((v) => UserEntry.fromJSON(v)); diff --git a/package.json b/package.json index 43b29fd31..7d317998f 100644 --- a/package.json +++ b/package.json @@ -64,9 +64,12 @@ "publish:win": "npm run build:dist && npm run clean:dist && electron-builder --win --x64 --ia32 -p always -c.win.certificateSubjectName=\"8bit Solutions LLC\"", "prettier": "prettier --write .", "prepare": "husky install", - "test": "jest", - "test:watch": "jest --watch", - "test:watch:all": "jest --watchAll", + "test": "jest --testPathIgnorePatterns=.integration.spec.ts", + "test:watch": "jest --watch --testPathIgnorePatterns=.integration.spec.ts", + "test:watch:all": "jest --watchAll --testPathIgnorePatterns=.integration.spec.ts", + "test:integration": "jest .integration.spec.ts", + "test:integration:watch": "jest .integration.spec.ts --watch", + "test:integration:setup": "sh ./openldap/mkcert.sh && docker compose up -d", "test:types": "npx tsc --noEmit" }, "devDependencies": { diff --git a/src/models/groupEntry.ts b/src/models/groupEntry.ts index 39b946446..de7d56868 100644 --- a/src/models/groupEntry.ts +++ b/src/models/groupEntry.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Entry } from "./entry"; import { UserEntry } from "./userEntry"; @@ -14,4 +16,38 @@ export class GroupEntry extends Entry { return this.name; } + + toJSON() { + return { + name: this.name, + referenceId: this.referenceId, + externalId: this.externalId, + userMemberExternalIds: + this.userMemberExternalIds == null ? null : [...this.userMemberExternalIds], + groupMemberReferenceIds: + this.groupMemberReferenceIds == null ? null : [...this.groupMemberReferenceIds], + users: this.users?.map((u) => u.toJSON()), + }; + } + + static fromJSON(data: Jsonify) { + const result = new GroupEntry(); + result.referenceId = data.referenceId; + result.externalId = data.externalId; + result.name = data.name; + + if (data.userMemberExternalIds != null) { + result.userMemberExternalIds = new Set(data.userMemberExternalIds); + } + + if (data.groupMemberReferenceIds != null) { + result.groupMemberReferenceIds = new Set(data.groupMemberReferenceIds); + } + + if (data.users != null) { + result.users = data.users.map((u) => UserEntry.fromJSON(u)); + } + + return result; + } } diff --git a/src/models/userEntry.ts b/src/models/userEntry.ts index be9a61cff..9af9ffa1a 100644 --- a/src/models/userEntry.ts +++ b/src/models/userEntry.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Entry } from "./entry"; export class UserEntry extends Entry { @@ -12,4 +14,26 @@ export class UserEntry extends Entry { return this.email; } + + toJSON() { + return { + referenceId: this.referenceId, + externalId: this.externalId, + email: this.email, + disabled: this.disabled, + deleted: this.deleted, + }; + } + + static fromJSON(data: Jsonify) { + const result = new UserEntry(); + result.referenceId = data.referenceId; + result.externalId = data.externalId; + + result.email = data.email; + result.disabled = data.disabled; + result.deleted = data.deleted; + + return result; + } } diff --git a/src/services/ldap-directory.service.integration.spec.ts b/src/services/ldap-directory.service.integration.spec.ts new file mode 100644 index 000000000..af06fb5ce --- /dev/null +++ b/src/services/ldap-directory.service.integration.spec.ts @@ -0,0 +1,207 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "../../jslib/common/src/abstractions/i18n.service"; +import { LogService } from "../../jslib/common/src/abstractions/log.service"; +import { groupFixtures } from "../../openldap/group-fixtures"; +import { userFixtures } from "../../openldap/user-fixtures"; +import { DirectoryType } from "../enums/directoryType"; +import { LdapConfiguration } from "../models/ldapConfiguration"; +import { SyncConfiguration } from "../models/syncConfiguration"; + +import { LdapDirectoryService } from "./ldap-directory.service"; +import { StateService } from "./state.service"; + +// These tests integrate with the OpenLDAP docker image and seed data located in the openldap folder. +// To run theses tests: +// Install mkcert, e.g.: brew install mkcert +// Configure the environment: npm run test:integration:setup +// Run tests: npm run test:integration:watch + +describe("ldapDirectoryService", () => { + let logService: MockProxy; + let i18nService: MockProxy; + let stateService: MockProxy; + + let directoryService: LdapDirectoryService; + + beforeEach(() => { + logService = mock(); + i18nService = mock(); + stateService = mock(); + + stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap); + stateService.getLastUserSync.mockResolvedValue(null); // do not filter results by last modified date + i18nService.t.mockImplementation((id) => id); // passthrough implementation for any error messages + + directoryService = new LdapDirectoryService(logService, i18nService, stateService); + }); + + describe("basic sync fetching users and groups", () => { + it("with an unencrypted connection", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true })); + + const result = await directoryService.getEntries(true, true); + expect(result).toEqual([groupFixtures, userFixtures]); + }); + + // StartTLS opportunistically encrypts an otherwise unencrypted connection and therefore uses the same port + it("with StartTLS + SSL", async () => { + stateService.getDirectory.calledWith(DirectoryType.Ldap).mockResolvedValue( + getLdapConfiguration({ + ssl: true, + startTls: true, + tlsCaPath: "./openldap/certs/rootCA.pem", + }), + ); + stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true })); + + const result = await directoryService.getEntries(true, true); + expect(result).toEqual([groupFixtures, userFixtures]); + }); + + // The ldaps protocol requires use of SSL and uses the secure port + it("with SSL using the ldaps protocol", async () => { + stateService.getDirectory.calledWith(DirectoryType.Ldap).mockResolvedValue( + getLdapConfiguration({ + port: 1636, + ssl: true, + sslCaPath: "./openldap/certs/rootCA.pem", + }), + ); + stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true })); + + const result = await directoryService.getEntries(true, true); + expect(result).toEqual([groupFixtures, userFixtures]); + }); + }); + + describe("users", () => { + it("respects the users path", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ + users: true, + userPath: "ou=Human Resources", + }), + ); + + // These users are in the Human Resources ou + const hrUsers = userFixtures.filter( + (u) => + u.referenceId === "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com" || + u.referenceId === "cn=Charin Goulfine,ou=Human Resources,dc=bitwarden,dc=com" || + u.referenceId === "cn=Angelle Guarino,ou=Human Resources,dc=bitwarden,dc=com", + ); + + const result = await directoryService.getEntries(true, true); + expect(result[1]).toEqual(expect.arrayContaining(hrUsers)); + expect(result[1].length).toEqual(hrUsers.length); + }); + + it("filters users", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ users: true, userFilter: "(cn=Roland Dyke)" }), + ); + + const roland = userFixtures.find( + (u) => u.referenceId === "cn=Roland Dyke,ou=Human Resources,dc=bitwarden,dc=com", + ); + const result = await directoryService.getEntries(true, true); + expect(result).toEqual([undefined, [roland]]); + }); + }); + + describe("groups", () => { + it("respects the groups path", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ + groups: true, + groupPath: "ou=Janitorial", + }), + ); + + // These groups are in the Janitorial ou + const janitorialGroups = groupFixtures.filter((g) => g.name === "Cleaners"); + + const result = await directoryService.getEntries(true, true); + expect(result).toEqual([janitorialGroups, undefined]); + }); + + it("filters groups", async () => { + stateService.getDirectory + .calledWith(DirectoryType.Ldap) + .mockResolvedValue(getLdapConfiguration()); + stateService.getSync.mockResolvedValue( + getSyncConfiguration({ groups: true, groupFilter: "(cn=Red Team)" }), + ); + + const redTeam = groupFixtures.find( + (u) => u.referenceId === "cn=Red Team,dc=bitwarden,dc=com", + ); + const result = await directoryService.getEntries(true, true); + expect(result).toEqual([[redTeam], undefined]); + }); + }); +}); + +/** + * @returns a basic ldap configuration without TLS/SSL enabled. Can be overridden by passing in a partial configuration. + */ +const getLdapConfiguration = (config?: Partial): LdapConfiguration => ({ + ssl: false, + startTls: false, + tlsCaPath: null, + sslAllowUnauthorized: false, + sslCertPath: null, + sslKeyPath: null, + sslCaPath: null, + hostname: "localhost", + port: 1389, + domain: null, + rootPath: "dc=bitwarden,dc=com", + currentUser: false, + username: "cn=admin,dc=bitwarden,dc=com", + password: "admin", + ad: false, + pagedSearch: false, + ...(config ?? {}), +}); + +/** + * @returns a basic sync configuration. Can be overridden by passing in a partial configuration. + */ +const getSyncConfiguration = (config?: Partial): SyncConfiguration => ({ + users: false, + groups: false, + interval: 5, + userFilter: null, + groupFilter: null, + removeDisabled: false, + overwriteExisting: false, + largeImport: false, + // Ldap properties + groupObjectClass: "posixGroup", + userObjectClass: "person", + groupPath: null, + userPath: null, + groupNameAttribute: "cn", + userEmailAttribute: "mail", + memberAttribute: "memberUid", + useEmailPrefixSuffix: false, + emailPrefixAttribute: "sAMAccountName", + emailSuffix: null, + creationDateAttribute: "whenCreated", + revisionDateAttribute: "whenChanged", + ...(config ?? {}), +});