1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-16 00:04:34 +00:00

Compare commits

..

41 Commits

Author SHA1 Message Date
Andy Pixley
13b5574723 [BRE-831] migrate secrets AKV (#796) 2025-07-10 12:05:51 -05:00
Jared McCannon
a35d921993 [PM-32177] - Fixing Backward Compatibility with Azure AD (#813)
* Updating the fetching of the config and key to check entra and check azure afterwards.

* Making this camelCase to match other values.

(cherry picked from commit 284206b735)
2025-07-02 09:36:03 -05:00
Vincent Salucci
3a46e1781e chore: bump version to v2025.6.1 (#812) 2025-06-30 14:08:03 -05:00
Jared McCannon
dc64f7191e [PM-21187] Rename Azure AD to Entra ID (#797)
* Changed label to entraID

* Performed rename of Azure AD to Entra ID

* Added check to maintain backward compatibility.

* Swapping Azure for Entra

* one last spot

* Adding property for the data.json for backward compatibility.

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* Removing unneeded setting using the old azure property.

* Accidentally removed. Adding entra back in.

* Adding backward compatibility comment. Added here because it's required for SecureStorageKeys

* Adding backward compatibility comments.

* Fixing comment

* Removing unused fields.

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2025-06-27 08:28:04 -05:00
renovate[bot]
570bcf1581 [deps]: Update ts-jest to v29.4.0 (#759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 14:49:56 +01:00
renovate[bot]
fc06bf401a [deps]: Update electron-log to v5.4.1 (#751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 14:41:01 +01:00
Andy Pixley
61d7c996c1 [BRE-848] Adding Workflow Permissions (#798) 2025-06-23 11:14:00 -05:00
renovate[bot]
71a19fecaa [deps]: Update sass-loader to v16.0.5 (#747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:51:51 +01:00
renovate[bot]
ae37cea276 [deps]: Update form-data to v4.0.3 (#744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:47:48 +01:00
renovate[bot]
09f1f6981c [deps]: Update @electron/rebuild to v3.7.2 (#741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 14:43:57 +01:00
Vince Grassia
ceff0559f2 Remove checksum assets from releases (#795) 2025-06-12 12:06:11 -04:00
Matt Andreko
4d55bf0527 Added explicit permissions to check-run (#794) 2025-06-09 14:55:48 -04:00
renovate[bot]
7347c1992f [deps]: Update sonarsource/sonarqube-scan-action action to v5 (#790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 14:29:32 -04:00
Brandon Treston
46d2797d8c Lock file maintenance (#791)
* [deps]: Lock file maintenance

* wip deps

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 10:04:15 -04:00
Vincent Salucci
ed58d7c758 chore: bump verstion to v2025.6.0 (#793) 2025-06-02 11:17:31 -05:00
renovate[bot]
cd6bbd792a [deps]: Update node-abi to v3.75.0 (#779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 09:52:51 -05:00
Rui Tomé
3b3ea8ac47 [PM-15456] Update AzureDirectoryService to dynamically select Graph API endpoint based on identity authority (public or government) (#777)
Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
2025-06-02 14:00:07 +01:00
Matt Andreko
5f9adf9ab7 fix: update scan workflow (#792) 2025-06-02 08:28:33 -04:00
renovate[bot]
1deb22a446 [deps]: Update eslint-config-prettier to v10.1.5 (#753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 11:29:23 -05:00
renovate[bot]
115a60316d [deps]: Update lint-staged to v15.5.2 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 11:11:09 -05:00
renovate[bot]
e11225b2ce [deps]: Update typescript-eslint monorepo to v8.32.1 (#761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 14:51:42 -05:00
renovate[bot]
4909d306ba [deps]: Update dotenv to v16.5.0 (#749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 10:53:01 -05:00
renovate[bot]
caa8c4d070 [deps]: Update core-js to v3.42.0 (#748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 09:36:15 -04:00
Brandon Treston
ed1d941282 remove dependency (#783) 2025-05-21 09:31:45 -04:00
renovate[bot]
f6f874360f [deps]: Update electron-updater to v6.6.2 (#752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 16:54:54 -04:00
renovate[bot]
18b110e70d [deps]: Update ldapts to v7.4.0 (#756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 12:54:17 -04:00
renovate[bot]
83c42cec73 [deps]: Update type-fest to v4.41.0 (#760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon Treston <btreston@bitwarden.com>
2025-05-19 11:46:23 -04:00
renovate[bot]
2d80fceb8c [deps]: Update jest-preset-angular to v14.5.5 (#745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon Treston <btreston@bitwarden.com>
2025-05-19 11:02:43 -04:00
renovate[bot]
0489f0cbe9 [deps]: Update angular-cli monorepo to v17.3.17 (#742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon Treston <btreston@bitwarden.com>
2025-05-19 10:19:53 -04:00
Brandon Treston
c5d4cb9fb6 fix null error (#782) 2025-05-14 11:12:01 -04:00
Vincent Salucci
16d6647090 chore: bump version to v2025.5.0 (#778) 2025-05-06 11:52:17 -05:00
renovate[bot]
a08673917b [deps]: Update prettier to v3.5.3 (#758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 10:52:46 +10:00
renovate[bot]
27e1ab9bcf [deps]: Update rxjs to v7.8.2 (#746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 10:32:24 +10:00
Brandon Treston
3573e201a6 [PM-20134] Fix overwriteExisting and largeImport causing users to be deleted (#737)
* Fix mixed up bools, use whole object

* disallow overwriteExisting on large syncs

* remove unused file

* add test, always set overwriteExisting to false for batched requests

* add more tests

* wip

* Clean up

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2025-04-30 09:26:15 -04:00
Brandon Treston
23d285a9f6 change g suite propertey name in example to match the docs (#739) 2025-04-15 09:51:08 -04:00
renovate[bot]
527d2cb75d [deps]: Lock file maintenance (#738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 08:43:39 -05:00
Andy Pixley
42efd689e3 [BRE-773] Fixing windows signing cert (#736) 2025-04-09 23:46:47 -04:00
Thomas Rittson
2fe980dea6 Bump version to 2024.4.0 (#735)
Note that this is effectively a re-release of 2025.1.0
in order to rollback from defective release 2025.3.0. 
rc will branch from 2025.1.0 and have this cherry-picked into it.
2025-04-10 12:53:21 +10:00
Matt Bishop
9446eedec7 Remove references to Codecov token (#734) 2025-03-27 15:51:22 -07:00
Vicki League
41ee0d82d5 [CL-570] Remove deprecated icons from use (#727) 2025-03-27 09:48:07 -04:00
renovate[bot]
40a85bb875 [deps]: Lock file maintenance (#731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 17:00:16 +00:00
34 changed files with 1827 additions and 1385 deletions

View File

@@ -9,10 +9,15 @@ on:
- "hotfix-rc" - "hotfix-rc"
workflow_dispatch: {} workflow_dispatch: {}
permissions:
contents: read
jobs: jobs:
cloc: cloc:
name: CLOC name: CLOC
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
contents: read
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -29,6 +34,8 @@ jobs:
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
contents: read
outputs: outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }} package_version: ${{ steps.retrieve-version.outputs.package_version }}
steps: steps:
@@ -50,6 +57,8 @@ jobs:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 18.5.0 _PKG_FETCH_NODE_VERSION: 18.5.0
_PKG_FETCH_VERSION: 3.4 _PKG_FETCH_VERSION: 3.4
permissions:
contents: read
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -95,11 +104,6 @@ jobs:
- name: Zip - name: Zip
run: zip -j dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip dist-cli/linux/bwdc keytar/linux/build/Release/keytar.node run: zip -j dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip dist-cli/linux/bwdc keytar/linux/build/Release/keytar.node
- name: Create checksums
run: |
shasum -a 256 dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip | \
cut -d " " -f 1 > dist-cli/bwdc-linux-sha256-$_PACKAGE_VERSION.txt
- name: Version Test - name: Version Test
run: | run: |
sudo apt-get update sudo apt-get update
@@ -129,18 +133,13 @@ jobs:
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
macos-cli: macos-cli:
name: Build Mac CLI name: Build Mac CLI
runs-on: macos-13 runs-on: macos-13
needs: setup needs: setup
permissions:
contents: read
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_PKG_FETCH_NODE_VERSION: 18.5.0 _PKG_FETCH_NODE_VERSION: 18.5.0
@@ -190,11 +189,6 @@ jobs:
- name: Zip - name: Zip
run: zip -j dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip dist-cli/macos/bwdc keytar/macos/build/Release/keytar.node run: zip -j dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip dist-cli/macos/bwdc keytar/macos/build/Release/keytar.node
- name: Create checksums
run: |
shasum -a 256 dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip | \
cut -d " " -f 1 > dist-cli/bwdc-macos-sha256-$_PACKAGE_VERSION.txt
- name: Version Test - name: Version Test
run: | run: |
mkdir -p test/macos mkdir -p test/macos
@@ -217,17 +211,13 @@ jobs:
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-cli: windows-cli:
name: Build Windows CLI name: Build Windows CLI
runs-on: windows-2022 runs-on: windows-2022
needs: setup needs: setup
permissions:
contents: read
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_WIN_PKG_FETCH_VERSION: 18.5.0 _WIN_PKG_FETCH_VERSION: 18.5.0
@@ -349,11 +339,6 @@ jobs:
Throw "Version test failed." Throw "Version test failed."
} }
- name: Create checksums
run: |
checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload Windows Zip to GitHub - name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
@@ -361,18 +346,14 @@ jobs:
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error if-no-files-found: error
- name: Upload Windows checksum to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-gui: windows-gui:
name: Build Windows GUI name: Build Windows GUI
runs-on: windows-2022 runs-on: windows-2022
needs: setup needs: setup
permissions:
contents: read
id-token: write
env: env:
NODE_OPTIONS: --max_old_space_size=4096 NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
@@ -404,15 +385,36 @@ jobs:
- name: Install Node dependencies - name: Install Node dependencies
run: npm install run: npm install
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "code-signing-vault-url,
code-signing-client-id,
code-signing-tenant-id,
code-signing-client-secret,
code-signing-cert-name"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Build & Sign - name: Build & Sign
run: npm run dist:win run: npm run dist:win
env: env:
ELECTRON_BUILDER_SIGN: 1 ELECTRON_BUILDER_SIGN: 1
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }} SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }}
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }} SIGNING_CLIENT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-client-id }}
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }} SIGNING_TENANT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-tenant-id }}
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }} SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }} SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
- name: Upload Portable Executable to GitHub - name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
@@ -447,6 +449,8 @@ jobs:
name: Build Linux GUI name: Build Linux GUI
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: setup needs: setup
permissions:
contents: read
env: env:
NODE_OPTIONS: --max_old_space_size=4096 NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
@@ -501,6 +505,9 @@ jobs:
name: Build MacOS GUI name: Build MacOS GUI
runs-on: macos-13 runs-on: macos-13
needs: setup needs: setup
permissions:
contents: read
id-token: write
env: env:
NODE_OPTIONS: --max_old_space_size=4096 NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
@@ -528,10 +535,19 @@ jobs:
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
- name: Login to Azure - name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: bitwarden/gh-actions/azure-login@main
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-directory-connector
secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER"
- name: Get certificates - name: Get certificates
run: | run: |
@@ -546,9 +562,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Set up keychain - name: Set up keychain
env: env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: | run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain security default-keychain -s build.keychain
@@ -582,13 +601,13 @@ jobs:
run: | run: |
mkdir ~/private_keys mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_UFD296548T.p8 cat << EOF > ~/private_keys/AuthKey_UFD296548T.p8
${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }}
EOF EOF
- name: Build application - name: Build application
run: npm run dist:mac run: npm run dist:mac
env: env:
APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: UFD296548T APP_STORE_CONNECT_AUTH_KEY: UFD296548T
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8 APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8
CSC_FOR_PULL_REQUEST: true CSC_FOR_PULL_REQUEST: true
@@ -634,6 +653,8 @@ jobs:
- windows-gui - windows-gui
- linux-gui - linux-gui
- macos-gui - macos-gui
permissions:
id-token: write
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: | if: |
@@ -643,11 +664,13 @@ jobs:
&& contains(needs.*.result, 'failure') && contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - CI subscription - name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
if: failure() if: failure()
uses: bitwarden/gh-actions/azure-login@main
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets - name: Retrieve secrets
id: retrieve-secrets id: retrieve-secrets
@@ -657,6 +680,9 @@ jobs:
keyvault: "bitwarden-ci" keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url" secrets: "devops-alerts-slack-webhook-url"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Notify Slack on failure - name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure() if: failure()

View File

@@ -3,6 +3,9 @@ name: Enforce PR labels
on: on:
pull_request: pull_request:
types: [labeled, unlabeled, opened, edited, synchronize] types: [labeled, unlabeled, opened, edited, synchronize]
permissions:
contents: read
pull-requests: read
jobs: jobs:
enforce-label: enforce-label:
name: EnforceLabel name: EnforceLabel

View File

@@ -74,5 +74,3 @@ jobs:
- name: Upload results to codecov.io - name: Upload results to codecov.io
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -13,10 +13,15 @@ on:
- Redeploy - Redeploy
- Dry Run - Dry Run
permissions:
contents: read
jobs: jobs:
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
contents: read
outputs: outputs:
release_version: ${{ steps.version.outputs.version }} release_version: ${{ steps.version.outputs.version }}
steps: steps:
@@ -45,6 +50,10 @@ jobs:
name: Release name: Release
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: setup needs: setup
permissions:
actions: read
packages: read
contents: write
steps: steps:
- name: Download all artifacts - name: Download all artifacts
if: ${{ inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
@@ -71,9 +80,6 @@ jobs:
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip, artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip,
./bwdc-macos-${{ env.PKG_VERSION }}.zip, ./bwdc-macos-${{ env.PKG_VERSION }}.zip,
./bwdc-linux-${{ env.PKG_VERSION }}.zip, ./bwdc-linux-${{ env.PKG_VERSION }}.zip,
./bwdc-windows-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-macos-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe, ./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe, ./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap, ./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap,

View File

@@ -5,13 +5,23 @@ on:
push: push:
branches: branches:
- "main" - "main"
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize, reopened]
branches:
- main
permissions: {}
jobs: jobs:
check-run: check-run:
name: Check PR run name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
permissions:
contents: read
sast: sast:
name: SAST scan name: SAST scan
@@ -21,6 +31,7 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
security-events: write security-events: write
id-token: write
steps: steps:
- name: Check out repo - name: Check out repo
@@ -28,16 +39,33 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Scan with Checkmarx - name: Scan with Checkmarx
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41 uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
env: env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with: with:
project_name: ${{ github.repository }} project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }} cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }}
base_uri: https://ast.checkmarx.net/ base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }} cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }} cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }}
additional_params: | additional_params: |
--report-format sarif \ --report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \ --filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
@@ -57,6 +85,7 @@ jobs:
permissions: permissions:
contents: read contents: read
pull-requests: write pull-requests: write
id-token: write
steps: steps:
- name: Check out repo - name: Check out repo
@@ -65,10 +94,27 @@ jobs:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "SONAR-TOKEN"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Scan with SonarCloud - name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1 uses: sonarsource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5.2.0
env: env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}
with: with:
args: > args: >
-Dsonar.organization=${{ github.repository_owner }} -Dsonar.organization=${{ github.repository_owner }}

View File

@@ -64,5 +64,3 @@ jobs:
- name: Upload results to codecov.io - name: Upload results to codecov.io
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2 uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -12,6 +12,9 @@ jobs:
bump_version: bump_version:
name: Bump Version name: Bump Version
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
steps: steps:
- name: Validate version input - name: Validate version input
if: ${{ inputs.version_number_override != '' }} if: ${{ inputs.version_number_override != '' }}
@@ -19,12 +22,29 @@ jobs:
with: with:
version: ${{ inputs.version_number_override }} version: ${{ inputs.version_number_override }}
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token - name: Generate GH App token
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token id: app-token
with: with:
app-id: ${{ secrets.BW_GHAPP_ID }} app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }} private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
- name: Checkout Branch - name: Checkout Branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

View File

@@ -9,7 +9,7 @@ Supported directories:
- Active Directory - Active Directory
- Any other LDAP-based directory - Any other LDAP-based directory
- Azure Active Directory - Microsoft Entra ID
- G Suite (Google) - G Suite (Google)
- Okta - Okta

View File

@@ -8,16 +8,12 @@ export class OrganizationImportRequest {
overwriteExisting = false; overwriteExisting = false;
largeImport = false; largeImport = false;
constructor( constructor(model: {
model: groups: Required<OrganizationImportGroupRequest>[];
| { users: Required<OrganizationImportMemberRequest>[];
groups: Required<OrganizationImportGroupRequest>[]; overwriteExisting: boolean;
users: Required<OrganizationImportMemberRequest>[]; largeImport: boolean;
overwriteExisting: boolean; }) {
largeImport: boolean;
}
| ImportDirectoryRequest,
) {
if (model instanceof ImportDirectoryRequest) { if (model instanceof ImportDirectoryRequest) {
this.groups = model.groups.map((g) => new OrganizationImportGroupRequest(g)); this.groups = model.groups.map((g) => new OrganizationImportGroupRequest(g));
this.members = model.users.map((u) => new OrganizationImportMemberRequest(u)); this.members = model.users.map((u) => new OrganizationImportMemberRequest(u));

2333
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/directory-connector", "name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector", "productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.", "description": "Sync your user directory to your Bitwarden organization.",
"version": "2025.3.0", "version": "2025.6.1",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",
@@ -73,15 +73,15 @@
"test:types": "npx tsc --noEmit" "test:types": "npx tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "17.3.11", "@angular-devkit/build-angular": "17.3.17",
"@angular-eslint/eslint-plugin-template": "17.5.3", "@angular-eslint/eslint-plugin-template": "17.5.3",
"@angular-eslint/template-parser": "17.5.3", "@angular-eslint/template-parser": "17.5.3",
"@angular/compiler-cli": "17.3.12", "@angular/compiler-cli": "17.3.12",
"@electron/notarize": "2.5.0", "@electron/notarize": "2.5.0",
"@electron/rebuild": "3.7.1", "@electron/rebuild": "3.7.2",
"@fluffy-spoon/substitute": "1.208.0", "@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.40.0", "@microsoft/microsoft-graph-types": "2.40.0",
"@ngtools/webpack": "17.3.11", "@ngtools/webpack": "17.3.17",
"@types/inquirer": "8.2.10", "@types/inquirer": "8.2.10",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/lowdb": "1.0.15", "@types/lowdb": "1.0.15",
@@ -90,50 +90,49 @@
"@types/node-forge": "1.3.11", "@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4", "@types/proper-lockfile": "4.1.4",
"@types/tldjs": "2.3.4", "@types/tldjs": "2.3.4",
"@typescript-eslint/eslint-plugin": "8.23.0", "@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.23.0", "@typescript-eslint/parser": "8.32.1",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"concurrently": "9.1.2", "concurrently": "9.1.2",
"copy-webpack-plugin": "12.0.2", "copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"dotenv": "16.4.7", "dotenv": "16.5.0",
"electron": "34.1.1", "electron": "34.1.1",
"electron-builder": "24.13.3", "electron-builder": "24.13.3",
"electron-log": "5.2.4", "electron-log": "5.4.1",
"electron-reload": "2.0.0-alpha.1", "electron-reload": "2.0.0-alpha.1",
"electron-store": "8.2.0", "electron-store": "8.2.0",
"electron-updater": "6.3.9", "electron-updater": "6.6.2",
"eslint": "8.57.1", "eslint": "8.57.1",
"eslint-config-prettier": "10.0.1", "eslint-config-prettier": "10.1.5",
"eslint-import-resolver-typescript": "3.7.0", "eslint-import-resolver-typescript": "3.7.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-rxjs-angular": "2.0.1",
"form-data": "4.0.1", "form-data": "4.0.3",
"html-loader": "5.1.0", "html-loader": "5.1.0",
"html-webpack-plugin": "5.6.3", "html-webpack-plugin": "5.6.3",
"husky": "9.1.7", "husky": "9.1.7",
"jest": "29.7.0", "jest": "29.7.0",
"jest-junit": "16.0.0", "jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7", "jest-mock-extended": "3.0.7",
"jest-preset-angular": "14.5.0", "jest-preset-angular": "14.5.5",
"lint-staged": "15.4.1", "lint-staged": "15.5.2",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"minimatch": "3.1.2", "node-abi": "3.75.0",
"node-abi": "3.74.0",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-loader": "2.1.0", "node-loader": "2.1.0",
"pkg": "5.8.1", "pkg": "5.8.1",
"prettier": "3.4.2", "prettier": "3.5.3",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"rxjs": "7.8.1", "rxjs": "7.8.2",
"sass": "1.79.4", "sass": "1.79.4",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"ts-jest": "29.2.5", "ts-jest": "29.4.0",
"ts-loader": "9.5.2", "ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "4.2.0", "tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "4.32.0", "type-fest": "4.41.0",
"typescript": "5.4.5", "typescript": "5.4.5",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "6.0.1", "webpack-cli": "6.0.1",
@@ -157,21 +156,22 @@
"browser-hrtime": "1.1.8", "browser-hrtime": "1.1.8",
"chalk": "4.1.2", "chalk": "4.1.2",
"commander": "13.1.0", "commander": "13.1.0",
"core-js": "3.40.0", "core-js": "3.42.0",
"form-data": "4.0.1", "form-data": "4.0.3",
"google-auth-library": "9.15.1", "google-auth-library": "9.15.1",
"googleapis": "144.0.0", "googleapis": "144.0.0",
"https-proxy-agent": "7.0.6", "https-proxy-agent": "7.0.6",
"inquirer": "8.2.6", "inquirer": "8.2.6",
"keytar": "7.9.0", "keytar": "7.9.0",
"ldapts": "7.3.1", "ldapts": "7.4.0",
"lowdb": "1.0.0", "lowdb": "1.0.0",
"ngx-toastr": "19.0.0", "ngx-toastr": "19.0.0",
"node-fetch": "2.7.0", "node-fetch": "2.7.0",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.2",
"tldjs": "2.3.1", "tldjs": "2.3.1",
"zone.js": "0.14.10" "zone.js": "0.14.10",
"parse5": "7.2.1"
}, },
"engines": { "engines": {
"node": "~22.13.0", "node": "~22.13.0",

View File

@@ -3,11 +3,15 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
import { GroupEntry } from "@/src/models/groupEntry"; import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
export interface RequestBuilderOptions {
removeDisabled: boolean;
overwriteExisting: boolean;
}
export abstract class RequestBuilder { export abstract class RequestBuilder {
buildRequest: ( buildRequest: (
groups: GroupEntry[], groups: GroupEntry[],
users: UserEntry[], users: UserEntry[],
removeDisabled: boolean, options: RequestBuilderOptions,
overwriteExisting: boolean,
) => OrganizationImportRequest[]; ) => OrganizationImportRequest[];
} }

View File

@@ -3,7 +3,7 @@ import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"
import { DirectoryType } from "@/src/enums/directoryType"; import { DirectoryType } from "@/src/enums/directoryType";
import { Account } from "@/src/models/account"; import { Account } from "@/src/models/account";
import { AzureConfiguration } from "@/src/models/azureConfiguration"; import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/src/models/ldapConfiguration"; import { LdapConfiguration } from "@/src/models/ldapConfiguration";
import { OktaConfiguration } from "@/src/models/oktaConfiguration"; import { OktaConfiguration } from "@/src/models/oktaConfiguration";
@@ -17,7 +17,7 @@ export abstract class StateService extends BaseStateServiceAbstraction<Account>
config: config:
| LdapConfiguration | LdapConfiguration
| GSuiteConfiguration | GSuiteConfiguration
| AzureConfiguration | EntraIdConfiguration
| OktaConfiguration | OktaConfiguration
| OneLoginConfiguration, | OneLoginConfiguration,
) => Promise<any>; ) => Promise<any>;
@@ -25,8 +25,8 @@ export abstract class StateService extends BaseStateServiceAbstraction<Account>
setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>; setLdapConfiguration: (value: LdapConfiguration, options?: StorageOptions) => Promise<void>;
getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>; getGsuiteConfiguration: (options?: StorageOptions) => Promise<GSuiteConfiguration>;
setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>; setGsuiteConfiguration: (value: GSuiteConfiguration, options?: StorageOptions) => Promise<void>;
getAzureConfiguration: (options?: StorageOptions) => Promise<AzureConfiguration>; getEntraConfiguration: (options?: StorageOptions) => Promise<EntraIdConfiguration>;
setAzureConfiguration: (value: AzureConfiguration, options?: StorageOptions) => Promise<void>; setEntraConfiguration: (value: EntraIdConfiguration, options?: StorageOptions) => Promise<void>;
getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>; getOktaConfiguration: (options?: StorageOptions) => Promise<OktaConfiguration>;
setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>; setOktaConfiguration: (value: OktaConfiguration, options?: StorageOptions) => Promise<void>;
getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>; getOneLoginConfiguration: (options?: StorageOptions) => Promise<OneLoginConfiguration>;

View File

@@ -22,18 +22,15 @@
class="btn btn-primary" class="btn btn-primary"
[disabled]="startForm.loading" [disabled]="startForm.loading"
> >
<i class="bwi bwi-play bwi-fw" [hidden]="startForm.loading"></i>
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!startForm.loading"></i> <i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!startForm.loading"></i>
{{ "startSync" | i18n }} {{ "startSync" | i18n }}
</button> </button>
</form> </form>
<button type="button" (click)="stop()" class="btn btn-primary"> <button type="button" (click)="stop()" class="btn btn-danger text-white">
<i class="bwi bwi-stop bwi-fw"></i>
{{ "stopSync" | i18n }} {{ "stopSync" | i18n }}
</button> </button>
<form #syncForm [appApiAction]="syncPromise" class="d-inline"> <form #syncForm [appApiAction]="syncPromise" class="d-inline">
<button type="button" (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading"> <button type="button" (click)="sync()" class="btn btn-primary" [disabled]="syncForm.loading">
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': syncForm.loading }"></i>
{{ "syncNow" | i18n }} {{ "syncNow" | i18n }}
</button> </button>
</form> </form>
@@ -51,7 +48,6 @@
[disabled]="simForm.loading" [disabled]="simForm.loading"
> >
<i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!simForm.loading"></i> <i class="bwi bwi-spinner bwi-fw bwi-spin" [hidden]="!simForm.loading"></i>
<i class="bwi bwi-bug bwi-fw" [hidden]="simForm.loading"></i>
{{ "testNow" | i18n }} {{ "testNow" | i18n }}
</button> </button>
</form> </form>

View File

@@ -242,7 +242,7 @@
</div> </div>
</div> </div>
</div> </div>
<div [hidden]="directory != directoryType.AzureActiveDirectory"> <div [hidden]="directory != directoryType.EntraID">
<div class="mb-3"> <div class="mb-3">
<label for="identityAuthority" class="form-label">{{ <label for="identityAuthority" class="form-label">{{
"identityAuthority" | i18n "identityAuthority" | i18n
@@ -251,10 +251,10 @@
class="form-select" class="form-select"
id="identityAuthority" id="identityAuthority"
name="IdentityAuthority" name="IdentityAuthority"
[(ngModel)]="azure.identityAuthority" [(ngModel)]="entra.identityAuthority"
> >
<option value="login.microsoftonline.com">Azure AD Public</option> <option value="login.microsoftonline.com">Entra Id Public</option>
<option value="login.microsoftonline.us">Azure AD Government</option> <option value="login.microsoftonline.us">Entra Id Government</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -264,7 +264,7 @@
class="form-control" class="form-control"
id="tenant" id="tenant"
name="Tenant" name="Tenant"
[(ngModel)]="azure.tenant" [(ngModel)]="entra.tenant"
/> />
<div class="form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</div> <div class="form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</div>
</div> </div>
@@ -275,29 +275,29 @@
class="form-control" class="form-control"
id="applicationId" id="applicationId"
name="ApplicationId" name="ApplicationId"
[(ngModel)]="azure.applicationId" [(ngModel)]="entra.applicationId"
/> />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="secretKey" class="form-label">{{ "secretKey" | i18n }}</label> <label for="secretKey" class="form-label">{{ "secretKey" | i18n }}</label>
<div class="input-group"> <div class="input-group">
<input <input
type="{{ showAzureKey ? 'text' : 'password' }}" type="{{ showEntraKey ? 'text' : 'password' }}"
class="form-control" class="form-control"
id="secretKey" id="secretKey"
name="SecretKey" name="SecretKey"
[(ngModel)]="azure.key" [(ngModel)]="entra.key"
/> />
<button <button
type="button" type="button"
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}" appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleAzureKey()" (click)="toggleEntraKey()"
> >
<i <i
class="bwi bwi-lg" class="bwi bwi-lg"
aria-hidden="true" aria-hidden="true"
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'" [ngClass]="showEntraKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i> ></i>
</button> </button>
</div> </div>
@@ -607,14 +607,14 @@
<div class="form-text" *ngIf="directory === directoryType.Ldap"> <div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin))) {{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div> </div>
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory"> <div class="form-text" *ngIf="directory === directoryType.EntraID">
{{ "ex" | i18n }} exclude:joe&#64;company.com {{ "ex" | i18n }} exclude:joe&#64;company.com
</div> </div>
<div class="form-text" *ngIf="directory === directoryType.Okta"> <div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John" {{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div> </div>
<div class="form-text" *ngIf="directory === directoryType.GSuite"> <div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgName=Engineering {{ "ex" | i18n }} exclude:joe&#64;company.com | orgUnitPath=/Engineering
</div> </div>
</div> </div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap"> <div class="mb-3" [hidden]="directory != directoryType.Ldap">
@@ -684,7 +684,7 @@
<div class="form-text" *ngIf="directory === directoryType.Ldap"> <div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*))) {{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div> </div>
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory"> <div class="form-text" *ngIf="directory === directoryType.EntraID">
{{ "ex" | i18n }} include:Sales,IT {{ "ex" | i18n }} include:Sales,IT
</div> </div>
<div class="form-text" *ngIf="directory === directoryType.Okta"> <div class="form-text" *ngIf="directory === directoryType.Okta">

View File

@@ -5,7 +5,7 @@ import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "../../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { DirectoryType } from "../../enums/directoryType"; import { DirectoryType } from "../../enums/directoryType";
import { AzureConfiguration } from "../../models/azureConfiguration"; import { EntraIdConfiguration } from "../../models/entraIdConfiguration";
import { GSuiteConfiguration } from "../../models/gsuiteConfiguration"; import { GSuiteConfiguration } from "../../models/gsuiteConfiguration";
import { LdapConfiguration } from "../../models/ldapConfiguration"; import { LdapConfiguration } from "../../models/ldapConfiguration";
import { OktaConfiguration } from "../../models/oktaConfiguration"; import { OktaConfiguration } from "../../models/oktaConfiguration";
@@ -22,13 +22,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
directoryType = DirectoryType; directoryType = DirectoryType;
ldap = new LdapConfiguration(); ldap = new LdapConfiguration();
gsuite = new GSuiteConfiguration(); gsuite = new GSuiteConfiguration();
azure = new AzureConfiguration(); entra = new EntraIdConfiguration();
okta = new OktaConfiguration(); okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration(); oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration(); sync = new SyncConfiguration();
directoryOptions: any[]; directoryOptions: any[];
showLdapPassword = false; showLdapPassword = false;
showAzureKey = false; showEntraKey = false;
showOktaKey = false; showOktaKey = false;
showOneLoginSecret = false; showOneLoginSecret = false;
@@ -42,7 +42,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.directoryOptions = [ this.directoryOptions = [
{ name: this.i18nService.t("select"), value: null }, { name: this.i18nService.t("select"), value: null },
{ name: "Active Directory / LDAP", value: DirectoryType.Ldap }, { name: "Active Directory / LDAP", value: DirectoryType.Ldap },
{ name: "Azure Active Directory", value: DirectoryType.AzureActiveDirectory }, { name: "Entra ID", value: DirectoryType.EntraID },
{ name: "G Suite (Google)", value: DirectoryType.GSuite }, { name: "G Suite (Google)", value: DirectoryType.GSuite },
{ name: "Okta", value: DirectoryType.Okta }, { name: "Okta", value: DirectoryType.Okta },
{ name: "OneLogin", value: DirectoryType.OneLogin }, { name: "OneLogin", value: DirectoryType.OneLogin },
@@ -56,10 +56,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.gsuite = this.gsuite =
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) || (await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite; this.gsuite;
this.azure = this.entra =
(await this.stateService.getDirectory<AzureConfiguration>( (await this.stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID)) ||
DirectoryType.AzureActiveDirectory, this.entra;
)) || this.azure;
this.okta = this.okta =
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta; (await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
this.oneLogin = this.oneLogin =
@@ -80,7 +79,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.stateService.setDirectoryType(this.directory); await this.stateService.setDirectoryType(this.directory);
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap); await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite); await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure); await this.stateService.setDirectory(DirectoryType.EntraID, this.entra);
await this.stateService.setDirectory(DirectoryType.Okta, this.okta); await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin); await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.stateService.setSync(this.sync); await this.stateService.setSync(this.sync);
@@ -135,8 +134,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
document.getElementById("password").focus(); document.getElementById("password").focus();
} }
toggleAzureKey() { toggleEntraKey() {
this.showAzureKey = !this.showAzureKey; this.showEntraKey = !this.showEntraKey;
document.getElementById("secretKey").focus(); document.getElementById("secretKey").focus();
} }

View File

@@ -2,19 +2,16 @@
<ul class="nav nav-tabs mb-3"> <ul class="nav nav-tabs mb-3">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active"> <a class="nav-link" routerLink="dashboard" routerLinkActive="active">
<i class="bwi bwi-dashboard"></i>
{{ "dashboard" | i18n }} {{ "dashboard" | i18n }}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active"> <a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs"></i>
{{ "settings" | i18n }} {{ "settings" | i18n }}
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="more" routerLinkActive="active"> <a class="nav-link" routerLink="more" routerLinkActive="active">
<i class="bwi bwi-sliders"></i>
{{ "more" | i18n }} {{ "more" | i18n }}
</a> </a>
</li> </li>

View File

@@ -8,7 +8,7 @@ import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageRes
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { AzureConfiguration } from "../models/azureConfiguration"; import { EntraIdConfiguration } from "../models/entraIdConfiguration";
import { GSuiteConfiguration } from "../models/gsuiteConfiguration"; import { GSuiteConfiguration } from "../models/gsuiteConfiguration";
import { LdapConfiguration } from "../models/ldapConfiguration"; import { LdapConfiguration } from "../models/ldapConfiguration";
import { OktaConfiguration } from "../models/oktaConfiguration"; import { OktaConfiguration } from "../models/oktaConfiguration";
@@ -20,7 +20,7 @@ export class ConfigCommand {
private directory: DirectoryType; private directory: DirectoryType;
private ldap = new LdapConfiguration(); private ldap = new LdapConfiguration();
private gsuite = new GSuiteConfiguration(); private gsuite = new GSuiteConfiguration();
private azure = new AzureConfiguration(); private entra = new EntraIdConfiguration();
private okta = new OktaConfiguration(); private okta = new OktaConfiguration();
private oneLogin = new OneLoginConfiguration(); private oneLogin = new OneLoginConfiguration();
private sync = new SyncConfiguration(); private sync = new SyncConfiguration();
@@ -54,8 +54,11 @@ export class ConfigCommand {
case "gsuite.key": case "gsuite.key":
await this.setGSuiteKey(value); await this.setGSuiteKey(value);
break; break;
// Azure Active Directory was renamed to Entra ID, but we've kept the old key name
// to be backwards compatible with existing configurations.
case "azure.key": case "azure.key":
await this.setAzureKey(value); case "entra.key":
await this.setEntraIdKey(value);
break; break;
case "okta.token": case "okta.token":
await this.setOktaToken(value); await this.setOktaToken(value);
@@ -102,9 +105,9 @@ export class ConfigCommand {
await this.saveConfig(); await this.saveConfig();
} }
private async setAzureKey(key: string) { private async setEntraIdKey(key: string) {
await this.loadConfig(); await this.loadConfig();
this.azure.key = key; this.entra.key = key;
await this.saveConfig(); await this.saveConfig();
} }
@@ -127,10 +130,9 @@ export class ConfigCommand {
this.gsuite = this.gsuite =
(await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) || (await this.stateService.getDirectory<GSuiteConfiguration>(DirectoryType.GSuite)) ||
this.gsuite; this.gsuite;
this.azure = this.entra =
(await this.stateService.getDirectory<AzureConfiguration>( (await this.stateService.getDirectory<EntraIdConfiguration>(DirectoryType.EntraID)) ||
DirectoryType.AzureActiveDirectory, this.entra;
)) || this.azure;
this.okta = this.okta =
(await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta; (await this.stateService.getDirectory<OktaConfiguration>(DirectoryType.Okta)) || this.okta;
this.oneLogin = this.oneLogin =
@@ -144,7 +146,7 @@ export class ConfigCommand {
await this.stateService.setDirectoryType(this.directory); await this.stateService.setDirectoryType(this.directory);
await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap); await this.stateService.setDirectory(DirectoryType.Ldap, this.ldap);
await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite); await this.stateService.setDirectory(DirectoryType.GSuite, this.gsuite);
await this.stateService.setDirectory(DirectoryType.AzureActiveDirectory, this.azure); await this.stateService.setDirectory(DirectoryType.EntraID, this.entra);
await this.stateService.setDirectory(DirectoryType.Okta, this.okta); await this.stateService.setDirectory(DirectoryType.Okta, this.okta);
await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin); await this.stateService.setDirectory(DirectoryType.OneLogin, this.oneLogin);
await this.stateService.setSync(this.sync); await this.stateService.setSync(this.sync);

View File

@@ -1,6 +1,6 @@
export enum DirectoryType { export enum DirectoryType {
Ldap = 0, Ldap = 0,
AzureActiveDirectory = 1, EntraID = 1,
GSuite = 2, GSuite = 2,
Okta = 3, Okta = 3,
OneLogin = 4, OneLogin = 4,

View File

@@ -2,7 +2,7 @@ import { Account as BaseAccount } from "@/jslib/common/src/models/domain/account
import { DirectoryType } from "@/src/enums/directoryType"; import { DirectoryType } from "@/src/enums/directoryType";
import { AzureConfiguration } from "./azureConfiguration"; import { EntraIdConfiguration } from "./entraIdConfiguration";
import { GSuiteConfiguration } from "./gsuiteConfiguration"; import { GSuiteConfiguration } from "./gsuiteConfiguration";
import { LdapConfiguration } from "./ldapConfiguration"; import { LdapConfiguration } from "./ldapConfiguration";
import { OktaConfiguration } from "./oktaConfiguration"; import { OktaConfiguration } from "./oktaConfiguration";
@@ -29,7 +29,10 @@ export class ClientKeys {
export class DirectoryConfigurations { export class DirectoryConfigurations {
ldap: LdapConfiguration; ldap: LdapConfiguration;
gsuite: GSuiteConfiguration; gsuite: GSuiteConfiguration;
azure: AzureConfiguration; entra: EntraIdConfiguration;
// Azure Active Directory was renamed to Entra ID, but we've kept the old account property name
// to be backwards compatible with existing configurations.
azure: EntraIdConfiguration;
okta: OktaConfiguration; okta: OktaConfiguration;
oneLogin: OneLoginConfiguration; oneLogin: OneLoginConfiguration;
} }

View File

@@ -1,6 +1,6 @@
import { IConfiguration } from "./IConfiguration"; import { IConfiguration } from "./IConfiguration";
export class AzureConfiguration implements IConfiguration { export class EntraIdConfiguration implements IConfiguration {
identityAuthority: string; identityAuthority: string;
tenant: string; tenant: string;
applicationId: string; applicationId: string;

View File

@@ -190,7 +190,7 @@ export class Program extends BaseProgram {
writeLn(" server - On-premise hosted installation URL."); writeLn(" server - On-premise hosted installation URL.");
writeLn(" directory - The type of directory to use."); writeLn(" directory - The type of directory to use.");
writeLn(" ldap.password - The password for connection to this LDAP server."); writeLn(" ldap.password - The password for connection to this LDAP server.");
writeLn(" azure.key - The Azure AD secret key."); writeLn(" entra.key - The Entra Id secret key.");
writeLn(" gsuite.key - The G Suite private key."); writeLn(" gsuite.key - The G Suite private key.");
writeLn(" okta.token - The Okta token."); writeLn(" okta.token - The Okta token.");
writeLn(" onelogin.secret - The OneLogin client secret."); writeLn(" onelogin.secret - The OneLogin client secret.");
@@ -202,7 +202,7 @@ export class Program extends BaseProgram {
writeLn(" bwdc config directory 1"); writeLn(" bwdc config directory 1");
writeLn(" bwdc config ldap.password <password>"); writeLn(" bwdc config ldap.password <password>");
writeLn(" bwdc config ldap.password --secretenv LDAP_PWD"); writeLn(" bwdc config ldap.password --secretenv LDAP_PWD");
writeLn(" bwdc config azure.key <key>"); writeLn(" bwdc config entra.key <key>");
writeLn(" bwdc config gsuite.key <key>"); writeLn(" bwdc config gsuite.key <key>");
writeLn(" bwdc config okta.token <token>"); writeLn(" bwdc config okta.token <token>");
writeLn(" bwdc config onelogin.secret <secret>"); writeLn(" bwdc config onelogin.secret <secret>");

View File

@@ -3,7 +3,7 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
import { GroupEntry } from "@/src/models/groupEntry"; import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
import { RequestBuilder } from "../abstractions/request-builder.service"; import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
import { batchSize } from "./sync.service"; import { batchSize } from "./sync.service";
@@ -16,17 +16,22 @@ export class BatchRequestBuilder implements RequestBuilder {
buildRequest( buildRequest(
groups: GroupEntry[], groups: GroupEntry[],
users: UserEntry[], users: UserEntry[],
removeDisabled: boolean, options: RequestBuilderOptions,
overwriteExisting: boolean,
): OrganizationImportRequest[] { ): OrganizationImportRequest[] {
if (options.overwriteExisting) {
throw new Error(
"You cannot use the 'Remove and re-add organization users during the next sync' option with large imports.",
);
}
const requests: OrganizationImportRequest[] = []; const requests: OrganizationImportRequest[] = [];
if (users.length > 0) { if (users?.length > 0) {
const usersRequest = users.map((u) => { const usersRequest = users.map((u) => {
return { return {
email: u.email, email: u.email,
externalId: u.externalId, externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled), deleted: u.deleted || (options.removeDisabled && u.disabled),
}; };
}); });
@@ -37,13 +42,13 @@ export class BatchRequestBuilder implements RequestBuilder {
groups: [], groups: [],
users: u, users: u,
largeImport: true, largeImport: true,
overwriteExisting, overwriteExisting: false,
}); });
requests.push(req); requests.push(req);
} }
} }
if (groups.length > 0) { if (groups?.length > 0) {
const groupRequest = groups.map((g) => { const groupRequest = groups.map((g) => {
return { return {
name: g.name, name: g.name,
@@ -59,7 +64,7 @@ export class BatchRequestBuilder implements RequestBuilder {
groups: g, groups: g,
users: [], users: [],
largeImport: true, largeImport: true,
overwriteExisting, overwriteExisting: false,
}); });
requests.push(req); requests.push(req);
} }

View File

@@ -1,46 +1,74 @@
import { GroupEntry } from "@/src/models/groupEntry"; import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
import { BatchRequestBuilder } from "./batch-request-builder"; import { BatchRequestBuilder } from "./batch-request-builder";
import { SingleRequestBuilder } from "./single-request-builder";
describe("BatchRequestBuilder", () => { describe("BatchRequestBuilder", () => {
let batchRequestBuilder: BatchRequestBuilder; let batchRequestBuilder: BatchRequestBuilder;
let singleRequestBuilder: SingleRequestBuilder;
function userSimulator(userCount: number) {
return Array(userCount).fill(new UserEntry());
}
function groupSimulator(groupCount: number) {
return Array(groupCount).fill(new GroupEntry());
}
beforeEach(async () => { beforeEach(async () => {
batchRequestBuilder = new BatchRequestBuilder(); batchRequestBuilder = new BatchRequestBuilder();
singleRequestBuilder = new SingleRequestBuilder(); });
const defaultOptions: RequestBuilderOptions = Object.freeze({
overwriteExisting: false,
removeDisabled: false,
}); });
it("BatchRequestBuilder batches requests for > 2000 users", () => { it("BatchRequestBuilder batches requests for > 2000 users", () => {
const mockGroups = groupSimulator(11000); const mockGroups = groupSimulator(11000);
const mockUsers = userSimulator(11000); const mockUsers = userSimulator(11000);
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, defaultOptions);
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);
expect(requests.length).toEqual(12); expect(requests.length).toEqual(12);
}); });
it("SingleRequestBuilder returns single request for 200 users", () => { it("BatchRequestBuilder throws error when overwriteExisting is true", () => {
const mockGroups = groupSimulator(200); const mockGroups = groupSimulator(11000);
const mockUsers = userSimulator(200); const mockUsers = userSimulator(11000);
const options = { ...defaultOptions, overwriteExisting: true };
const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, true, true); const r = () => batchRequestBuilder.buildRequest(mockGroups, mockUsers, options);
expect(requests.length).toEqual(1); expect(r).toThrow(
"You cannot use the 'Remove and re-add organization users during the next sync' option with large imports.",
);
});
it("BatchRequestBuilder returns requests with deleted users when removeDisabled is true", () => {
const mockGroups = groupSimulator(11000);
const mockUsers = userSimulator(11000);
const disabledUser1 = new UserEntry();
const disabledUserEmail1 = GetUniqueString() + "@email.com";
const disabledUser2 = new UserEntry();
const disabledUserEmail2 = GetUniqueString() + "@email.com";
disabledUser1.disabled = true;
disabledUser1.email = disabledUserEmail1;
disabledUser2.disabled = true;
disabledUser2.email = disabledUserEmail2;
mockUsers[0] = disabledUser1;
mockUsers.push(disabledUser2);
const options = { ...defaultOptions, removeDisabled: true };
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, options);
expect(requests[0].members).toContainEqual({ email: disabledUserEmail1, deleted: true });
expect(requests[1].members.find((m) => m.deleted)).toBeUndefined();
expect(requests[3].members.find((m) => m.deleted)).toBeUndefined();
expect(requests[4].members.find((m) => m.deleted)).toBeUndefined();
expect(requests[5].members).toContainEqual({ email: disabledUserEmail2, deleted: true });
}); });
it("BatchRequestBuilder retuns an empty array when there are no users or groups", () => { it("BatchRequestBuilder retuns an empty array when there are no users or groups", () => {
const requests = batchRequestBuilder.buildRequest([], [], true, true); const requests = batchRequestBuilder.buildRequest([], [], defaultOptions);
expect(requests).toEqual([]); expect(requests).toEqual([]);
}); });

View File

@@ -5,7 +5,7 @@ import { DirectoryFactoryService } from "../abstractions/directory-factory.servi
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { AzureDirectoryService } from "./azure-directory.service"; import { EntraIdDirectoryService } from "./entra-id-directory.service";
import { GSuiteDirectoryService } from "./gsuite-directory.service"; import { GSuiteDirectoryService } from "./gsuite-directory.service";
import { LdapDirectoryService } from "./ldap-directory.service"; import { LdapDirectoryService } from "./ldap-directory.service";
import { OktaDirectoryService } from "./okta-directory.service"; import { OktaDirectoryService } from "./okta-directory.service";
@@ -22,8 +22,8 @@ export class DefaultDirectoryFactoryService implements DirectoryFactoryService {
switch (directoryType) { switch (directoryType) {
case DirectoryType.GSuite: case DirectoryType.GSuite:
return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService); return new GSuiteDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.AzureActiveDirectory: case DirectoryType.EntraID:
return new AzureDirectoryService(this.logService, this.i18nService, this.stateService); return new EntraIdDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.Ldap: case DirectoryType.Ldap:
return new LdapDirectoryService(this.logService, this.i18nService, this.stateService); return new LdapDirectoryService(this.logService, this.i18nService, this.stateService);
case DirectoryType.Okta: case DirectoryType.Okta:

View File

@@ -9,7 +9,7 @@ import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { DirectoryType } from "../enums/directoryType"; import { DirectoryType } from "../enums/directoryType";
import { AzureConfiguration } from "../models/azureConfiguration"; import { EntraIdConfiguration } from "../models/entraIdConfiguration";
import { GroupEntry } from "../models/groupEntry"; import { GroupEntry } from "../models/groupEntry";
import { SyncConfiguration } from "../models/syncConfiguration"; import { SyncConfiguration } from "../models/syncConfiguration";
import { UserEntry } from "../models/userEntry"; import { UserEntry } from "../models/userEntry";
@@ -17,8 +17,10 @@ import { UserEntry } from "../models/userEntry";
import { BaseDirectoryService } from "./baseDirectory.service"; import { BaseDirectoryService } from "./baseDirectory.service";
import { IDirectoryService } from "./directory.service"; import { IDirectoryService } from "./directory.service";
const AzurePublicIdentityAuhtority = "login.microsoftonline.com"; const EntraIdPublicIdentityAuthority = "login.microsoftonline.com";
const AzureGovermentIdentityAuhtority = "login.microsoftonline.us"; const EntraIdPublicGraphEndpoint = "https://graph.microsoft.com";
const EntraIdGovernmentIdentityAuthority = "login.microsoftonline.us";
const EntraIdGovernmentGraphEndpoint = "https://graph.microsoft.us";
const NextLink = "@odata.nextLink"; const NextLink = "@odata.nextLink";
const DeltaLink = "@odata.deltaLink"; const DeltaLink = "@odata.deltaLink";
@@ -32,9 +34,9 @@ enum UserSetType {
ExcludeGroup, ExcludeGroup,
} }
export class AzureDirectoryService extends BaseDirectoryService implements IDirectoryService { export class EntraIdDirectoryService extends BaseDirectoryService implements IDirectoryService {
private client: graph.Client; private client: graph.Client;
private dirConfig: AzureConfiguration; private dirConfig: EntraIdConfiguration;
private syncConfig: SyncConfiguration; private syncConfig: SyncConfiguration;
private accessToken: string; private accessToken: string;
private accessTokenExpiration: Date; private accessTokenExpiration: Date;
@@ -50,12 +52,12 @@ export class AzureDirectoryService extends BaseDirectoryService implements IDire
async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> { async getEntries(force: boolean, test: boolean): Promise<[GroupEntry[], UserEntry[]]> {
const type = await this.stateService.getDirectoryType(); const type = await this.stateService.getDirectoryType();
if (type !== DirectoryType.AzureActiveDirectory) { if (type !== DirectoryType.EntraID) {
return; return;
} }
this.dirConfig = await this.stateService.getDirectory<AzureConfiguration>( this.dirConfig = await this.stateService.getDirectory<EntraIdConfiguration>(
DirectoryType.AzureActiveDirectory, DirectoryType.EntraID,
); );
if (this.dirConfig == null) { if (this.dirConfig == null) {
return; return;
@@ -207,7 +209,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements IDire
if (keyword === "excludeadministrativeunit" || keyword === "includeadministrativeunit") { if (keyword === "excludeadministrativeunit" || keyword === "includeadministrativeunit") {
for (const p of pieces) { for (const p of pieces) {
let auMembers = await this.client let auMembers = await this.client
.api(`https://graph.microsoft.com/v1.0/directory/administrativeUnits/${p}/members`) .api(`${this.getGraphApiEndpoint()}/v1.0/directory/administrativeUnits/${p}/members`)
.get(); .get();
// eslint-disable-next-line // eslint-disable-next-line
while (true) { while (true) {
@@ -457,10 +459,10 @@ export class AzureDirectoryService extends BaseDirectoryService implements IDire
const identityAuthority = const identityAuthority =
this.dirConfig.identityAuthority != null this.dirConfig.identityAuthority != null
? this.dirConfig.identityAuthority ? this.dirConfig.identityAuthority
: AzurePublicIdentityAuhtority; : EntraIdPublicIdentityAuthority;
if ( if (
identityAuthority !== AzurePublicIdentityAuhtority && identityAuthority !== EntraIdPublicIdentityAuthority &&
identityAuthority !== AzureGovermentIdentityAuhtority identityAuthority !== EntraIdGovernmentIdentityAuthority
) { ) {
done(new Error(this.i18nService.t("dirConfigIncomplete")), null); done(new Error(this.i18nService.t("dirConfigIncomplete")), null);
return; return;
@@ -478,7 +480,7 @@ export class AzureDirectoryService extends BaseDirectoryService implements IDire
client_id: this.dirConfig.applicationId, client_id: this.dirConfig.applicationId,
client_secret: this.dirConfig.key, client_secret: this.dirConfig.key,
grant_type: "client_credentials", grant_type: "client_credentials",
scope: "https://graph.microsoft.com/.default", scope: `${this.getGraphApiEndpoint()}/.default`,
}); });
const req = https const req = https
@@ -542,4 +544,10 @@ export class AzureDirectoryService extends BaseDirectoryService implements IDire
exp.setSeconds(exp.getSeconds() + expSeconds); exp.setSeconds(exp.getSeconds() + expSeconds);
this.accessTokenExpiration = exp; this.accessTokenExpiration = exp;
} }
private getGraphApiEndpoint(): string {
return this.dirConfig.identityAuthority === EntraIdGovernmentIdentityAuthority
? EntraIdGovernmentGraphEndpoint
: EntraIdPublicGraphEndpoint;
}
} }

View File

@@ -0,0 +1,79 @@
import { GetUniqueString } from "@/jslib/common/spec/utils";
import { UserEntry } from "@/src/models/userEntry";
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
import { SingleRequestBuilder } from "./single-request-builder";
describe("SingleRequestBuilder", () => {
let singleRequestBuilder: SingleRequestBuilder;
beforeEach(async () => {
singleRequestBuilder = new SingleRequestBuilder();
});
const defaultOptions: RequestBuilderOptions = Object.freeze({
overwriteExisting: false,
removeDisabled: false,
});
it("SingleRequestBuilder returns single request for 200 users", () => {
const mockGroups = groupSimulator(200);
const mockUsers = userSimulator(200);
const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, defaultOptions);
expect(requests.length).toEqual(1);
});
it("SingleRequestBuilder returns request with overwriteExisting enabled", () => {
const mockGroups = groupSimulator(200);
const mockUsers = userSimulator(200);
const options = { ...defaultOptions, overwriteExisting: true };
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
expect(request.overwriteExisting).toBe(true);
});
it("SingleRequestBuilder returns request with deleted user when removeDisabled is true", () => {
const mockGroups = groupSimulator(200);
const mockUsers = userSimulator(200);
const disabledUser = new UserEntry();
const disabledUserEmail = GetUniqueString() + "@example.com";
disabledUser.disabled = true;
disabledUser.email = disabledUserEmail;
mockUsers.push(disabledUser);
const options = { ...defaultOptions, removeDisabled: true };
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
expect(request.members.length).toEqual(201);
expect(request.members.pop()).toEqual(
expect.objectContaining({ email: disabledUserEmail, deleted: true }),
);
expect(request.overwriteExisting).toBe(false);
});
it("SingleRequestBuilder returns request with deleted user and overwriteExisting enabled when overwriteExisting and removeDisabled are true", () => {
const mockGroups = groupSimulator(200);
const mockUsers = userSimulator(200);
const disabledUser = new UserEntry();
const disabledUserEmail = GetUniqueString() + "@example.com";
disabledUser.disabled = true;
disabledUser.email = disabledUserEmail;
mockUsers.push(disabledUser);
const options = { overwriteExisting: true, removeDisabled: true };
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
expect(request.members.pop()).toEqual(
expect.objectContaining({ email: disabledUserEmail, deleted: true }),
);
expect(request.overwriteExisting).toBe(true);
});
});

View File

@@ -3,7 +3,7 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
import { GroupEntry } from "@/src/models/groupEntry"; import { GroupEntry } from "@/src/models/groupEntry";
import { UserEntry } from "@/src/models/userEntry"; import { UserEntry } from "@/src/models/userEntry";
import { RequestBuilder } from "../abstractions/request-builder.service"; import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
/** /**
* This class is responsible for building small (<2k users) syncs as a single * This class is responsible for building small (<2k users) syncs as a single
@@ -15,8 +15,7 @@ export class SingleRequestBuilder implements RequestBuilder {
buildRequest( buildRequest(
groups: GroupEntry[], groups: GroupEntry[],
users: UserEntry[], users: UserEntry[],
removeDisabled: boolean, options: RequestBuilderOptions,
overwriteExisting: boolean,
): OrganizationImportRequest[] { ): OrganizationImportRequest[] {
return [ return [
new OrganizationImportRequest({ new OrganizationImportRequest({
@@ -31,10 +30,10 @@ export class SingleRequestBuilder implements RequestBuilder {
return { return {
email: u.email, email: u.email,
externalId: u.externalId, externalId: u.externalId,
deleted: u.deleted || (removeDisabled && u.disabled), deleted: u.deleted || (options.removeDisabled && u.disabled),
}; };
}), }),
overwriteExisting: overwriteExisting, overwriteExisting: options.overwriteExisting,
largeImport: false, largeImport: false,
}), }),
]; ];

View File

@@ -11,7 +11,7 @@ import { StateService as StateServiceAbstraction } from "@/src/abstractions/stat
import { DirectoryType } from "@/src/enums/directoryType"; import { DirectoryType } from "@/src/enums/directoryType";
import { IConfiguration } from "@/src/models/IConfiguration"; import { IConfiguration } from "@/src/models/IConfiguration";
import { Account } from "@/src/models/account"; import { Account } from "@/src/models/account";
import { AzureConfiguration } from "@/src/models/azureConfiguration"; import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/src/models/ldapConfiguration"; import { LdapConfiguration } from "@/src/models/ldapConfiguration";
import { OktaConfiguration } from "@/src/models/oktaConfiguration"; import { OktaConfiguration } from "@/src/models/oktaConfiguration";
@@ -21,7 +21,10 @@ import { SyncConfiguration } from "@/src/models/syncConfiguration";
const SecureStorageKeys = { const SecureStorageKeys = {
ldap: "ldapPassword", ldap: "ldapPassword",
gsuite: "gsuitePrivateKey", gsuite: "gsuitePrivateKey",
// Azure Active Directory was renamed to Entra ID, but we've kept the old property name
// to be backwards compatible with existing configurations.
azure: "azureKey", azure: "azureKey",
entra: "entraKey",
okta: "oktaToken", okta: "oktaToken",
oneLogin: "oneLoginClientSecret", oneLogin: "oneLoginClientSecret",
userDelta: "userDeltaToken", userDelta: "userDeltaToken",
@@ -68,8 +71,8 @@ export class StateService
case DirectoryType.Ldap: case DirectoryType.Ldap:
(configWithSecrets as any).password = await this.getLdapKey(); (configWithSecrets as any).password = await this.getLdapKey();
break; break;
case DirectoryType.AzureActiveDirectory: case DirectoryType.EntraID:
(configWithSecrets as any).key = await this.getAzureKey(); (configWithSecrets as any).key = await this.getEntraKey();
break; break;
case DirectoryType.Okta: case DirectoryType.Okta:
(configWithSecrets as any).token = await this.getOktaKey(); (configWithSecrets as any).token = await this.getOktaKey();
@@ -93,7 +96,7 @@ export class StateService
config: config:
| LdapConfiguration | LdapConfiguration
| GSuiteConfiguration | GSuiteConfiguration
| AzureConfiguration | EntraIdConfiguration
| OktaConfiguration | OktaConfiguration
| OneLoginConfiguration, | OneLoginConfiguration,
): Promise<any> { ): Promise<any> {
@@ -106,11 +109,11 @@ export class StateService
await this.setLdapConfiguration(ldapConfig); await this.setLdapConfiguration(ldapConfig);
break; break;
} }
case DirectoryType.AzureActiveDirectory: { case DirectoryType.EntraID: {
const azureConfig = config as AzureConfiguration; const entraConfig = config as EntraIdConfiguration;
await this.setAzureKey(azureConfig.key); await this.setEntraKey(entraConfig.key);
azureConfig.key = StoredSecurely; entraConfig.key = StoredSecurely;
await this.setAzureConfiguration(azureConfig); await this.setEntraConfiguration(entraConfig);
break; break;
} }
case DirectoryType.Okta: { case DirectoryType.Okta: {
@@ -187,23 +190,32 @@ export class StateService
); );
} }
private async getAzureKey(options?: StorageOptions): Promise<string> { private async getEntraKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) { if (options?.userId == null) {
return null; return null;
} }
const entraKey = await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.entra}`,
);
if (entraKey != null) {
return entraKey;
}
return await this.secureStorageService.get<string>( return await this.secureStorageService.get<string>(
`${options.userId}_${SecureStorageKeys.azure}`, `${options.userId}_${SecureStorageKeys.azure}`,
); );
} }
private async setAzureKey(value: string, options?: StorageOptions): Promise<void> { private async setEntraKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) { if (options?.userId == null) {
return; return;
} }
await this.secureStorageService.save( await this.secureStorageService.save(
`${options.userId}_${SecureStorageKeys.azure}`, `${options.userId}_${SecureStorageKeys.entra}`,
value, value,
options, options,
); );
@@ -259,8 +271,8 @@ export class StateService
return await this.getLdapConfiguration(); return await this.getLdapConfiguration();
case DirectoryType.GSuite: case DirectoryType.GSuite:
return await this.getGsuiteConfiguration(); return await this.getGsuiteConfiguration();
case DirectoryType.AzureActiveDirectory: case DirectoryType.EntraID:
return await this.getAzureConfiguration(); return await this.getEntraConfiguration();
case DirectoryType.Okta: case DirectoryType.Okta:
return await this.getOktaConfiguration(); return await this.getOktaConfiguration();
case DirectoryType.OneLogin: case DirectoryType.OneLogin:
@@ -305,17 +317,28 @@ export class StateService
); );
} }
async getAzureConfiguration(options?: StorageOptions): Promise<AzureConfiguration> { async getEntraConfiguration(options?: StorageOptions): Promise<EntraIdConfiguration> {
const entraConfig = (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.entra;
if (entraConfig != null) {
return entraConfig;
}
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.directoryConfigurations?.azure; )?.directoryConfigurations?.azure;
} }
async setAzureConfiguration(value: AzureConfiguration, options?: StorageOptions): Promise<void> { async setEntraConfiguration(
value: EntraIdConfiguration,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount( const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()), this.reconcileOptions(options, await this.defaultOnDiskOptions()),
); );
account.directoryConfigurations.azure = value; account.directoryConfigurations.entra = value;
await this.saveAccount( await this.saveAccount(
account, account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()), this.reconcileOptions(options, await this.defaultOnDiskOptions()),

View File

@@ -3,7 +3,7 @@ import { StateMigrationService as BaseStateMigrationService } from "@/jslib/comm
import { DirectoryType } from "@/src/enums/directoryType"; import { DirectoryType } from "@/src/enums/directoryType";
import { Account, DirectoryConfigurations, DirectorySettings } from "@/src/models/account"; import { Account, DirectoryConfigurations, DirectorySettings } from "@/src/models/account";
import { AzureConfiguration } from "@/src/models/azureConfiguration"; import { EntraIdConfiguration } from "@/src/models/entraIdConfiguration";
import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration"; import { GSuiteConfiguration } from "@/src/models/gsuiteConfiguration";
import { LdapConfiguration } from "@/src/models/ldapConfiguration"; import { LdapConfiguration } from "@/src/models/ldapConfiguration";
import { OktaConfiguration } from "@/src/models/oktaConfiguration"; import { OktaConfiguration } from "@/src/models/oktaConfiguration";
@@ -14,6 +14,7 @@ const SecureStorageKeys: { [key: string]: any } = {
ldap: "ldapPassword", ldap: "ldapPassword",
gsuite: "gsuitePrivateKey", gsuite: "gsuitePrivateKey",
azure: "azureKey", azure: "azureKey",
entra: "entraIdKey",
okta: "oktaToken", okta: "oktaToken",
oneLogin: "oneLoginClientSecret", oneLogin: "oneLoginClientSecret",
directoryConfigPrefix: "directoryConfig_", directoryConfigPrefix: "directoryConfig_",
@@ -104,13 +105,16 @@ export class StateMigrationService extends BaseStateMigrationService {
} }
}; };
// Initilize typed objects from key/value pairs in storage to either be saved temporarily until an account is authed or applied to the active account // Initialize typed objects from key/value pairs in storage to either be saved temporarily until an account is authed or applied to the active account
const getDirectoryConfig = async <T>(type: DirectoryType) => const getDirectoryConfig = async <T>(type: DirectoryType) =>
await this.get<T>(SecureStorageKeys.directoryConfigPrefix + type); await this.get<T>(SecureStorageKeys.directoryConfigPrefix + type);
const directoryConfigs: DirectoryConfigurations = { const directoryConfigs: DirectoryConfigurations = {
ldap: await getDirectoryConfig<LdapConfiguration>(DirectoryType.Ldap), ldap: await getDirectoryConfig<LdapConfiguration>(DirectoryType.Ldap),
gsuite: await getDirectoryConfig<GSuiteConfiguration>(DirectoryType.GSuite), gsuite: await getDirectoryConfig<GSuiteConfiguration>(DirectoryType.GSuite),
azure: await getDirectoryConfig<AzureConfiguration>(DirectoryType.AzureActiveDirectory), // Azure Active Directory was renamed to Entra ID, but we've kept the old property name
// to be backwards compatible with existing configurations.
azure: await getDirectoryConfig<EntraIdConfiguration>(DirectoryType.EntraID),
entra: await getDirectoryConfig<EntraIdConfiguration>(DirectoryType.EntraID),
okta: await getDirectoryConfig<OktaConfiguration>(DirectoryType.Okta), okta: await getDirectoryConfig<OktaConfiguration>(DirectoryType.Okta),
oneLogin: await getDirectoryConfig<OneLoginConfiguration>(DirectoryType.OneLogin), oneLogin: await getDirectoryConfig<OneLoginConfiguration>(DirectoryType.OneLogin),
}; };

View File

@@ -0,0 +1,132 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
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 { DirectoryFactoryService } from "../abstractions/directory-factory.service";
import { DirectoryType } from "../enums/directoryType";
import { getLdapConfiguration, getSyncConfiguration } from "../utils/test-fixtures";
import { BatchRequestBuilder } from "./batch-request-builder";
import { LdapDirectoryService } from "./ldap-directory.service";
import { SingleRequestBuilder } from "./single-request-builder";
import { StateService } from "./state.service";
import { SyncService } from "./sync.service";
import * as constants from "./sync.service";
describe("SyncService", () => {
let logService: MockProxy<LogService>;
let i18nService: MockProxy<I18nService>;
let stateService: MockProxy<StateService>;
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
let apiService: MockProxy<ApiService>;
let messagingService: MockProxy<MessagingService>;
let environmentService: MockProxy<EnvironmentService>;
let directoryFactory: MockProxy<DirectoryFactoryService>;
let batchRequestBuilder: BatchRequestBuilder;
let singleRequestBuilder: SingleRequestBuilder;
let syncService: SyncService;
let directoryService: LdapDirectoryService;
const originalBatchSize = constants.batchSize;
beforeEach(() => {
logService = mock();
i18nService = mock();
stateService = mock();
cryptoFunctionService = mock();
apiService = mock();
messagingService = mock();
environmentService = mock();
directoryFactory = mock();
stateService.getDirectoryType.mockResolvedValue(DirectoryType.Ldap);
stateService.getOrganizationId.mockResolvedValue("fakeId");
directoryService = new LdapDirectoryService(logService, i18nService, stateService);
directoryFactory.createService.mockReturnValue(directoryService);
batchRequestBuilder = new BatchRequestBuilder();
singleRequestBuilder = new SingleRequestBuilder();
syncService = new SyncService(
cryptoFunctionService,
apiService,
messagingService,
i18nService,
environmentService,
stateService,
batchRequestBuilder,
singleRequestBuilder,
directoryFactory,
);
});
describe("OpenLdap integration: ", () => {
it("with largeImport disabled matches directory fixture data", async () => {
stateService.getDirectory
.calledWith(DirectoryType.Ldap)
.mockResolvedValue(getLdapConfiguration());
stateService.getSync.mockResolvedValue(
getSyncConfiguration({
users: true,
groups: true,
largeImport: false,
overwriteExisting: false,
}),
);
cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1));
// This arranges the last hash to be differet from the ArrayBuffer after it is converted to b64
stateService.getLastSyncHash.mockResolvedValue("unique hash");
const syncResult = await syncService.sync(false, false);
expect(syncResult).toEqual([groupFixtures, userFixtures]);
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(
expect.objectContaining({ overwriteExisting: false }),
);
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(1);
});
it("with largeImport enabled matches directory fixture data", async () => {
stateService.getDirectory
.calledWith(DirectoryType.Ldap)
.mockResolvedValue(getLdapConfiguration());
stateService.getSync.mockResolvedValue(
getSyncConfiguration({
users: true,
groups: true,
largeImport: true,
overwriteExisting: false,
}),
);
cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1));
// This arranges the last hash to be differet from the ArrayBuffer after it is converted to b64
stateService.getLastSyncHash.mockResolvedValue("unique hash");
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
constants.batchSize = 4;
const syncResult = await syncService.sync(false, false);
expect(syncResult).toEqual([groupFixtures, userFixtures]);
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(
expect.objectContaining({ overwriteExisting: false }),
);
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(6);
// @ts-expect-error Reset batch size to original state.
constants.batchSize = originalBatchSize;
});
});
});

View File

@@ -34,6 +34,8 @@ describe("SyncService", () => {
let syncService: SyncService; let syncService: SyncService;
const originalBatchSize = constants.batchSize;
beforeEach(() => { beforeEach(() => {
cryptoFunctionService = mock(); cryptoFunctionService = mock();
apiService = mock(); apiService = mock();
@@ -115,11 +117,12 @@ describe("SyncService", () => {
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[3]); expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[3]);
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[4]); expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[4]);
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]); expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]);
// @ts-expect-error Reset batch size back to original value.
constants.batchSize = originalBatchSize;
}); });
it("does not post for the same hash", async () => { it("does not post for the same hash", async () => {
// @ts-expect-error this sets the batch size back to its expexted value for this test.
constants.batchSize = 2000;
stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true })); stateService.getSync.mockResolvedValue(getSyncConfiguration({ groups: true, users: true }));
cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1)); cryptoFunctionService.hash.mockResolvedValue(new ArrayBuffer(1));
// This arranges the last hash to be the same as the ArrayBuffer after it is converted to b64 // This arranges the last hash to be the same as the ArrayBuffer after it is converted to b64

View File

@@ -83,13 +83,7 @@ export class SyncService {
return [groups, users]; return [groups, users];
} }
const reqs = this.buildRequest( const reqs = this.buildRequest(groups, users, syncConfig);
groups,
users,
syncConfig.removeDisabled,
syncConfig.overwriteExisting,
syncConfig.largeImport,
);
const result: HashResult = await this.generateHash(reqs); const result: HashResult = await this.generateHash(reqs);
@@ -219,24 +213,12 @@ export class SyncService {
private buildRequest( private buildRequest(
groups: GroupEntry[], groups: GroupEntry[],
users: UserEntry[], users: UserEntry[],
removeDisabled: boolean, syncConfig: SyncConfiguration,
overwriteExisting: boolean,
largeImport = false,
): OrganizationImportRequest[] { ): OrganizationImportRequest[] {
if (largeImport && groups.length + users.length > batchSize) { if (syncConfig.largeImport && (groups?.length ?? 0) + (users?.length ?? 0) > batchSize) {
return this.batchRequestBuilder.buildRequest( return this.batchRequestBuilder.buildRequest(groups, users, syncConfig);
groups,
users,
overwriteExisting,
removeDisabled,
);
} else { } else {
return this.singleRequestBuilder.buildRequest( return this.singleRequestBuilder.buildRequest(groups, users, syncConfig);
groups,
users,
overwriteExisting,
removeDisabled,
);
} }
} }

View File

@@ -0,0 +1,26 @@
import { GetUniqueString } from "@/jslib/common/spec/utils";
import { GroupEntry } from "../models/groupEntry";
import { UserEntry } from "../models/userEntry";
export function userSimulator(userCount: number): UserEntry[] {
const users: UserEntry[] = [];
while (userCount > 0) {
const userEntry = new UserEntry();
userEntry.email = GetUniqueString() + "@example.com";
users.push(userEntry);
userCount--;
}
return users;
}
export function groupSimulator(groupCount: number): GroupEntry[] {
const groups: GroupEntry[] = [];
while (groupCount > 0) {
const groupEntry = new GroupEntry();
groupEntry.name = GetUniqueString();
groups.push(groupEntry);
groupCount--;
}
return groups;
}