From 83f9061474f54cbb4923f907d3bd83396ad6a0c1 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:54:28 -0400 Subject: [PATCH 001/179] [BRE-831] migrate secrets akv (#15158) --- .github/workflows/build-browser-target.yml | 5 + .github/workflows/build-browser.yml | 60 ++++++-- .github/workflows/build-cli-target.yml | 5 + .github/workflows/build-cli.yml | 61 ++++++-- .github/workflows/build-desktop-target.yml | 5 + .github/workflows/build-desktop.yml | 136 ++++++++++++++---- .github/workflows/build-web-target.yml | 6 + .github/workflows/build-web.yml | 60 +++++--- .github/workflows/chromatic.yml | 26 +++- .github/workflows/crowdin-pull.yml | 41 ++++-- .github/workflows/deploy-web.yml | 77 ++++++---- .github/workflows/lint-crowdin-config.yml | 22 ++- .github/workflows/publish-cli.yml | 53 +++++-- .github/workflows/publish-desktop.yml | 50 +++++-- .github/workflows/publish-web.yml | 30 +++- .github/workflows/release-desktop-beta.yml | 112 ++++++++++++--- .github/workflows/repository-management.yml | 54 ++++++- .../retrieve-current-desktop-rollout.yml | 13 +- .github/workflows/staged-rollout-desktop.yml | 13 +- .github/workflows/version-auto-bump.yml | 24 +++- 20 files changed, 680 insertions(+), 173 deletions(-) diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml index a2ae48d419b..ef3beef4b8b 100644 --- a/.github/workflows/build-browser-target.yml +++ b/.github/workflows/build-browser-target.yml @@ -28,6 +28,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read run-workflow: name: Build Browser @@ -35,4 +37,7 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} uses: ./.github/workflows/build-browser.yml secrets: inherit + permissions: + contents: read + id-token: write diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 40b03d9e753..bd7d70e8543 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -41,7 +41,8 @@ defaults: run: shell: bash -permissions: {} +permissions: + contents: read jobs: setup: @@ -77,10 +78,8 @@ jobs: - name: Check secrets id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT @@ -302,6 +301,9 @@ jobs: build-safari: name: Build Safari runs-on: macos-13 + permissions: + contents: read + id-token: write needs: - setup - locales-test @@ -327,10 +329,19 @@ jobs: node --version npm --version - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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-clients + secrets: "KEYCHAIN-PASSWORD" - name: Download Provisioning Profiles secrets env: @@ -366,9 +377,12 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | 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 env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -440,6 +454,10 @@ jobs: name: Crowdin Push if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + id-token: write needs: - build - build-safari @@ -449,10 +467,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -461,6 +481,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "crowdin-api-token" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Upload Sources uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1 env: @@ -478,6 +501,9 @@ jobs: name: Check for failures if: always() runs-on: ubuntu-22.04 + permissions: + contents: read + id-token: write needs: - setup - locales-test @@ -493,11 +519,13 @@ jobs: && contains(needs.*.result, 'failure') run: exit 1 - - name: Login to Azure - Prod Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure if: failure() + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -507,6 +535,10 @@ jobs: keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" + - name: Log out from Azure + if: failure() + uses: bitwarden/gh-actions/azure-logout@main + - name: Notify Slack on failure uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml index 6b493d4e6d9..54865ddaddd 100644 --- a/.github/workflows/build-cli-target.yml +++ b/.github/workflows/build-cli-target.yml @@ -28,6 +28,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read run-workflow: name: Build CLI @@ -35,4 +37,7 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} uses: ./.github/workflows/build-cli.yml secrets: inherit + permissions: + contents: read + id-token: write diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index ac314a4c33a..b31b22b926e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -78,10 +78,8 @@ jobs: - name: Check secrets id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT @@ -108,6 +106,10 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} _WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_VERSION: 3.5 + permissions: + contents: read + id-token: write + steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -156,9 +158,11 @@ jobs: - name: Login to Azure if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main 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 certificates if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} @@ -168,10 +172,21 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 + - name: Get Azure Key Vault secrets + id: get-kv-secrets + if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-clients + secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Set up keychain if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -199,13 +214,13 @@ jobs: run: | mkdir ~/private_keys cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} + ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} EOF - name: Notarize app if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} 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: 6TV9MKN3GP APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 run: | @@ -261,6 +276,9 @@ jobs: { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } ] runs-on: windows-2022 + permissions: + contents: read + id-token: write needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} @@ -344,11 +362,13 @@ jobs: ResourceHacker -open version-info.rc -save version-info.res -action compile ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action addoverwrite -resource version-info.res - - name: Login to Azure + - name: Log in to Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -362,6 +382,10 @@ jobs: code-signing-client-secret, code-signing-cert-name" + - name: Log out from Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: bitwarden/gh-actions/azure-logout@main + - name: Install run: npm ci working-directory: ./ @@ -520,6 +544,9 @@ jobs: name: Check for failures if: always() runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write needs: - setup - cli @@ -534,11 +561,13 @@ jobs: && contains(needs.*.result, 'failure') run: exit 1 - - name: Login to Azure - Prod Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure if: failure() + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -548,6 +577,10 @@ jobs: keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" + - name: Log out from Azure + if: failure() + uses: bitwarden/gh-actions/azure-logout@main + - name: Notify Slack on failure uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml index fa21b3fe5d9..31ac819a3e6 100644 --- a/.github/workflows/build-desktop-target.yml +++ b/.github/workflows/build-desktop-target.yml @@ -28,6 +28,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read run-workflow: name: Build Desktop @@ -35,4 +37,7 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} uses: ./.github/workflows/build-desktop.yml secrets: inherit + permissions: + contents: read + id-token: write diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index a022fe7fd0f..366d439fb45 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -147,10 +147,8 @@ jobs: - name: Check secrets id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT linux: @@ -404,6 +402,9 @@ jobs: runs-on: windows-2022 needs: - setup + permissions: + contents: read + id-token: write defaults: run: shell: pwsh @@ -438,11 +439,13 @@ jobs: choco --version rustup show - - name: Login to Azure + - name: Log in to Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -456,6 +459,10 @@ jobs: code-signing-client-secret, code-signing-cert-name" + - name: Log out from Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: bitwarden/gh-actions/azure-logout@main + - name: Install Node dependencies run: npm ci working-directory: ./ @@ -655,6 +662,9 @@ jobs: runs-on: macos-13 needs: - setup + permissions: + contents: read + id-token: write env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -700,11 +710,21 @@ jobs: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - - name: Login to Azure + - name: Log in to Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main 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 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-clients + secrets: "KEYCHAIN-PASSWORD" - name: Download Provisioning Profiles secrets if: ${{ needs.setup.outputs.has_secrets == 'true' }} @@ -747,10 +767,14 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 + - name: Log out from Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: bitwarden/gh-actions/azure-logout@main + - name: Set up keychain if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -850,6 +874,10 @@ jobs: if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: ./.github/workflows/build-browser.yml secrets: inherit + permissions: + contents: write + pull-requests: write + id-token: write macos-package-github: @@ -860,6 +888,9 @@ jobs: - browser-build - macos-build - setup + permissions: + contents: read + id-token: write env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -905,10 +936,19 @@ jobs: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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-clients + secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER" - name: Download Provisioning Profiles secrets env: @@ -949,9 +989,12 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | 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 env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -1055,12 +1098,12 @@ jobs: run: | mkdir ~/private_keys cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} + ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} EOF - name: Build application (dist) 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: 6TV9MKN3GP APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 CSC_FOR_PULL_REQUEST: true @@ -1103,6 +1146,9 @@ jobs: - browser-build - macos-build - setup + permissions: + contents: read + id-token: write env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -1148,10 +1194,19 @@ jobs: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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-clients + secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER" - name: Retrieve Slack secret id: retrieve-slack-secret @@ -1199,9 +1254,12 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | 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 env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -1305,12 +1363,12 @@ jobs: run: | mkdir ~/private_keys cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} + ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} EOF - name: Build application for App Store 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: 6TV9MKN3GP APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 CSC_FOR_PULL_REQUEST: true @@ -1334,7 +1392,7 @@ jobs: cat << EOF > ~/secrets/appstoreconnect-fastlane.json { - "issuer_id": "${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}", + "issuer_id": "${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}", "key_id": "6TV9MKN3GP", "key": "$KEY_WITHOUT_NEWLINES" } @@ -1346,7 +1404,7 @@ jobs: github.event_name != 'pull_request_target' && (inputs.testflight_distribute || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') 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: 6TV9MKN3GP BRANCH: ${{ github.ref }} run: | @@ -1396,6 +1454,10 @@ jobs: - windows - macos-package-github - macos-package-mas + permissions: + contents: write + pull-requests: write + id-token: write runs-on: ubuntu-22.04 steps: - name: Check out repo @@ -1403,10 +1465,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -1415,6 +1479,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "crowdin-api-token" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Upload Sources uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1 env: @@ -1442,6 +1509,9 @@ jobs: - macos-package-github - macos-package-mas - crowdin-push + permissions: + contents: read + id-token: write steps: - name: Check if any job failed if: | @@ -1450,11 +1520,13 @@ jobs: && contains(needs.*.result, 'failure') run: exit 1 - - name: Login to Azure - Prod Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure if: failure() + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -1464,6 +1536,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Notify Slack on failure uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() @@ -1471,3 +1546,4 @@ jobs: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} with: status: ${{ job.status }} + diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml index ca10e6d46f2..b1055885400 100644 --- a/.github/workflows/build-web-target.yml +++ b/.github/workflows/build-web-target.yml @@ -27,6 +27,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read run-workflow: name: Build Web @@ -34,4 +36,8 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} uses: ./.github/workflows/build-web.yml secrets: inherit + permissions: + contents: read + id-token: write + security-events: write diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 745376b46d8..b4163d161cf 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -51,7 +51,8 @@ env: _AZ_REGISTRY: bitwardenprod.azurecr.io _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} -permissions: {} +permissions: + contents: read jobs: setup: @@ -80,10 +81,8 @@ jobs: - name: Check secrets id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT @@ -204,11 +203,13 @@ jobs: uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 ########## ACRs ########## - - name: Login to Prod Azure + - name: Log in to Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Log into Prod container registry if: ${{ needs.setup.outputs.has_secrets == 'true' }} @@ -328,11 +329,19 @@ jobs: - name: Log out of Docker run: docker logout $_AZ_REGISTRY + - name: Log out from Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} + uses: bitwarden/gh-actions/azure-logout@main + crowdin-push: name: Crowdin Push if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' needs: build-containers + permissions: + contents: write + pull-requests: write + id-token: write runs-on: ubuntu-24.04 steps: - name: Check out repo @@ -340,10 +349,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -352,6 +363,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "crowdin-api-token" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Upload Sources uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1 env: @@ -370,11 +384,15 @@ jobs: if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 needs: build-containers + permissions: + id-token: write steps: - - name: Login to Azure - CI Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 github PAT secrets id: retrieve-secret-pat @@ -383,6 +401,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Trigger web vault deploy using GitHub Run ID uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: @@ -409,6 +430,8 @@ jobs: - build-containers - crowdin-push - trigger-web-vault-deploy + permissions: + id-token: write steps: - name: Check if any job failed if: | @@ -417,11 +440,13 @@ jobs: && contains(needs.*.result, 'failure') run: exit 1 - - name: Login to Azure - Prod Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main if: failure() 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 id: retrieve-secrets @@ -431,6 +456,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Notify Slack on failure uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 78733bc5a8b..4ee39305f84 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -15,6 +15,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read chromatic: name: Chromatic @@ -23,6 +25,7 @@ jobs: permissions: contents: read pull-requests: write + id-token: write steps: - name: Check out repo @@ -30,13 +33,13 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - + - name: Get changed files id: get-changed-files-for-chromatic uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | - storyFiles: + storyFiles: - "apps/!(cli)/**" - "bitwarden_license/bit-web/src/app/**" - "libs/!(eslint)/**" @@ -74,11 +77,28 @@ jobs: if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true' run: npm run build-storybook:ci + - 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-clients + secrets: "CHROMATIC-PROJECT-TOKEN" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Publish to Chromatic uses: chromaui/action@e8cc4c31775280b175a3c440076c00d19a9014d7 # v11.28.2 with: token: ${{ secrets.GITHUB_TOKEN }} - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + projectToken: ${{ steps.get-kv-secrets.outputs.CHROMATIC-PROJECT-TOKEN }} storybookBuildDir: ./storybook-static exitOnceUploaded: true onlyChanged: true diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 2fc035ec038..0b891203855 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -10,6 +10,9 @@ jobs: crowdin-sync: name: Autosync runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write strategy: fail-fast: false matrix: @@ -21,22 +24,19 @@ jobs: - app_name: web crowdin_project_id: "308189" steps: - - name: Generate GH App token - uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 - id: app-token + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - token: ${{ steps.app-token.outputs.token }} - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + keyvault: gh-org-bitwarden + secrets: "BW-GHAPP-ID,BW-GHAPP-KEY" - name: Retrieve secrets id: retrieve-secrets @@ -45,6 +45,21 @@ jobs: keyvault: "bitwarden-ci" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Generate GH App token + uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 + id: app-token + with: + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} + + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + token: ${{ steps.app-token.outputs.token }} + - name: Download translations uses: bitwarden/gh-actions/crowdin@main env: diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 1cde8dd636a..3ffe18d1729 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -66,8 +66,9 @@ jobs: environment_url: ${{ steps.config.outputs.environment_url }} environment_name: ${{ steps.config.outputs.environment_name }} environment_artifact: ${{ steps.config.outputs.environment_artifact }} - azure_login_creds: ${{ steps.config.outputs.azure_login_creds }} - retrive_secrets_keyvault: ${{ steps.config.outputs.retrive_secrets_keyvault }} + azure_login_client_key_name: ${{ steps.config.outputs.azure_login_client_key_name }} + azure_login_subscription_id_key_name: ${{ steps.config.outputs.azure_login_subscription_id_key_name }} + retrieve_secrets_keyvault: ${{ steps.config.outputs.retrieve_secrets_keyvault }} sync_utility: ${{ steps.config.outputs.sync_utility }} sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }} slack_channel_name: ${{ steps.config.outputs.slack_channel_name }} @@ -81,40 +82,45 @@ jobs: case ${{ inputs.environment }} in "USQA") - echo "azure_login_creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrive_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_USQA" >> $GITHUB_OUTPUT + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USQA" >> $GITHUB_OUTPUT + echo "retrieve_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "EUQA") - echo "azure_login_creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrive_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUQA" >> $GITHUB_OUTPUT + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUQA" >> $GITHUB_OUTPUT + echo "retrieve_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "USPROD") - echo "azure_login_creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrive_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_USPROD" >> $GITHUB_OUTPUT + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USPROD" >> $GITHUB_OUTPUT + echo "retrieve_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "EUPROD") - echo "azure_login_creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrive_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUPROD" >> $GITHUB_OUTPUT + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUPROD" >> $GITHUB_OUTPUT + echo "retrieve_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "USDEV") - echo "azure_login_creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrive_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_USDEV" >> $GITHUB_OUTPUT + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USDEV" >> $GITHUB_OUTPUT + echo "retrieve_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT @@ -180,6 +186,9 @@ jobs: name: Check if Web artifact is present runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read + id-token: write env: _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} outputs: @@ -209,11 +218,13 @@ jobs: branch: ${{ inputs.branch-or-tag }} artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - name: Login to Azure + - name: Log in to Azure if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main 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 for Build trigger if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} @@ -223,6 +234,10 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: bitwarden/gh-actions/azure-logout@main + - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 @@ -277,7 +292,9 @@ jobs: event: 'start' commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} update-summary: name: Display commit @@ -302,6 +319,9 @@ jobs: _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment_url }} _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment_name }} _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} + permissions: + id-token: write + deployments: write steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 @@ -309,23 +329,25 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' - environment_url: ${{ env._ENVIRONMENT_URL }} + environment-url: ${{ env._ENVIRONMENT_URL }} environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' ref: ${{ needs.artifact-check.outputs.artifact_build_commit }} - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets[needs.setup.outputs.azure_login_creds] }} + subscription_id: ${{ secrets[needs.setup.outputs.azure_login_subscription_id_key_name] }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets[needs.setup.outputs.azure_login_client_key_name] }} - name: Retrieve Storage Account connection string for az sync if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} id: retrieve-secrets-az-sync uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} + keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-dev-key-temp" - name: Retrieve Storage Account name and SPN credentials for azcopy @@ -333,9 +355,12 @@ jobs: id: retrieve-secrets-azcopy uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} + keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' if: ${{ inputs.build-web-run-id }} uses: bitwarden/gh-actions/download-artifacts@main @@ -397,7 +422,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment_url: ${{ env._ENVIRONMENT_URL }} + environment-url: ${{ env._ENVIRONMENT_URL }} state: 'success' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -406,7 +431,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment_url: ${{ env._ENVIRONMENT_URL }} + environment-url: ${{ env._ENVIRONMENT_URL }} state: 'failure' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -419,6 +444,8 @@ jobs: - notify-start - azure-deploy - artifact-check + permissions: + id-token: write steps: - name: Notify Slack with result uses: bitwarden/gh-actions/report-deployment-status-to-slack@main @@ -431,4 +458,6 @@ jobs: url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} update-ts: ${{ needs.notify-start.outputs.ts }} - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml index adb5950e3a0..38a3ef59ea7 100644 --- a/.github/workflows/lint-crowdin-config.yml +++ b/.github/workflows/lint-crowdin-config.yml @@ -5,12 +5,14 @@ on: types: [opened, synchronize] paths: - '**/crowdin.yml' -permissions: {} jobs: lint-crowdin-config: name: Lint Crowdin Config ${{ matrix.app.name }} runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write strategy: matrix: app: [ @@ -22,17 +24,25 @@ jobs: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - fetch-depth: 1 - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + fetch-depth: 1 + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" secrets: "crowdin-api-token" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Lint ${{ matrix.app.name }} config uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1 env: @@ -42,4 +52,4 @@ jobs: with: dryrun_action: true command: 'config lint' - command_args: '--verbose -c ${{ matrix.app.config_path }}' \ No newline at end of file + command_args: '--verbose -c ${{ matrix.app.config_path }}' diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index d758e6f11c9..efb0f541d70 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -48,6 +48,10 @@ jobs: defaults: run: working-directory: . + permissions: + contents: read + deployments: write + steps: - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -86,6 +90,10 @@ jobs: name: Deploy Snap runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read + packages: read + id-token: write if: inputs.snap_publish env: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} @@ -93,10 +101,12 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -105,6 +115,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "snapcraft-store-token" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Install Snap uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1 @@ -123,6 +136,10 @@ jobs: name: Deploy Choco runs-on: windows-2022 needs: setup + permissions: + contents: read + packages: read + id-token: write if: inputs.choco_publish env: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} @@ -130,10 +147,12 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -142,6 +161,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "cli-choco-api-key" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Setup Chocolatey run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ env: @@ -163,6 +185,10 @@ jobs: name: Publish NPM runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read + packages: read + id-token: write if: inputs.npm_publish env: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} @@ -170,10 +196,12 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -182,6 +210,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "npm-api-key" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Download and set up artifact run: | mkdir -p build @@ -210,6 +241,10 @@ jobs: - npm - snap - choco + permissions: + contents: read + deployments: write + if: ${{ always() && inputs.publish_type != 'Dry Run' }} steps: - name: Check if any job failed diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index ae631165db9..aafc4d25ed4 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -42,6 +42,9 @@ jobs: release_channel: ${{ steps.release_channel.outputs.channel }} tag_name: ${{ steps.version.outputs.tag_name }} deployment_id: ${{ steps.deployment.outputs.deployment_id }} + permissions: + contents: read + deployments: write steps: - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -106,14 +109,21 @@ jobs: name: Electron blob publish runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read + packages: read + id-token: write + deployments: write env: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -124,6 +134,9 @@ jobs: aws-electron-access-key, aws-electron-bucket-name" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Create artifacts directory run: mkdir -p apps/desktop/artifacts @@ -176,6 +189,9 @@ jobs: name: Deploy Snap runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read + id-token: write if: inputs.snap_publish env: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} @@ -184,10 +200,12 @@ jobs: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -196,6 +214,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "snapcraft-store-token" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Install Snap uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1 @@ -220,6 +241,9 @@ jobs: name: Deploy Choco runs-on: windows-2022 needs: setup + permissions: + contents: read + id-token: write if: inputs.choco_publish env: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} @@ -233,10 +257,12 @@ jobs: dotnet --version dotnet nuget --version - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -245,6 +271,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "cli-choco-api-key" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Setup Chocolatey run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ env: @@ -271,6 +300,9 @@ jobs: - electron-blob - snap - choco + permissions: + contents: read + deployments: write if: ${{ always() && inputs.publish_type != 'Dry Run' }} steps: - name: Check if any job failed diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 69b29086d36..a6f0f1be066 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -24,6 +24,8 @@ jobs: outputs: release_version: ${{ steps.version.outputs.version }} tag_version: ${{ steps.version.outputs.tag }} + permissions: + contents: read steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -52,6 +54,10 @@ jobs: name: Release self-host docker runs-on: ubuntu-22.04 needs: setup + permissions: + id-token: write + contents: read + deployments: write env: _BRANCH_NAME: ${{ github.ref_name }} _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} @@ -69,10 +75,12 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 ########## ACR ########## - - name: Login to Azure - PROD Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Login to Azure ACR run: az acr login -n bitwardenprod @@ -121,6 +129,9 @@ jobs: docker push $_AZ_REGISTRY/web-sh:latest fi + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Update deployment status to Success if: ${{ inputs.publish_type != 'Dry Run' && success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 @@ -147,11 +158,15 @@ jobs: runs-on: ubuntu-22.04 needs: - setup + permissions: + id-token: write steps: - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 GitHub PAT secrets id: retrieve-secret-pat @@ -160,6 +175,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Trigger self-host build uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index a5e374395d8..e3eb9090cb7 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -15,6 +15,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + permissions: + contents: write outputs: release_version: ${{ steps.version.outputs.version }} release_channel: ${{ steps.release_channel.outputs.channel }} @@ -115,6 +117,8 @@ jobs: name: Linux Build runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -204,6 +208,9 @@ jobs: name: Windows Build runs-on: windows-2022 needs: setup + permissions: + contents: read + id-token: write defaults: run: shell: pwsh @@ -237,10 +244,12 @@ jobs: npm --version choco --version - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -253,6 +262,9 @@ jobs: code-signing-client-secret, code-signing-cert-name" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Install Node dependencies run: npm ci working-directory: ./ @@ -394,6 +406,9 @@ jobs: name: MacOS Build runs-on: macos-13 needs: setup + permissions: + contents: read + id-token: write env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -438,6 +453,20 @@ jobs: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension + - 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-clients + secrets: "KEYCHAIN-PASSWORD" + - name: Download Provisioning Profiles secrets env: ACCOUNT_NAME: bitwardenci @@ -472,9 +501,12 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | 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 env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -528,6 +560,10 @@ jobs: needs: - setup - macos-build + permissions: + contents: read + packages: read + id-token: write env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -572,10 +608,19 @@ jobs: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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-clients + secrets: "KEYCHAIN-PASSWORD,APPLE-ID-USERNAME,APPLE-ID-PASSWORD" - name: Download Provisioning Profiles secrets env: @@ -611,9 +656,12 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | 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 env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -702,8 +750,8 @@ jobs: - name: Build application (dist) env: - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_ID_USERNAME: ${{ steps.get-kv-secrets.outputs.APPLE-ID-USERNAME }} + APPLE_ID_PASSWORD: ${{ steps.get-kv-secrets.outputs.APPLE-ID-PASSWORD }} run: npm run pack:mac - name: Upload .zip artifact @@ -741,6 +789,10 @@ jobs: needs: - setup - macos-build + permissions: + contents: read + packages: read + id-token: write env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -785,6 +837,20 @@ jobs: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension + - 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-clients + secrets: "KEYCHAIN-PASSWORD,APPLE-ID-USERNAME,APPLE-ID-PASSWORD" + - name: Download Provisioning Profiles secrets env: ACCOUNT_NAME: bitwardenci @@ -819,9 +885,12 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | 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 env: - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security default-keychain -s build.keychain @@ -911,8 +980,8 @@ jobs: - name: Build application for App Store run: npm run pack:mac:mas env: - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_ID_USERNAME: ${{ steps.get-kv-secrets.outputs.APPLE-ID-USERNAME }} + APPLE_ID_PASSWORD: ${{ steps.get-kv-secrets.outputs.APPLE-ID-PASSWORD }} - name: Upload .pkg artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 @@ -931,6 +1000,10 @@ jobs: - macos-build - macos-package-github - macos-package-mas + permissions: + contents: read + id-token: write + deployments: write steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 @@ -942,10 +1015,12 @@ jobs: description: 'Deployment ${{ needs.setup.outputs.release_version }} to channel ${{ needs.setup.outputs.release_channel }} from branch ${{ needs.setup.outputs.branch_name }}' task: release - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -956,6 +1031,9 @@ jobs: aws-electron-access-key, aws-electron-bucket-name" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Download all artifacts uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: @@ -1008,6 +1086,8 @@ jobs: - macos-package-github - macos-package-mas - release + permissions: + contents: write steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index d91e0a12afd..ecb8e448a8a 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -36,7 +36,9 @@ on: description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" required: false type: string + permissions: {} + jobs: setup: name: Setup @@ -56,6 +58,7 @@ jobs: fi echo "branch=$BRANCH" >> $GITHUB_OUTPUT + bump_version: name: Bump Version if: ${{ always() }} @@ -66,6 +69,9 @@ jobs: version_cli: ${{ steps.set-final-version-output.outputs.version_cli }} version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }} version_web: ${{ steps.set-final-version-output.outputs.version_web }} + permissions: + id-token: write + steps: - name: Validate version input format if: ${{ inputs.version_number_override != '' }} @@ -73,12 +79,29 @@ jobs: with: 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 uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 id: app-token with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -400,6 +423,7 @@ jobs: - name: Push changes if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} run: git push + cut_branch: name: Cut branch if: ${{ needs.setup.outputs.branch == 'rc' }} @@ -407,13 +431,33 @@ jobs: - setup - bump_version runs-on: ubuntu-24.04 + permissions: + id-token: write + steps: + - 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 uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 id: app-token with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -435,4 +479,4 @@ jobs: BRANCH_NAME: ${{ needs.setup.outputs.branch }} run: | git switch --quiet --create $BRANCH_NAME - git push --quiet --set-upstream origin $BRANCH_NAME \ No newline at end of file + git push --quiet --set-upstream origin $BRANCH_NAME diff --git a/.github/workflows/retrieve-current-desktop-rollout.yml b/.github/workflows/retrieve-current-desktop-rollout.yml index 2ab3072f566..c45453ed9d0 100644 --- a/.github/workflows/retrieve-current-desktop-rollout.yml +++ b/.github/workflows/retrieve-current-desktop-rollout.yml @@ -11,11 +11,15 @@ jobs: rollout: name: Retrieve Rollout Percentage runs-on: ubuntu-22.04 + permissions: + id-token: write steps: - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -26,6 +30,9 @@ jobs: aws-electron-access-key, aws-electron-bucket-name" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Download channel update info files from S3 env: AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index 4ec3af3be97..4adf81100bd 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -18,11 +18,15 @@ jobs: rollout: name: Update Rollout Percentage runs-on: ubuntu-22.04 + permissions: + id-token: write steps: - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main 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 id: retrieve-secrets @@ -33,6 +37,9 @@ jobs: aws-electron-access-key, aws-electron-bucket-name" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Download channel update info files from S3 env: AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index e8bd1dde246..3cb5646886a 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -9,13 +9,33 @@ jobs: bump-version: name: Bump Desktop Version runs-on: ubuntu-24.04 + permissions: + id-token: write + contents: write steps: + - 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 uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 id: app-token with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From 1f20bcecf0b0154ed45c889625e734069113a643 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:06:02 -0500 Subject: [PATCH 002/179] Hide bank account for premium and when non-premium selects non-US country (#15707) --- .../change-payment-method-dialog.component.ts | 6 +++- .../enter-payment-method.component.ts | 31 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts index efd0055fb95..ff5156ba636 100644 --- a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts @@ -28,7 +28,11 @@ type DialogResult = {{ "changePaymentMethod" | i18n }}
- +
diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts index 4f5b2e3b15c..b73c3297e9e 100644 --- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { BehaviorSubject, startWith, Subject, takeUntil } from "rxjs"; +import { map, Observable, of, startWith, Subject, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -48,7 +48,7 @@ type PaymentMethodFormGroup = FormGroup<{ {{ "creditCard" | i18n }} - @if (showBankAccount) { + @if (showBankAccount$ | async) { @@ -226,20 +226,12 @@ type PaymentMethodFormGroup = FormGroup<{ export class EnterPaymentMethodComponent implements OnInit { @Input({ required: true }) group!: PaymentMethodFormGroup; - private showBankAccountSubject = new BehaviorSubject(true); - showBankAccount$ = this.showBankAccountSubject.asObservable(); - @Input() - set showBankAccount(value: boolean) { - this.showBankAccountSubject.next(value); - } - get showBankAccount(): boolean { - return this.showBankAccountSubject.value; - } - - @Input() showPayPal: boolean = true; - @Input() showAccountCredit: boolean = false; - @Input() includeBillingAddress: boolean = false; + @Input() private showBankAccount = true; + @Input() showPayPal = true; + @Input() showAccountCredit = false; + @Input() includeBillingAddress = false; + protected showBankAccount$!: Observable; protected selectableCountries = selectableCountries; private destroy$ = new Subject(); @@ -267,7 +259,16 @@ export class EnterPaymentMethodComponent implements OnInit { } if (!this.includeBillingAddress) { + this.showBankAccount$ = of(this.showBankAccount); this.group.controls.billingAddress.disable(); + } else { + this.group.controls.billingAddress.patchValue({ + country: "US", + }); + this.showBankAccount$ = this.group.controls.billingAddress.controls.country.valueChanges.pipe( + startWith(this.group.controls.billingAddress.controls.country.value), + map((country) => this.showBankAccount && country === "US"), + ); } this.group.controls.type.valueChanges From 77940116e6818ef5eb4eb8247aab6e75574bf0a3 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Mon, 21 Jul 2025 16:15:39 -0400 Subject: [PATCH 003/179] DDG integration files modified workflow (#15665) * Create alert-ddg-files-modified.yml * Update alert-ddg-files-modified.yml * Add encrypted-message-handler.service to alert-ddg-files-modified.yml * Pin action versions * Add permissions * Update alert-ddg-files-modified.yml * Update alert-ddg-files-modified.yml * Add parameter to get list of files changed * Wording update * Update CODEOWNERS * Make branch target main --- .github/CODEOWNERS | 2 + .../workflows/alert-ddg-files-modified.yml | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 .github/workflows/alert-ddg-files-modified.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ef2e26916e5..7d7fec2a5ea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -138,6 +138,8 @@ apps/desktop/desktop_native/autotype @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev +apps/desktop/src/services/encrypted-message-handler.service.ts @bitwarden/team-autofill-dev +.github/workflows/alert-ddg-files-modified.yml @bitwarden/team-autofill-dev # SSH Agent apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bitwarden/wg-ssh-keys diff --git a/.github/workflows/alert-ddg-files-modified.yml b/.github/workflows/alert-ddg-files-modified.yml new file mode 100644 index 00000000000..61bb7f1e8af --- /dev/null +++ b/.github/workflows/alert-ddg-files-modified.yml @@ -0,0 +1,50 @@ +name: DDG File Change Monitor + +on: + pull_request: + branches: [ main ] + types: [ opened, synchronize ] + +jobs: + check-files: + name: Check files + runs-on: ubuntu-22.04 + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + list-files: shell + filters: | + monitored: + - 'apps/desktop/native-messaging-test-runner/**' + - 'apps/desktop/src/services/duckduckgo-message-handler.service.ts' + - 'apps/desktop/src/services/encrypted-message-handler.service.ts' + + - name: Comment on PR if monitored files changed + if: steps.changed-files.outputs.monitored == 'true' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const changedFiles = `${{ steps.changed-files.outputs.monitored_files }}`.split(' ').filter(file => file.trim() !== ''); + + const message = `⚠️🦆 **DuckDuckGo Integration files have been modified in this PR:** + + ${changedFiles.map(file => `- \`${file}\``).join('\n')} + + Please run the DuckDuckGo native messaging test runner from this branch using [these instructions](https://contributing.bitwarden.com/getting-started/clients/desktop/native-messaging-test-runner) and ensure it functions properly.`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); From 81ee26733e6040a8d32afda67b5c3db3d8d094e0 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:17:39 -0400 Subject: [PATCH 004/179] [BRE-831] Fixing permissions (#15713) --- .github/workflows/deploy-web.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 3ffe18d1729..e21f7ae1e79 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -277,6 +277,8 @@ jobs: - artifact-check runs-on: ubuntu-22.04 if: ${{ always() && ( contains( inputs.environment , 'QA' ) || contains( inputs.environment , 'DEV' ) ) }} + permissions: + id-token: write outputs: channel_id: ${{ steps.slack-message.outputs.channel_id }} ts: ${{ steps.slack-message.outputs.ts }} From 391f540d1f488354002090823c3054e960b28a15 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 21 Jul 2025 23:27:01 -0700 Subject: [PATCH 005/179] [PM-22136] Implement SDK cipher encryption (#15337) * [PM-22136] Update sdk cipher view map to support uknown uuid type * [PM-22136] Add key to CipherView for copying to SdkCipherView for encryption * [PM-22136] Add fromSdk* helpers to Cipher domain objects * [PM-22136] Add toSdk* helpers to Cipher View objects * [PM-22136] Add encrypt() to cipher encryption service * [PM-22136] Add feature flag * [PM-22136] Use new SDK encrypt method when feature flag is enabled * [PM-22136] Filter out null/empty URIs * [PM-22136] Change default value for cipher view arrays to []. See ADR-0014. * [PM-22136] Keep encrypted key value on attachment so that it is passed to the SDK * [PM-22136] Keep encrypted key value on CipherView so that it is passed to the SDK during encryption * [PM-22136] Update failing attachment test * [PM-22136] Update failing importer tests due to new default value for arrays * [PM-22136] Update CipherView.fromJson to handle the prototype of EncString for the cipher key * [PM-22136] Add tickets for followup work * [PM-22136] Use new set_fido2_credentials SDK method instead * [PM-22136] Fix missing prototype when decrypting Fido2Credentials * [PM-22136] Fix test after sdk change * [PM-22136] Update @bitwarden/sdk-internal version * [PM-22136] Fix some strict typing errors * [PM-23348] Migrate move cipher to org to SDK (#15567) * [PM-23348] Add moveToOrganization method to cipher-encryption.service.ts * [PM-23348] Use cipherEncryptionService.moveToOrganization in cipherService shareWithServer and shareManyWithServer methods * [PM-23348] Update cipherFormService to use the shareWithServer() method instead of encrypt() * [PM-23348] Fix typo * [PM-23348] Add missing docs * [PM-22136] Fix EncString import after merge with main --- libs/common/src/enums/feature-flag.enum.ts | 2 + .../abstractions/cipher-encryption.service.ts | 25 +- .../src/vault/abstractions/cipher.service.ts | 10 + .../models/api/cipher-permissions.api.ts | 9 +- .../src/vault/models/data/local.data.ts | 41 +++ .../vault/models/domain/attachment.spec.ts | 1 + .../src/vault/models/domain/attachment.ts | 21 ++ libs/common/src/vault/models/domain/card.ts | 20 ++ .../src/vault/models/domain/cipher.spec.ts | 188 +++++++++++++- libs/common/src/vault/models/domain/cipher.ts | 60 ++++- .../vault/models/domain/fido2-credential.ts | 28 ++ .../src/vault/models/domain/field.spec.ts | 39 ++- libs/common/src/vault/models/domain/field.ts | 18 ++ .../src/vault/models/domain/identity.ts | 32 +++ .../src/vault/models/domain/login-uri.ts | 13 + libs/common/src/vault/models/domain/login.ts | 27 ++ .../src/vault/models/domain/password.ts | 16 ++ .../src/vault/models/domain/secure-note.ts | 15 ++ .../common/src/vault/models/domain/ssh-key.ts | 17 ++ .../common/src/vault/models/view/card.view.ts | 10 +- .../src/vault/models/view/cipher.view.spec.ts | 94 ++++++- .../src/vault/models/view/cipher.view.ts | 116 +++++++-- .../models/view/fido2-credential.view.ts | 23 +- .../src/vault/models/view/field.view.ts | 14 +- .../src/vault/models/view/identity.view.ts | 10 +- .../src/vault/models/view/login-uri.view.ts | 9 + .../src/vault/models/view/login.view.ts | 22 +- .../models/view/password-history.view.spec.ts | 13 + .../models/view/password-history.view.ts | 10 + .../src/vault/models/view/secure-note.view.ts | 10 +- .../src/vault/models/view/ssh-key.view.ts | 11 + .../src/vault/services/cipher.service.spec.ts | 167 +++++++++++- .../src/vault/services/cipher.service.ts | 122 +++++++-- .../default-cipher-encryption.service.spec.ts | 245 ++++++++++++++++-- .../default-cipher-encryption.service.ts | 106 +++++++- libs/importer/src/importers/base-importer.ts | 6 - .../keeper/keeper-csv-importer.spec.ts | 2 +- .../keeper/keeper-json-importer.spec.ts | 2 +- .../services/default-cipher-form.service.ts | 35 ++- .../cipher-view/cipher-view.component.html | 4 +- .../password-history-view.component.spec.ts | 11 +- package-lock.json | 8 +- package.json | 2 +- 43 files changed, 1485 insertions(+), 149 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 1af2ab1f0a9..8e9dc6fd35e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -54,6 +54,7 @@ export enum FeatureFlag { PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk", PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view", + PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption", CipherKeyEncryption = "cipher-key-encryption", EndUserNotifications = "pm-10609-end-user-notifications", RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy", @@ -103,6 +104,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.RemoveCardItemTypePolicy]: FALSE, [FeatureFlag.PM22134SdkCipherListView]: FALSE, [FeatureFlag.PM19315EndUserActivationMvp]: FALSE, + [FeatureFlag.PM22136_SdkCipherEncryption]: FALSE, /* Auth */ [FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE, diff --git a/libs/common/src/vault/abstractions/cipher-encryption.service.ts b/libs/common/src/vault/abstractions/cipher-encryption.service.ts index 6b2a8e8943e..067c63b2110 100644 --- a/libs/common/src/vault/abstractions/cipher-encryption.service.ts +++ b/libs/common/src/vault/abstractions/cipher-encryption.service.ts @@ -1,6 +1,7 @@ +import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherListView } from "@bitwarden/sdk-internal"; -import { UserId } from "../../types/guid"; +import { UserId, OrganizationId } from "../../types/guid"; import { Cipher } from "../models/domain/cipher"; import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; @@ -9,6 +10,28 @@ import { CipherView } from "../models/view/cipher.view"; * Service responsible for encrypting and decrypting ciphers. */ export abstract class CipherEncryptionService { + /** + * Encrypts a cipher using the SDK for the given userId. + * @param model The cipher view to encrypt + * @param userId The user ID to initialize the SDK client with + * + * @returns A promise that resolves to the encryption context, or undefined if encryption fails + */ + abstract encrypt(model: CipherView, userId: UserId): Promise; + + /** + * Move the cipher to the specified organization by re-encrypting its keys with the organization's key. + * The cipher.organizationId will be updated to the new organizationId. + * @param model The cipher view to move to the organization + * @param organizationId The ID of the organization to move the cipher to + * @param userId The user ID to initialize the SDK client with + */ + abstract moveToOrganization( + model: CipherView, + organizationId: OrganizationId, + userId: UserId, + ): Promise; + /** * Decrypts a cipher using the SDK for the given userId. * diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index d1d686a66af..2f186369463 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -120,11 +120,21 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + + /** + * Move a cipher to an organization by re-encrypting its keys with the organization's key. + * @param cipher The cipher to move + * @param organizationId The Id of the organization to move the cipher to + * @param collectionIds The collection Ids to assign the cipher to in the organization + * @param userId The Id of the user performing the operation + * @param originalCipher Optional original cipher that will be used to compare/update password history + */ abstract shareWithServer( cipher: CipherView, organizationId: string, collectionIds: string[], userId: UserId, + originalCipher?: Cipher, ): Promise; abstract shareManyWithServer( ciphers: CipherView[], diff --git a/libs/common/src/vault/models/api/cipher-permissions.api.ts b/libs/common/src/vault/models/api/cipher-permissions.api.ts index b7341d39b1d..f9b62c4fc8d 100644 --- a/libs/common/src/vault/models/api/cipher-permissions.api.ts +++ b/libs/common/src/vault/models/api/cipher-permissions.api.ts @@ -4,7 +4,7 @@ import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-intern import { BaseResponse } from "../../../models/response/base.response"; -export class CipherPermissionsApi extends BaseResponse { +export class CipherPermissionsApi extends BaseResponse implements SdkCipherPermissions { delete: boolean = false; restore: boolean = false; @@ -35,4 +35,11 @@ export class CipherPermissionsApi extends BaseResponse { return permissions; } + + /** + * Converts the CipherPermissionsApi to an SdkCipherPermissions + */ + toSdkCipherPermissions(): SdkCipherPermissions { + return this; + } } diff --git a/libs/common/src/vault/models/data/local.data.ts b/libs/common/src/vault/models/data/local.data.ts index 9ba820a58a2..50a24feba6f 100644 --- a/libs/common/src/vault/models/data/local.data.ts +++ b/libs/common/src/vault/models/data/local.data.ts @@ -1,4 +1,45 @@ +import { + LocalDataView as SdkLocalDataView, + LocalData as SdkLocalData, +} from "@bitwarden/sdk-internal"; + export type LocalData = { lastUsedDate?: number; lastLaunched?: number; }; + +/** + * Convert the SdkLocalDataView to LocalData + * @param localData + */ +export function fromSdkLocalData( + localData: SdkLocalDataView | SdkLocalData | undefined, +): LocalData | undefined { + if (localData == null) { + return undefined; + } + return { + lastUsedDate: localData.lastUsedDate ? new Date(localData.lastUsedDate).getTime() : undefined, + lastLaunched: localData.lastLaunched ? new Date(localData.lastLaunched).getTime() : undefined, + }; +} + +/** + * Convert the LocalData to SdkLocalData + * @param localData + */ +export function toSdkLocalData( + localData: LocalData | undefined, +): (SdkLocalDataView & SdkLocalData) | undefined { + if (localData == null) { + return undefined; + } + return { + lastUsedDate: localData.lastUsedDate + ? new Date(localData.lastUsedDate).toISOString() + : undefined, + lastLaunched: localData.lastLaunched + ? new Date(localData.lastLaunched).toISOString() + : undefined, + }; +} diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index d2b536b0590..2ea2c3d9a1d 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -93,6 +93,7 @@ describe("Attachment", () => { sizeName: "1.1 KB", fileName: "fileName", key: expect.any(SymmetricCryptoKey), + encryptedKey: attachment.key, }); }); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index abfebffb2e6..638f354c4b8 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -56,6 +56,7 @@ export class Attachment extends Domain { if (this.key != null) { view.key = await this.decryptAttachmentKey(orgId, encKey); + view.encryptedKey = this.key; // Keep the encrypted key for the view } return view; @@ -131,4 +132,24 @@ export class Attachment extends Domain { key: this.key?.toJSON(), }; } + + /** + * Maps an SDK Attachment object to an Attachment + * @param obj - The SDK attachment object + */ + static fromSdkAttachment(obj: SdkAttachment): Attachment | undefined { + if (!obj) { + return undefined; + } + + const attachment = new Attachment(); + attachment.id = obj.id; + attachment.url = obj.url; + attachment.size = obj.size; + attachment.sizeName = obj.sizeName; + attachment.fileName = EncString.fromJSON(obj.fileName); + attachment.key = EncString.fromJSON(obj.key); + + return attachment; + } } diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index c78f9dfb719..688053ae93c 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -103,4 +103,24 @@ export class Card extends Domain { code: this.code?.toJSON(), }; } + + /** + * Maps an SDK Card object to a Card + * @param obj - The SDK Card object + */ + static fromSdkCard(obj: SdkCard): Card | undefined { + if (obj == null) { + return undefined; + } + + const card = new Card(); + card.cardholderName = EncString.fromJSON(obj.cardholderName); + card.brand = EncString.fromJSON(obj.brand); + card.number = EncString.fromJSON(obj.number); + card.expMonth = EncString.fromJSON(obj.expMonth); + card.expYear = EncString.fromJSON(obj.expYear); + card.code = EncString.fromJSON(obj.code); + + return card; + } } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 3ea8916a10b..60fff8b510e 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -10,6 +10,7 @@ import { UriMatchType, CipherRepromptType as SdkCipherRepromptType, LoginLinkedIdType, + Cipher as SdkCipher, } from "@bitwarden/sdk-internal"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; @@ -206,7 +207,7 @@ describe("Cipher DTO", () => { it("Convert", () => { const cipher = new Cipher(cipherData); - expect(cipher).toEqual({ + expect(cipher).toMatchObject({ initializerKey: InitializerKey.Cipher, id: "id", organizationId: "orgId", @@ -339,9 +340,9 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, login: loginView, - attachments: null, - fields: null, - passwordHistory: null, + attachments: [], + fields: [], + passwordHistory: [], collectionIds: undefined, revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), @@ -462,9 +463,9 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, secureNote: { type: 0 }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: [], + fields: [], + passwordHistory: [], collectionIds: undefined, revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), @@ -603,9 +604,9 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, card: cardView, - attachments: null, - fields: null, - passwordHistory: null, + attachments: [], + fields: [], + passwordHistory: [], collectionIds: undefined, revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), @@ -768,9 +769,9 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, identity: identityView, - attachments: null, - fields: null, - passwordHistory: null, + attachments: [], + fields: [], + passwordHistory: [], collectionIds: undefined, revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), @@ -1001,6 +1002,167 @@ describe("Cipher DTO", () => { revisionDate: "2022-01-31T12:00:00.000Z", }); }); + + it("should map from SDK Cipher", () => { + jest.restoreAllMocks(); + const sdkCipher: SdkCipher = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + collectionIds: [], + key: "EncryptedString", + name: "EncryptedString", + notes: "EncryptedString", + type: SdkCipherType.Login, + login: { + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + uris: [ + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchType.Domain, + }, + ], + totp: "EncryptedString", + autofillOnPageLoad: false, + fido2Credentials: undefined, + }, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + favorite: false, + reprompt: SdkCipherRepromptType.None, + organizationUseTotp: true, + edit: true, + permissions: new CipherPermissionsApi(), + viewPassword: true, + localData: { + lastUsedDate: "2025-04-15T12:00:00.000Z", + lastLaunched: "2025-04-15T12:00:00.000Z", + }, + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], + fields: [ + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Username, + }, + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Password, + }, + ], + passwordHistory: [ + { + password: "EncryptedString", + lastUsedDate: "2022-01-31T12:00:00.000Z", + }, + ], + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: undefined, + revisionDate: "2022-01-31T12:00:00.000Z", + }; + + const lastUsedDate = new Date("2025-04-15T12:00:00.000Z").getTime(); + const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime(); + + const cipherData: CipherData = { + id: "id", + organizationId: "orgId", + folderId: "folderId", + edit: true, + permissions: new CipherPermissionsApi(), + collectionIds: [], + viewPassword: true, + organizationUseTotp: true, + favorite: false, + revisionDate: "2022-01-31T12:00:00.000Z", + type: CipherType.Login, + name: "EncryptedString", + notes: "EncryptedString", + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + reprompt: CipherRepromptType.None, + key: "EncryptedString", + login: { + uris: [ + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchStrategy.Domain, + }, + ], + username: "EncryptedString", + password: "EncryptedString", + passwordRevisionDate: "2022-01-31T12:00:00.000Z", + totp: "EncryptedString", + autofillOnPageLoad: false, + }, + passwordHistory: [ + { password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }, + ], + attachments: [ + { + id: "a1", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + { + id: "a2", + url: "url", + size: "1100", + sizeName: "1.1 KB", + fileName: "file", + key: "EncKey", + }, + ], + fields: [ + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedId.Username, + }, + { + name: "EncryptedString", + value: "EncryptedString", + type: FieldType.Linked, + linkedId: LoginLinkedId.Password, + }, + ], + }; + const expectedCipher = new Cipher(cipherData, { lastUsedDate, lastLaunched }); + + const cipher = Cipher.fromSdkCipher(sdkCipher); + + expect(cipher).toEqual(expectedCipher); + }); }); }); diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 2f64fb82726..2a13cb06d71 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; +import { uuidToString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; @@ -14,7 +15,7 @@ import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherPermissionsApi } from "../api/cipher-permissions.api"; import { CipherData } from "../data/cipher.data"; -import { LocalData } from "../data/local.data"; +import { LocalData, fromSdkLocalData, toSdkLocalData } from "../data/local.data"; import { AttachmentView } from "../view/attachment.view"; import { CipherView } from "../view/cipher.view"; import { FieldView } from "../view/field.view"; @@ -361,16 +362,7 @@ export class Cipher extends Domain implements Decryptable { } : undefined, viewPassword: this.viewPassword ?? true, - localData: this.localData - ? { - lastUsedDate: this.localData.lastUsedDate - ? new Date(this.localData.lastUsedDate).toISOString() - : undefined, - lastLaunched: this.localData.lastLaunched - ? new Date(this.localData.lastLaunched).toISOString() - : undefined, - } - : undefined, + localData: toSdkLocalData(this.localData), attachments: this.attachments?.map((a) => a.toSdkAttachment()), fields: this.fields?.map((f) => f.toSdkField()), passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()), @@ -408,4 +400,50 @@ export class Cipher extends Domain implements Decryptable { return sdkCipher; } + + /** + * Maps an SDK Cipher object to a Cipher + * @param sdkCipher - The SDK Cipher object + */ + static fromSdkCipher(sdkCipher: SdkCipher | null): Cipher | undefined { + if (sdkCipher == null) { + return undefined; + } + + const cipher = new Cipher(); + + cipher.id = sdkCipher.id ? uuidToString(sdkCipher.id) : undefined; + cipher.organizationId = sdkCipher.organizationId + ? uuidToString(sdkCipher.organizationId) + : undefined; + cipher.folderId = sdkCipher.folderId ? uuidToString(sdkCipher.folderId) : undefined; + cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidToString) : []; + cipher.key = EncString.fromJSON(sdkCipher.key); + cipher.name = EncString.fromJSON(sdkCipher.name); + cipher.notes = EncString.fromJSON(sdkCipher.notes); + cipher.type = sdkCipher.type; + cipher.favorite = sdkCipher.favorite; + cipher.organizationUseTotp = sdkCipher.organizationUseTotp; + cipher.edit = sdkCipher.edit; + cipher.permissions = CipherPermissionsApi.fromSdkCipherPermissions(sdkCipher.permissions); + cipher.viewPassword = sdkCipher.viewPassword; + cipher.localData = fromSdkLocalData(sdkCipher.localData); + cipher.attachments = sdkCipher.attachments?.map((a) => Attachment.fromSdkAttachment(a)) ?? []; + cipher.fields = sdkCipher.fields?.map((f) => Field.fromSdkField(f)) ?? []; + cipher.passwordHistory = + sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? []; + cipher.creationDate = new Date(sdkCipher.creationDate); + cipher.revisionDate = new Date(sdkCipher.revisionDate); + cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : null; + cipher.reprompt = sdkCipher.reprompt; + + // Cipher type specific properties + cipher.login = Login.fromSdkLogin(sdkCipher.login); + cipher.secureNote = SecureNote.fromSdkSecureNote(sdkCipher.secureNote); + cipher.card = Card.fromSdkCard(sdkCipher.card); + cipher.identity = Identity.fromSdkIdentity(sdkCipher.identity); + cipher.sshKey = SshKey.fromSdkSshKey(sdkCipher.sshKey); + + return cipher; + } } diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index 508f8a6d5fb..5dbf55b44fc 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -173,4 +173,32 @@ export class Fido2Credential extends Domain { creationDate: this.creationDate.toISOString(), }; } + + /** + * Maps an SDK Fido2Credential object to a Fido2Credential + * @param obj - The SDK Fido2Credential object + */ + static fromSdkFido2Credential(obj: SdkFido2Credential): Fido2Credential | undefined { + if (!obj) { + return undefined; + } + + const credential = new Fido2Credential(); + + credential.credentialId = EncString.fromJSON(obj.credentialId); + credential.keyType = EncString.fromJSON(obj.keyType); + credential.keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm); + credential.keyCurve = EncString.fromJSON(obj.keyCurve); + credential.keyValue = EncString.fromJSON(obj.keyValue); + credential.rpId = EncString.fromJSON(obj.rpId); + credential.userHandle = EncString.fromJSON(obj.userHandle); + credential.userName = EncString.fromJSON(obj.userName); + credential.counter = EncString.fromJSON(obj.counter); + credential.rpName = EncString.fromJSON(obj.rpName); + credential.userDisplayName = EncString.fromJSON(obj.userDisplayName); + credential.discoverable = EncString.fromJSON(obj.discoverable); + credential.creationDate = new Date(obj.creationDate); + + return credential; + } } diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts index 08bc0da84fe..b5e26199e7d 100644 --- a/libs/common/src/vault/models/domain/field.spec.ts +++ b/libs/common/src/vault/models/domain/field.spec.ts @@ -1,6 +1,14 @@ +import { + Field as SdkField, + FieldType, + LoginLinkedIdType, + CardLinkedIdType, + IdentityLinkedIdType, +} from "@bitwarden/sdk-internal"; + import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; -import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums"; +import { CardLinkedId, IdentityLinkedId, LoginLinkedId } from "../../enums"; import { FieldData } from "../../models/data/field.data"; import { Field } from "../../models/domain/field"; @@ -103,5 +111,34 @@ describe("Field", () => { identityField.linkedId = IdentityLinkedId.LicenseNumber; expect(identityField.toSdkField().linkedId).toBe(415); }); + + it("should map from SDK Field", () => { + // Test Login LinkedId + const loginField: SdkField = { + name: undefined, + value: undefined, + type: FieldType.Linked, + linkedId: LoginLinkedIdType.Username, + }; + expect(Field.fromSdkField(loginField)!.linkedId).toBe(100); + + // Test Card LinkedId + const cardField: SdkField = { + name: undefined, + value: undefined, + type: FieldType.Linked, + linkedId: CardLinkedIdType.Number, + }; + expect(Field.fromSdkField(cardField)!.linkedId).toBe(305); + + // Test Identity LinkedId + const identityFieldSdkField: SdkField = { + name: undefined, + value: undefined, + type: FieldType.Linked, + linkedId: IdentityLinkedIdType.LicenseNumber, + }; + expect(Field.fromSdkField(identityFieldSdkField)!.linkedId).toBe(415); + }); }); }); diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index d6453932cc7..53756e21046 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -90,4 +90,22 @@ export class Field extends Domain { linkedId: this.linkedId as unknown as SdkLinkedIdType, }; } + + /** + * Maps SDK Field to Field + * @param obj The SDK Field object to map + */ + static fromSdkField(obj: SdkField): Field | undefined { + if (!obj) { + return undefined; + } + + const field = new Field(); + field.name = EncString.fromJSON(obj.name); + field.value = EncString.fromJSON(obj.value); + field.type = obj.type; + field.linkedId = obj.linkedId; + + return field; + } } diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index 5dc752531be..16e68c72551 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -195,4 +195,36 @@ export class Identity extends Domain { licenseNumber: this.licenseNumber?.toJSON(), }; } + + /** + * Maps an SDK Identity object to an Identity + * @param obj - The SDK Identity object + */ + static fromSdkIdentity(obj: SdkIdentity): Identity | undefined { + if (obj == null) { + return undefined; + } + + const identity = new Identity(); + identity.title = EncString.fromJSON(obj.title); + identity.firstName = EncString.fromJSON(obj.firstName); + identity.middleName = EncString.fromJSON(obj.middleName); + identity.lastName = EncString.fromJSON(obj.lastName); + identity.address1 = EncString.fromJSON(obj.address1); + identity.address2 = EncString.fromJSON(obj.address2); + identity.address3 = EncString.fromJSON(obj.address3); + identity.city = EncString.fromJSON(obj.city); + identity.state = EncString.fromJSON(obj.state); + identity.postalCode = EncString.fromJSON(obj.postalCode); + identity.country = EncString.fromJSON(obj.country); + identity.company = EncString.fromJSON(obj.company); + identity.email = EncString.fromJSON(obj.email); + identity.phone = EncString.fromJSON(obj.phone); + identity.ssn = EncString.fromJSON(obj.ssn); + identity.username = EncString.fromJSON(obj.username); + identity.passportNumber = EncString.fromJSON(obj.passportNumber); + identity.licenseNumber = EncString.fromJSON(obj.licenseNumber); + + return identity; + } } diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index e7bc2e8892e..9cfa4951dd8 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -102,4 +102,17 @@ export class LoginUri extends Domain { match: this.match, }; } + + static fromSdkLoginUri(obj: SdkLoginUri): LoginUri | undefined { + if (obj == null) { + return undefined; + } + + const view = new LoginUri(); + view.uri = EncString.fromJSON(obj.uri); + view.uriChecksum = obj.uriChecksum ? EncString.fromJSON(obj.uriChecksum) : undefined; + view.match = obj.match; + + return view; + } } diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index 4d77983d4af..93af2269185 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -163,4 +163,31 @@ export class Login extends Domain { fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()), }; } + + /** + * Maps an SDK Login object to a Login + * @param obj - The SDK Login object + */ + static fromSdkLogin(obj: SdkLogin): Login | undefined { + if (!obj) { + return undefined; + } + + const login = new Login(); + + login.uris = + obj.uris?.filter((u) => u.uri != null).map((uri) => LoginUri.fromSdkLoginUri(uri)) ?? []; + login.username = EncString.fromJSON(obj.username); + login.password = EncString.fromJSON(obj.password); + login.passwordRevisionDate = obj.passwordRevisionDate + ? new Date(obj.passwordRevisionDate) + : undefined; + login.totp = EncString.fromJSON(obj.totp); + login.autofillOnPageLoad = obj.autofillOnPageLoad ?? false; + login.fido2Credentials = obj.fido2Credentials?.map((f) => + Fido2Credential.fromSdkFido2Credential(f), + ); + + return login; + } } diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index f8aacf765bf..b8a30099454 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -71,4 +71,20 @@ export class Password extends Domain { lastUsedDate: this.lastUsedDate.toISOString(), }; } + + /** + * Maps an SDK PasswordHistory object to a Password + * @param obj - The SDK PasswordHistory object + */ + static fromSdkPasswordHistory(obj: PasswordHistory): Password | undefined { + if (!obj) { + return undefined; + } + + const passwordHistory = new Password(); + passwordHistory.password = EncString.fromJSON(obj.password); + passwordHistory.lastUsedDate = new Date(obj.lastUsedDate); + + return passwordHistory; + } } diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index ac7977b0e46..1426ff85eab 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -54,4 +54,19 @@ export class SecureNote extends Domain { type: this.type, }; } + + /** + * Maps an SDK SecureNote object to a SecureNote + * @param obj - The SDK SecureNote object + */ + static fromSdkSecureNote(obj: SdkSecureNote): SecureNote | undefined { + if (obj == null) { + return undefined; + } + + const secureNote = new SecureNote(); + secureNote.type = obj.type; + + return secureNote; + } } diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index c0afcd83fc2..0c8abf76e44 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -85,4 +85,21 @@ export class SshKey extends Domain { fingerprint: this.keyFingerprint.toJSON(), }; } + + /** + * Maps an SDK SshKey object to a SshKey + * @param obj - The SDK SshKey object + */ + static fromSdkSshKey(obj: SdkSshKey): SshKey | undefined { + if (obj == null) { + return undefined; + } + + const sshKey = new SshKey(); + sshKey.privateKey = EncString.fromJSON(obj.privateKey); + sshKey.publicKey = EncString.fromJSON(obj.publicKey); + sshKey.keyFingerprint = EncString.fromJSON(obj.fingerprint); + + return sshKey; + } } diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index dd7f5d6be57..ed02fa68365 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -10,7 +10,7 @@ import { linkedFieldOption } from "../../linked-field-option.decorator"; import { ItemView } from "./item.view"; -export class CardView extends ItemView { +export class CardView extends ItemView implements SdkCardView { @linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 }) cardholderName: string = null; @linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" }) @@ -168,4 +168,12 @@ export class CardView extends ItemView { return cardView; } + + /** + * Converts the CardView to an SDK CardView. + * The view implements the SdkView so we can safely return `this` + */ + toSdkCardView(): SdkCardView { + return this; + } } diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index b9d3e42aa62..46cea06979f 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -1,3 +1,7 @@ +import { Jsonify } from "type-fest"; + +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { CipherPermissionsApi } from "@bitwarden/common/vault/models/api/cipher-permissions.api"; import { CipherView as SdkCipherView, CipherType as SdkCipherType, @@ -85,6 +89,25 @@ describe("CipherView", () => { expect(actual).toMatchObject(expected); }); + + it("handle both string and object inputs for the cipher key", () => { + const cipherKeyString = "cipherKeyString"; + const cipherKeyObject = new EncString("cipherKeyObject"); + + // Test with string input + let actual = CipherView.fromJSON({ + key: cipherKeyString, + }); + expect(actual.key).toBeInstanceOf(EncString); + expect(actual.key?.toJSON()).toBe(cipherKeyString); + + // Test with object input (which can happen when cipher view is stored in an InMemory state provider) + actual = CipherView.fromJSON({ + key: cipherKeyObject, + } as Jsonify); + expect(actual.key).toBeInstanceOf(EncString); + expect(actual.key?.toJSON()).toBe(cipherKeyObject.toJSON()); + }); }); describe("fromSdkCipherView", () => { @@ -196,11 +219,80 @@ describe("CipherView", () => { __fromSdk: true, }, ], - passwordHistory: null, + passwordHistory: [], creationDate: new Date("2022-01-01T12:00:00.000Z"), revisionDate: new Date("2022-01-02T12:00:00.000Z"), deletedDate: null, }); }); }); + + describe("toSdkCipherView", () => { + it("maps properties correctly", () => { + const cipherView = new CipherView(); + cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602"; + cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c"; + cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f"; + cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"]; + cipherView.key = new EncString("some-key"); + cipherView.name = "name"; + cipherView.notes = "notes"; + cipherView.type = CipherType.Login; + cipherView.favorite = true; + cipherView.edit = true; + cipherView.viewPassword = false; + cipherView.reprompt = CipherRepromptType.None; + cipherView.organizationUseTotp = false; + cipherView.localData = { + lastLaunched: new Date("2022-01-01T12:00:00.000Z").getTime(), + lastUsedDate: new Date("2022-01-02T12:00:00.000Z").getTime(), + }; + cipherView.permissions = new CipherPermissionsApi(); + cipherView.permissions.restore = true; + cipherView.permissions.delete = true; + cipherView.attachments = []; + cipherView.fields = []; + cipherView.passwordHistory = []; + cipherView.login = new LoginView(); + cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z"); + cipherView.creationDate = new Date("2022-01-02T12:00:00.000Z"); + + const sdkCipherView = cipherView.toSdkCipherView(); + + expect(sdkCipherView).toMatchObject({ + id: "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602", + organizationId: "000f2a6e-da5e-4726-87ed-1c5c77322c3c", + folderId: "41b22db4-8e2a-4ed2-b568-f1186c72922f", + collectionIds: ["b0473506-3c3c-4260-a734-dfaaf833ab6f"], + key: "some-key", + name: "name", + notes: "notes", + type: SdkCipherType.Login, + favorite: true, + edit: true, + viewPassword: false, + reprompt: SdkCipherRepromptType.None, + organizationUseTotp: false, + localData: { + lastLaunched: "2022-01-01T12:00:00.000Z", + lastUsedDate: "2022-01-02T12:00:00.000Z", + }, + permissions: { + restore: true, + delete: true, + }, + deletedDate: undefined, + creationDate: "2022-01-02T12:00:00.000Z", + revisionDate: "2022-01-02T12:00:00.000Z", + attachments: [], + passwordHistory: [], + login: undefined, + identity: undefined, + card: undefined, + secureNote: undefined, + sshKey: undefined, + fields: [], + } as SdkCipherView); + }); + }); }); diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 353fffa8eef..0c41e49c3ab 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { uuidToString, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; @@ -9,7 +11,7 @@ import { DeepJsonify } from "../../../types/deep-jsonify"; import { CipherType, LinkedIdType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherPermissionsApi } from "../api/cipher-permissions.api"; -import { LocalData } from "../data/local.data"; +import { LocalData, toSdkLocalData, fromSdkLocalData } from "../data/local.data"; import { Cipher } from "../domain/cipher"; import { AttachmentView } from "./attachment.view"; @@ -41,14 +43,17 @@ export class CipherView implements View, InitializerMetadata { card = new CardView(); secureNote = new SecureNoteView(); sshKey = new SshKeyView(); - attachments: AttachmentView[] = null; - fields: FieldView[] = null; - passwordHistory: PasswordHistoryView[] = null; + attachments: AttachmentView[] = []; + fields: FieldView[] = []; + passwordHistory: PasswordHistoryView[] = []; collectionIds: string[] = null; revisionDate: Date = null; creationDate: Date = null; deletedDate: Date = null; reprompt: CipherRepromptType = CipherRepromptType.None; + // We need a copy of the encrypted key so we can pass it to + // the SdkCipherView during encryption + key?: EncString; /** * Flag to indicate if the cipher decryption failed. @@ -76,6 +81,7 @@ export class CipherView implements View, InitializerMetadata { this.deletedDate = c.deletedDate; // Old locally stored ciphers might have reprompt == null. If so set it to None. this.reprompt = c.reprompt ?? CipherRepromptType.None; + this.key = c.key; } private get item() { @@ -194,6 +200,18 @@ export class CipherView implements View, InitializerMetadata { const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)); const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)); const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)); + const permissions = CipherPermissionsApi.fromJSON(obj.permissions); + let key: EncString | undefined; + + if (obj.key != null) { + if (typeof obj.key === "string") { + // If the key is a string, we need to parse it as EncString + key = EncString.fromJSON(obj.key); + } else if ((obj.key as any) instanceof EncString) { + // If the key is already an EncString instance, we can use it directly + key = obj.key; + } + } Object.assign(view, obj, { creationDate: creationDate, @@ -202,6 +220,8 @@ export class CipherView implements View, InitializerMetadata { attachments: attachments, fields: fields, passwordHistory: passwordHistory, + permissions: permissions, + key: key, }); switch (obj.type) { @@ -236,9 +256,9 @@ export class CipherView implements View, InitializerMetadata { } const cipherView = new CipherView(); - cipherView.id = obj.id ?? null; - cipherView.organizationId = obj.organizationId ?? null; - cipherView.folderId = obj.folderId ?? null; + cipherView.id = uuidToString(obj.id) ?? null; + cipherView.organizationId = uuidToString(obj.organizationId) ?? null; + cipherView.folderId = uuidToString(obj.folderId) ?? null; cipherView.name = obj.name; cipherView.notes = obj.notes ?? null; cipherView.type = obj.type; @@ -247,26 +267,18 @@ export class CipherView implements View, InitializerMetadata { cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions); cipherView.edit = obj.edit; cipherView.viewPassword = obj.viewPassword; - cipherView.localData = obj.localData - ? { - lastUsedDate: obj.localData.lastUsedDate - ? new Date(obj.localData.lastUsedDate).getTime() - : undefined, - lastLaunched: obj.localData.lastLaunched - ? new Date(obj.localData.lastLaunched).getTime() - : undefined, - } - : undefined; + cipherView.localData = fromSdkLocalData(obj.localData); cipherView.attachments = - obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null; - cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null; + obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? []; + cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? []; cipherView.passwordHistory = - obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null; - cipherView.collectionIds = obj.collectionIds ?? null; + obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? []; + cipherView.collectionIds = obj.collectionIds?.map((i) => uuidToString(i)) ?? []; cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None; + cipherView.key = EncString.fromJSON(obj.key); switch (obj.type) { case CipherType.Card: @@ -290,4 +302,66 @@ export class CipherView implements View, InitializerMetadata { return cipherView; } + + /** + * Maps CipherView to SdkCipherView + * + * @returns {SdkCipherView} The SDK cipher view object + */ + toSdkCipherView(): SdkCipherView { + const sdkCipherView: SdkCipherView = { + id: this.id ? asUuid(this.id) : undefined, + organizationId: this.organizationId ? asUuid(this.organizationId) : undefined, + folderId: this.folderId ? asUuid(this.folderId) : undefined, + name: this.name ?? "", + notes: this.notes, + type: this.type ?? CipherType.Login, + favorite: this.favorite, + organizationUseTotp: this.organizationUseTotp, + permissions: this.permissions?.toSdkCipherPermissions(), + edit: this.edit, + viewPassword: this.viewPassword, + localData: toSdkLocalData(this.localData), + attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), + fields: this.fields?.map((f) => f.toSdkFieldView()), + passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistoryView()), + collectionIds: this.collectionIds?.map((i) => i) ?? [], + // Revision and creation dates are non-nullable in SDKCipherView + revisionDate: (this.revisionDate ?? new Date()).toISOString(), + creationDate: (this.creationDate ?? new Date()).toISOString(), + deletedDate: this.deletedDate?.toISOString(), + reprompt: this.reprompt ?? CipherRepromptType.None, + key: this.key?.toJSON(), + // Cipher type specific properties are set in the switch statement below + // CipherView initializes each with default constructors (undefined values) + // The SDK does not expect those undefined values and will throw exceptions + login: undefined, + card: undefined, + identity: undefined, + secureNote: undefined, + sshKey: undefined, + }; + + switch (this.type) { + case CipherType.Card: + sdkCipherView.card = this.card.toSdkCardView(); + break; + case CipherType.Identity: + sdkCipherView.identity = this.identity.toSdkIdentityView(); + break; + case CipherType.Login: + sdkCipherView.login = this.login.toSdkLoginView(); + break; + case CipherType.SecureNote: + sdkCipherView.secureNote = this.secureNote.toSdkSecureNoteView(); + break; + case CipherType.SshKey: + sdkCipherView.sshKey = this.sshKey.toSdkSshKeyView(); + break; + default: + break; + } + + return sdkCipherView; + } } diff --git a/libs/common/src/vault/models/view/fido2-credential.view.ts b/libs/common/src/vault/models/view/fido2-credential.view.ts index bf1d324d22d..410757ebe30 100644 --- a/libs/common/src/vault/models/view/fido2-credential.view.ts +++ b/libs/common/src/vault/models/view/fido2-credential.view.ts @@ -2,7 +2,10 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal"; +import { + Fido2CredentialView as SdkFido2CredentialView, + Fido2CredentialFullView, +} from "@bitwarden/sdk-internal"; import { ItemView } from "./item.view"; @@ -56,4 +59,22 @@ export class Fido2CredentialView extends ItemView { return view; } + + toSdkFido2CredentialFullView(): Fido2CredentialFullView { + return { + credentialId: this.credentialId, + keyType: this.keyType, + keyAlgorithm: this.keyAlgorithm, + keyCurve: this.keyCurve, + keyValue: this.keyValue, + rpId: this.rpId, + userHandle: this.userHandle, + userName: this.userName, + counter: this.counter.toString(), + rpName: this.rpName, + userDisplayName: this.userDisplayName, + discoverable: this.discoverable ? "true" : "false", + creationDate: this.creationDate?.toISOString(), + }; + } } diff --git a/libs/common/src/vault/models/view/field.view.ts b/libs/common/src/vault/models/view/field.view.ts index 770150f8a63..8c9a923aed2 100644 --- a/libs/common/src/vault/models/view/field.view.ts +++ b/libs/common/src/vault/models/view/field.view.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal"; +import { FieldView as SdkFieldView, FieldType as SdkFieldType } from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; import { FieldType, LinkedIdType } from "../../enums"; @@ -50,4 +50,16 @@ export class FieldView implements View { return view; } + + /** + * Converts the FieldView to an SDK FieldView. + */ + toSdkFieldView(): SdkFieldView { + return { + name: this.name ?? undefined, + value: this.value ?? undefined, + type: this.type ?? SdkFieldType.Text, + linkedId: this.linkedId ?? undefined, + }; + } } diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index 877940e4aea..2b863dc5e5f 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -10,7 +10,7 @@ import { linkedFieldOption } from "../../linked-field-option.decorator"; import { ItemView } from "./item.view"; -export class IdentityView extends ItemView { +export class IdentityView extends ItemView implements SdkIdentityView { @linkedFieldOption(LinkedId.Title, { sortPosition: 0 }) title: string = null; @linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 }) @@ -192,4 +192,12 @@ export class IdentityView extends ItemView { return identityView; } + + /** + * Converts the IdentityView to an SDK IdentityView. + * The view implements the SdkView so we can safely return `this` + */ + toSdkIdentityView(): SdkIdentityView { + return this; + } } diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index 43d47aa4a3c..38cd517e542 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -129,6 +129,15 @@ export class LoginUriView implements View { return view; } + /** Converts a LoginUriView object to an SDK LoginUriView object. */ + toSdkLoginUriView(): SdkLoginUriView { + return { + uri: this.uri ?? undefined, + match: this.match ?? undefined, + uriChecksum: undefined, // SDK handles uri checksum generation internally + }; + } + matchesUri( targetUri: string, equivalentDomains: Set, diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index c6e6ca001e4..d268cf4afaa 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -124,10 +124,30 @@ export class LoginView extends ItemView { obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); loginView.totp = obj.totp ?? null; loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null; - loginView.uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || []; + loginView.uris = + obj.uris + ?.filter((uri) => uri.uri != null && uri.uri !== "") + .map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || []; // FIDO2 credentials are not decrypted here, they remain encrypted loginView.fido2Credentials = null; return loginView; } + + /** + * Converts the LoginView to an SDK LoginView. + * + * Note: FIDO2 credentials remain encrypted in the SDK view so they are not included here. + */ + toSdkLoginView(): SdkLoginView { + return { + username: this.username, + password: this.password, + passwordRevisionDate: this.passwordRevisionDate?.toISOString(), + totp: this.totp, + autofillOnPageLoad: this.autofillOnPageLoad ?? undefined, + uris: this.uris?.map((uri) => uri.toSdkLoginUriView()), + fido2Credentials: undefined, // FIDO2 credentials are handled separately and remain encrypted + }; + } } diff --git a/libs/common/src/vault/models/view/password-history.view.spec.ts b/libs/common/src/vault/models/view/password-history.view.spec.ts index 81894ec7493..512ec8d86d8 100644 --- a/libs/common/src/vault/models/view/password-history.view.spec.ts +++ b/libs/common/src/vault/models/view/password-history.view.spec.ts @@ -33,4 +33,17 @@ describe("PasswordHistoryView", () => { }); }); }); + + describe("toSdkPasswordHistoryView", () => { + it("should return a SdkPasswordHistoryView", () => { + const passwordHistoryView = new PasswordHistoryView(); + passwordHistoryView.password = "password"; + passwordHistoryView.lastUsedDate = new Date("2023-10-01T00:00:00.000Z"); + + expect(passwordHistoryView.toSdkPasswordHistoryView()).toMatchObject({ + password: "password", + lastUsedDate: "2023-10-01T00:00:00.000Z", + }); + }); + }); }); diff --git a/libs/common/src/vault/models/view/password-history.view.ts b/libs/common/src/vault/models/view/password-history.view.ts index 31f05f4cc71..9bd708b19fd 100644 --- a/libs/common/src/vault/models/view/password-history.view.ts +++ b/libs/common/src/vault/models/view/password-history.view.ts @@ -41,4 +41,14 @@ export class PasswordHistoryView implements View { return view; } + + /** + * Converts the PasswordHistoryView to an SDK PasswordHistoryView. + */ + toSdkPasswordHistoryView(): SdkPasswordHistoryView { + return { + password: this.password ?? "", + lastUsedDate: this.lastUsedDate.toISOString(), + }; + } } diff --git a/libs/common/src/vault/models/view/secure-note.view.ts b/libs/common/src/vault/models/view/secure-note.view.ts index 8e7a6b4652d..5e401961869 100644 --- a/libs/common/src/vault/models/view/secure-note.view.ts +++ b/libs/common/src/vault/models/view/secure-note.view.ts @@ -9,7 +9,7 @@ import { SecureNote } from "../domain/secure-note"; import { ItemView } from "./item.view"; -export class SecureNoteView extends ItemView { +export class SecureNoteView extends ItemView implements SdkSecureNoteView { type: SecureNoteType = null; constructor(n?: SecureNote) { @@ -42,4 +42,12 @@ export class SecureNoteView extends ItemView { return secureNoteView; } + + /** + * Converts the SecureNoteView to an SDK SecureNoteView. + * The view implements the SdkView so we can safely return `this` + */ + toSdkSecureNoteView(): SdkSecureNoteView { + return this; + } } diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts index a83793678dc..0547eeb7f8e 100644 --- a/libs/common/src/vault/models/view/ssh-key.view.ts +++ b/libs/common/src/vault/models/view/ssh-key.view.ts @@ -63,4 +63,15 @@ export class SshKeyView extends ItemView { return sshKeyView; } + + /** + * Converts the SshKeyView to an SDK SshKeyView. + */ + toSdkSshKeyView(): SdkSshKeyView { + return { + privateKey: this.privateKey, + publicKey: this.publicKey, + fingerprint: this.keyFingerprint, + }; + } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index bf30b78ca63..f027122993d 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,7 +1,9 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management"; @@ -23,7 +25,7 @@ import { Utils } from "../../platform/misc/utils"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../platform/services/container.service"; -import { CipherId, UserId } from "../../types/guid"; +import { CipherId, UserId, OrganizationId, CollectionId } from "../../types/guid"; import { CipherKey, OrgKey, UserKey } from "../../types/key"; import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { EncryptionContext } from "../abstractions/cipher.service"; @@ -108,6 +110,7 @@ describe("Cipher Service", () => { const cipherEncryptionService = mock(); const userId = "TestUserId" as UserId; + const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b2" as OrganizationId; let cipherService: CipherService; let encryptionContext: EncryptionContext; @@ -155,7 +158,9 @@ describe("Cipher Service", () => { ); configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false)); - configService.getFeatureFlag.mockResolvedValue(false); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(false); const spy = jest.spyOn(cipherFileUploadService, "upload"); @@ -270,6 +275,55 @@ describe("Cipher Service", () => { jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true); }); + it("should call encrypt method of CipherEncryptionService when feature flag is true", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM22136_SdkCipherEncryption) + .mockResolvedValue(true); + cipherEncryptionService.encrypt.mockResolvedValue(encryptionContext); + + const result = await cipherService.encrypt(cipherView, userId); + + expect(result).toEqual(encryptionContext); + expect(cipherEncryptionService.encrypt).toHaveBeenCalledWith(cipherView, userId); + }); + + it("should call legacy encrypt when feature flag is false", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM22136_SdkCipherEncryption) + .mockResolvedValue(false); + + jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher); + + const result = await cipherService.encrypt(cipherView, userId); + + expect(result).toEqual(encryptionContext); + expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled(); + }); + + it("should call legacy encrypt when keys are provided", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM22136_SdkCipherEncryption) + .mockResolvedValue(true); + + jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher); + + const encryptKey = new SymmetricCryptoKey(new Uint8Array(32)); + const decryptKey = new SymmetricCryptoKey(new Uint8Array(32)); + + let result = await cipherService.encrypt(cipherView, userId, encryptKey); + + expect(result).toEqual(encryptionContext); + expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled(); + + result = await cipherService.encrypt(cipherView, userId, undefined, decryptKey); + expect(result).toEqual(encryptionContext); + expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled(); + + result = await cipherService.encrypt(cipherView, userId, encryptKey, decryptKey); + expect(result).toEqual(encryptionContext); + expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled(); + }); + it("should return the encrypting user id", async () => { keyService.getOrgKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), @@ -310,7 +364,9 @@ describe("Cipher Service", () => { }); it("is null when feature flag is false", async () => { - configService.getFeatureFlag.mockResolvedValue(false); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(false); const { cipher } = await cipherService.encrypt(cipherView, userId); expect(cipher.key).toBeNull(); @@ -318,7 +374,9 @@ describe("Cipher Service", () => { describe("when feature flag is true", () => { beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue(true); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(true); }); it("is null when the cipher is not viewPassword", async () => { @@ -348,7 +406,9 @@ describe("Cipher Service", () => { }); it("is not called when feature flag is false", async () => { - configService.getFeatureFlag.mockResolvedValue(false); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(false); await cipherService.encrypt(cipherView, userId); @@ -357,7 +417,9 @@ describe("Cipher Service", () => { describe("when feature flag is true", () => { beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue(true); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(true); }); it("is called when cipher viewPassword is true", async () => { @@ -401,7 +463,9 @@ describe("Cipher Service", () => { let encryptedKey: EncString; beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue(true); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(true); configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true)); searchService.indexedEntityId$.mockReturnValue(of(null)); @@ -474,7 +538,9 @@ describe("Cipher Service", () => { describe("decrypt", () => { it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => { - configService.getFeatureFlag.mockResolvedValue(true); + configService.getFeatureFlag + .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk) + .mockResolvedValue(true); cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(encryptionContext.cipher)); const result = await cipherService.decrypt(encryptionContext.cipher, userId); @@ -488,7 +554,9 @@ describe("Cipher Service", () => { it("should call legacy decrypt when feature flag is false", async () => { const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; - configService.getFeatureFlag.mockResolvedValue(false); + configService.getFeatureFlag + .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk) + .mockResolvedValue(false); cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); jest @@ -509,7 +577,9 @@ describe("Cipher Service", () => { it("should use SDK when feature flag is enabled", async () => { const cipher = new Cipher(cipherData); const attachment = new AttachmentView(cipher.attachments![0]); - configService.getFeatureFlag.mockResolvedValue(true); + configService.getFeatureFlag + .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk) + .mockResolvedValue(true); jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData })); cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent); @@ -534,7 +604,9 @@ describe("Cipher Service", () => { }); it("should use legacy decryption when feature flag is enabled", async () => { - configService.getFeatureFlag.mockResolvedValue(false); + configService.getFeatureFlag + .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk) + .mockResolvedValue(false); const cipher = new Cipher(cipherData); const attachment = new AttachmentView(cipher.attachments![0]); attachment.key = makeSymmetricCryptoKey(64); @@ -557,4 +629,77 @@ describe("Cipher Service", () => { expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key); }); }); + + describe("shareWithServer()", () => { + it("should use cipherEncryptionService to move the cipher when feature flag enabled", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM22136_SdkCipherEncryption) + .mockResolvedValue(true); + + apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData)); + + const expectedCipher = new Cipher(cipherData); + expectedCipher.organizationId = orgId; + const cipherView = new CipherView(expectedCipher); + const collectionIds = ["collection1", "collection2"] as CollectionId[]; + + cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test + + cipherEncryptionService.moveToOrganization.mockResolvedValue({ + cipher: expectedCipher, + encryptedFor: userId, + }); + + await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId); + + // Expect SDK usage + expect(cipherEncryptionService.moveToOrganization).toHaveBeenCalledWith( + cipherView, + orgId, + userId, + ); + // Expect collectionIds to be assigned + expect(apiService.putShareCipher).toHaveBeenCalledWith( + cipherView.id, + expect.objectContaining({ + cipher: expect.objectContaining({ organizationId: orgId }), + collectionIds: collectionIds, + }), + ); + }); + + it("should use legacy encryption when feature flag disabled", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM22136_SdkCipherEncryption) + .mockResolvedValue(false); + + apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData)); + + const expectedCipher = new Cipher(cipherData); + expectedCipher.organizationId = orgId; + const cipherView = new CipherView(expectedCipher); + const collectionIds = ["collection1", "collection2"] as CollectionId[]; + + cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test + + const oldEncryptSharedSpy = jest + .spyOn(cipherService as any, "encryptSharedCipher") + .mockResolvedValue({ + cipher: expectedCipher, + encryptedFor: userId, + }); + + await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId); + + // Expect no SDK usage + expect(cipherEncryptionService.moveToOrganization).not.toHaveBeenCalled(); + expect(oldEncryptSharedSpy).toHaveBeenCalledWith( + expect.objectContaining({ + organizationId: orgId, + collectionIds: collectionIds, + } as unknown as CipherView), + userId, + ); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8bef5289a95..1524e4e1b29 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -231,13 +231,14 @@ export class CipherService implements CipherServiceAbstraction { this.clearCipherViewsForUser$.next(userId); } - async encrypt( - model: CipherView, - userId: UserId, - keyForCipherEncryption?: SymmetricCryptoKey, - keyForCipherKeyDecryption?: SymmetricCryptoKey, - originalCipher: Cipher = null, - ): Promise { + /** + * Adjusts the cipher history for the given model by updating its history properties based on the original cipher. + * @param model The cipher model to adjust. + * @param userId The acting userId + * @param originalCipher The original cipher to compare against. If not provided, it will be fetched from the store. + * @private + */ + private async adjustCipherHistory(model: CipherView, userId: UserId, originalCipher?: Cipher) { if (model.id != null) { if (originalCipher == null) { originalCipher = await this.get(model.id, userId); @@ -247,6 +248,25 @@ export class CipherService implements CipherServiceAbstraction { } this.adjustPasswordHistoryLength(model); } + } + + async encrypt( + model: CipherView, + userId: UserId, + keyForCipherEncryption?: SymmetricCryptoKey, + keyForCipherKeyDecryption?: SymmetricCryptoKey, + originalCipher: Cipher = null, + ): Promise { + await this.adjustCipherHistory(model, userId, originalCipher); + + const sdkEncryptionEnabled = + (await this.configService.getFeatureFlag(FeatureFlag.PM22136_SdkCipherEncryption)) && + keyForCipherEncryption == null && // PM-23085 - SDK encryption does not currently support custom keys (e.g. key rotation) + keyForCipherKeyDecryption == null; // PM-23348 - Or has explicit methods for re-encrypting ciphers with different keys (e.g. move to org) + + if (sdkEncryptionEnabled) { + return await this.cipherEncryptionService.encrypt(model, userId); + } const cipher = new Cipher(); cipher.id = model.id; @@ -854,22 +874,48 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, collectionIds: string[], userId: UserId, + originalCipher?: Cipher, ): Promise { - const attachmentPromises: Promise[] = []; - if (cipher.attachments != null) { - cipher.attachments.forEach((attachment) => { - if (attachment.key == null) { - attachmentPromises.push( - this.shareAttachmentWithServer(attachment, cipher.id, organizationId), - ); - } - }); - } - await Promise.all(attachmentPromises); + const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM22136_SdkCipherEncryption, + ); + + await this.adjustCipherHistory(cipher, userId, originalCipher); + + let encCipher: EncryptionContext; + if (sdkCipherEncryptionEnabled) { + // The SDK does not expect the cipher to already have an organizationId. It will result in the wrong + // cipher encryption key being used during the move to organization operation. + if (cipher.organizationId != null) { + throw new Error("Cipher is already associated with an organization."); + } + + encCipher = await this.cipherEncryptionService.moveToOrganization( + cipher, + organizationId as OrganizationId, + userId, + ); + encCipher.cipher.collectionIds = collectionIds; + } else { + // This old attachment logic is safe to remove after it is replaced in PM-22750; which will require fixing + // the attachment before sharing. + const attachmentPromises: Promise[] = []; + if (cipher.attachments != null) { + cipher.attachments.forEach((attachment) => { + if (attachment.key == null) { + attachmentPromises.push( + this.shareAttachmentWithServer(attachment, cipher.id, organizationId), + ); + } + }); + } + await Promise.all(attachmentPromises); + + cipher.organizationId = organizationId; + cipher.collectionIds = collectionIds; + encCipher = await this.encryptSharedCipher(cipher, userId); + } - cipher.organizationId = organizationId; - cipher.collectionIds = collectionIds; - const encCipher = await this.encryptSharedCipher(cipher, userId); const request = new CipherShareRequest(encCipher); const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); @@ -883,16 +929,36 @@ export class CipherService implements CipherServiceAbstraction { collectionIds: string[], userId: UserId, ) { + const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM22136_SdkCipherEncryption, + ); const promises: Promise[] = []; const encCiphers: Cipher[] = []; for (const cipher of ciphers) { - cipher.organizationId = organizationId; - cipher.collectionIds = collectionIds; - promises.push( - this.encryptSharedCipher(cipher, userId).then((c) => { - encCiphers.push(c.cipher); - }), - ); + if (sdkCipherEncryptionEnabled) { + // The SDK does not expect the cipher to already have an organizationId. It will result in the wrong + // cipher encryption key being used during the move to organization operation. + if (cipher.organizationId != null) { + throw new Error("Cipher is already associated with an organization."); + } + + promises.push( + this.cipherEncryptionService + .moveToOrganization(cipher, organizationId as OrganizationId, userId) + .then((encCipher) => { + encCipher.cipher.collectionIds = collectionIds; + encCiphers.push(encCipher.cipher); + }), + ); + } else { + cipher.organizationId = organizationId; + cipher.collectionIds = collectionIds; + promises.push( + this.encryptSharedCipher(cipher, userId).then((c) => { + encCiphers.push(c.cipher); + }), + ); + } } await Promise.all(promises); const request = new CipherBulkShareRequest(encCiphers, collectionIds, userId); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index 4d05a5197fb..9e0cf62ed08 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -1,20 +1,22 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { Fido2Credential } from "@bitwarden/common/vault/models/domain/fido2-credential"; import { - Fido2Credential, + Fido2Credential as SdkFido2Credential, Cipher as SdkCipher, CipherType as SdkCipherType, CipherView as SdkCipherView, CipherListView, AttachmentView as SdkAttachmentView, + Fido2CredentialFullView, } from "@bitwarden/sdk-internal"; import { mockEnc } from "../../../spec"; import { UriMatchStrategy } from "../../models/domain/domain-service"; import { LogService } from "../../platform/abstractions/log.service"; import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; -import { UserId } from "../../types/guid"; +import { UserId, CipherId, OrganizationId } from "../../types/guid"; import { CipherRepromptType, CipherType } from "../enums"; import { CipherPermissionsApi } from "../models/api/cipher-permissions.api"; import { CipherData } from "../models/data/cipher.data"; @@ -25,10 +27,15 @@ import { Fido2CredentialView } from "../models/view/fido2-credential.view"; import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service"; +const cipherId = "bdc4ef23-1116-477e-ae73-247854af58cb" as CipherId; +const orgId = "c5e9654f-6cc5-44c4-8e09-3d323522668c" as OrganizationId; +const folderId = "a3e9654f-6cc5-44c4-8e09-3d323522668c"; +const userId = "59fbbb44-8cc8-4279-ab40-afc5f68704f4" as UserId; + const cipherData: CipherData = { - id: "id", - organizationId: "orgId", - folderId: "folderId", + id: cipherId, + organizationId: orgId, + folderId: folderId, edit: true, viewPassword: true, organizationUseTotp: true, @@ -78,13 +85,17 @@ describe("DefaultCipherEncryptionService", () => { const sdkService = mock(); const logService = mock(); let sdkCipherView: SdkCipherView; + let sdkCipher: SdkCipher; const mockSdkClient = { vault: jest.fn().mockReturnValue({ ciphers: jest.fn().mockReturnValue({ + encrypt: jest.fn(), + set_fido2_credentials: jest.fn(), decrypt: jest.fn(), decrypt_list: jest.fn(), decrypt_fido2_credentials: jest.fn(), + move_to_organization: jest.fn(), }), attachments: jest.fn().mockReturnValue({ decrypt_buffer: jest.fn(), @@ -99,21 +110,25 @@ describe("DefaultCipherEncryptionService", () => { take: jest.fn().mockReturnValue(mockRef), }; - const userId = "user-id" as UserId; - let cipherObj: Cipher; + let cipherViewObj: CipherView; beforeEach(() => { sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any; cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService); cipherObj = new Cipher(cipherData); + cipherViewObj = new CipherView(cipherObj); jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => { return { id: cipherData.id } as SdkCipher; }); + jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(() => { + return { id: cipherData.id } as SdkCipherView; + }); + sdkCipherView = { - id: "test-id", + id: cipherId as string, type: SdkCipherType.Login, name: "test-name", login: { @@ -121,16 +136,211 @@ describe("DefaultCipherEncryptionService", () => { password: "test-password", }, } as SdkCipherView; + + sdkCipher = { + id: cipherId, + type: SdkCipherType.Login, + name: "encrypted-name", + login: { + username: "encrypted-username", + password: "encrypted-password", + }, + } as unknown as SdkCipher; }); afterEach(() => { jest.clearAllMocks(); }); + describe("encrypt", () => { + it("should encrypt a cipher successfully", async () => { + const expectedCipher: Cipher = { + id: cipherId as string, + type: CipherType.Login, + name: "encrypted-name", + login: { + username: "encrypted-username", + password: "encrypted-password", + }, + } as unknown as Cipher; + + mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ + cipher: sdkCipher, + encryptedFor: userId, + }); + jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher); + + const result = await cipherEncryptionService.encrypt(cipherViewObj, userId); + + expect(result).toBeDefined(); + expect(result!.cipher).toEqual(expectedCipher); + expect(result!.encryptedFor).toBe(userId); + expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled(); + expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith({ id: cipherData.id }); + }); + + it("should encrypt FIDO2 credentials if present", async () => { + const fidoCredentialView = new Fido2CredentialView(); + fidoCredentialView.credentialId = "credentialId"; + + cipherViewObj.login.fido2Credentials = [fidoCredentialView]; + + jest.spyOn(fidoCredentialView, "toSdkFido2CredentialFullView").mockImplementation( + () => + ({ + credentialId: "credentialId", + }) as Fido2CredentialFullView, + ); + jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation( + () => + ({ + id: cipherId as string, + login: { + fido2Credentials: undefined, + }, + }) as unknown as SdkCipherView, + ); + + mockSdkClient + .vault() + .ciphers() + .set_fido2_credentials.mockReturnValue({ + id: cipherId as string, + login: { + fido2Credentials: [ + { + credentialId: "encrypted-credentialId", + }, + ], + }, + }); + + mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ + cipher: sdkCipher, + encryptedFor: userId, + }); + + cipherObj.login!.fido2Credentials = [ + { credentialId: "encrypted-credentialId" } as unknown as Fido2Credential, + ]; + + jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(cipherObj); + + const result = await cipherEncryptionService.encrypt(cipherViewObj, userId); + + expect(result).toBeDefined(); + expect(result!.cipher.login!.fido2Credentials).toHaveLength(1); + + // Ensure set_fido2_credentials was called with correct parameters + expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith( + expect.objectContaining({ id: cipherId }), + [{ credentialId: "credentialId" }], + ); + + // Encrypted fido2 credential should be in the cipher passed to encrypt + expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith( + expect.objectContaining({ + id: cipherId, + login: { fido2Credentials: [{ credentialId: "encrypted-credentialId" }] }, + }), + ); + }); + }); + + describe("moveToOrganization", () => { + it("should call the sdk method to move a cipher to an organization", async () => { + const expectedCipher: Cipher = { + id: cipherId as string, + type: CipherType.Login, + name: "encrypted-name", + organizationId: orgId, + login: { + username: "encrypted-username", + password: "encrypted-password", + }, + } as unknown as Cipher; + + mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({ + id: cipherId, + organizationId: orgId, + }); + mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ + cipher: sdkCipher, + encryptedFor: userId, + }); + jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher); + + const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId); + + expect(result).toBeDefined(); + expect(result!.cipher).toEqual(expectedCipher); + expect(result!.encryptedFor).toBe(userId); + expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled(); + expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith( + { id: cipherData.id }, + orgId, + ); + }); + + it("should re-encrypt any fido2 credentials when moving to an organization", async () => { + const mockSdkCredentialView = { + username: "username", + } as unknown as Fido2CredentialFullView; + const mockCredentialView = mock(); + mockCredentialView.toSdkFido2CredentialFullView.mockReturnValue(mockSdkCredentialView); + cipherViewObj.login.fido2Credentials = [mockCredentialView]; + const expectedCipher: Cipher = { + id: cipherId as string, + type: CipherType.Login, + name: "encrypted-name", + organizationId: orgId, + login: { + username: "encrypted-username", + password: "encrypted-password", + fido2Credentials: [{ username: "encrypted-username" }], + }, + } as unknown as Cipher; + + mockSdkClient + .vault() + .ciphers() + .set_fido2_credentials.mockReturnValue({ + id: cipherId as string, + login: { + fido2Credentials: [mockSdkCredentialView], + }, + } as SdkCipherView); + mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({ + id: cipherId, + organizationId: orgId, + }); + mockSdkClient.vault().ciphers().encrypt.mockReturnValue({ + cipher: sdkCipher, + encryptedFor: userId, + }); + jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher); + + const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId); + + expect(result).toBeDefined(); + expect(result!.cipher).toEqual(expectedCipher); + expect(result!.encryptedFor).toBe(userId); + expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled(); + expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith( + expect.objectContaining({ id: cipherId }), + expect.arrayContaining([mockSdkCredentialView]), + ); + expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith( + { id: cipherData.id, login: { fido2Credentials: [mockSdkCredentialView] } }, + orgId, + ); + }); + }); + describe("decrypt", () => { it("should decrypt a cipher successfully", async () => { const expectedCipherView: CipherView = { - id: "test-id", + id: cipherId as string, type: CipherType.Login, name: "test-name", login: { @@ -168,12 +378,12 @@ describe("DefaultCipherEncryptionService", () => { discoverable: mockEnc("true"), creationDate: new Date("2023-01-01T12:00:00.000Z"), }, - ] as unknown as Fido2Credential[]; + ] as unknown as SdkFido2Credential[]; sdkCipherView.login!.fido2Credentials = fido2Credentials; const expectedCipherView: CipherView = { - id: "test-id", + id: cipherId, type: CipherType.Login, name: "test-name", login: { @@ -228,13 +438,15 @@ describe("DefaultCipherEncryptionService", () => { it("should decrypt multiple ciphers successfully", async () => { const ciphers = [new Cipher(cipherData), new Cipher(cipherData)]; + const cipherId2 = "bdc4ef23-2222-477e-ae73-247854af58cb" as CipherId; + const expectedViews = [ { - id: "test-id-1", + id: cipherId as string, name: "test-name-1", } as CipherView, { - id: "test-id-2", + id: cipherId2 as string, name: "test-name-2", } as CipherView, ]; @@ -242,8 +454,11 @@ describe("DefaultCipherEncryptionService", () => { mockSdkClient .vault() .ciphers() - .decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView) - .mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView); + .decrypt.mockReturnValueOnce({ + id: cipherId, + name: "test-name-1", + } as unknown as SdkCipherView) + .mockReturnValueOnce({ id: cipherId2, name: "test-name-2" } as unknown as SdkCipherView); jest .spyOn(CipherView, "fromSdkCipherView") diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 2c57df6f5bb..3547bafb4c9 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -1,10 +1,15 @@ import { EMPTY, catchError, firstValueFrom, map } from "rxjs"; -import { CipherListView } from "@bitwarden/sdk-internal"; +import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { + CipherListView, + BitwardenClient, + CipherView as SdkCipherView, +} from "@bitwarden/sdk-internal"; import { LogService } from "../../platform/abstractions/log.service"; -import { SdkService } from "../../platform/abstractions/sdk/sdk.service"; -import { UserId } from "../../types/guid"; +import { SdkService, asUuid } from "../../platform/abstractions/sdk/sdk.service"; +import { UserId, OrganizationId } from "../../types/guid"; import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { CipherType } from "../enums"; import { Cipher } from "../models/domain/cipher"; @@ -18,6 +23,67 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { private logService: LogService, ) {} + async encrypt(model: CipherView, userId: UserId): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + const sdkCipherView = this.toSdkCipherView(model, ref.value); + + const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView); + + return { + cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, + encryptedFor: asUuid(encryptionContext.encryptedFor), + }; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to encrypt cipher: ${error}`); + return EMPTY; + }), + ), + ); + } + + async moveToOrganization( + model: CipherView, + organizationId: OrganizationId, + userId: UserId, + ): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map((sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + + using ref = sdk.take(); + const sdkCipherView = this.toSdkCipherView(model, ref.value); + + const movedCipherView = ref.value + .vault() + .ciphers() + .move_to_organization(sdkCipherView, asUuid(organizationId)); + + const encryptionContext = ref.value.vault().ciphers().encrypt(movedCipherView); + + return { + cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, + encryptedFor: asUuid(encryptionContext.encryptedFor), + }; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to move cipher to organization: ${error}`); + return EMPTY; + }), + ), + ); + } + async decrypt(cipher: Cipher, userId: UserId): Promise { return firstValueFrom( this.sdkService.userClient$(userId).pipe( @@ -51,11 +117,8 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { clientCipherView.login.fido2Credentials = fido2CredentialViews .map((f) => { const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; - - return { - ...view, - keyValue: decryptedKeyValue, - }; + view.keyValue = decryptedKeyValue; + return view; }) .filter((view): view is Fido2CredentialView => view !== undefined); } @@ -104,10 +167,8 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { clientCipherView.login.fido2Credentials = fido2CredentialViews .map((f) => { const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!; - return { - ...view, - keyValue: decryptedKeyValue, - }; + view.keyValue = decryptedKeyValue; + return view; }) .filter((view): view is Fido2CredentialView => view !== undefined); } @@ -187,4 +248,25 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { ), ); } + + /** + * Helper method to convert a CipherView model to an SDK CipherView. Has special handling for Fido2 credentials + * that need to be encrypted before being sent to the SDK. + * @param model The CipherView model to convert + * @param sdk An instance of SDK client + * @private + */ + private toSdkCipherView(model: CipherView, sdk: BitwardenClient): SdkCipherView { + let sdkCipherView = model.toSdkCipherView(); + + if (model.type === CipherType.Login && model.login?.hasFido2Credentials) { + // Encrypt Fido2 credentials separately + const fido2Credentials = model.login.fido2Credentials?.map((f) => + f.toSdkFido2CredentialFullView(), + ); + sdkCipherView = sdk.vault().ciphers().set_fido2_credentials(sdkCipherView, fido2Credentials); + } + + return sdkCipherView; + } } diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 463d61dbbdf..1a97bc5a325 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -320,12 +320,6 @@ export abstract class BaseImporter { } else { cipher.notes = cipher.notes.trim(); } - if (cipher.fields != null && cipher.fields.length === 0) { - cipher.fields = null; - } - if (cipher.passwordHistory != null && cipher.passwordHistory.length === 0) { - cipher.passwordHistory = null; - } } protected processKvp( diff --git a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts index d7a4d487bcb..026c501cf5a 100644 --- a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts @@ -66,7 +66,7 @@ describe("Keeper CSV Importer", () => { expect(result != null).toBe(true); const cipher = result.ciphers.shift(); - expect(cipher.fields).toBeNull(); + expect(cipher.fields.length).toBe(0); const cipher2 = result.ciphers.shift(); expect(cipher2.fields.length).toBe(2); diff --git a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts index 31169021e0c..22008f3b4c1 100644 --- a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts @@ -39,7 +39,7 @@ describe("Keeper Json Importer", () => { expect(cipher3.login.username).toEqual("someUserName"); expect(cipher3.login.password).toEqual("w4k4k1wergf$^&@#*%2"); expect(cipher3.notes).toBeNull(); - expect(cipher3.fields).toBeNull(); + expect(cipher3.fields.length).toBe(0); expect(cipher3.login.uris.length).toEqual(1); const uriView3 = cipher3.login.uris.shift(); expect(uriView3.uri).toEqual("https://example.com"); diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 5228c85c3f7..d195ff8b00b 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -5,7 +5,7 @@ import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { UserId } from "@bitwarden/common/types/guid"; +import { UserId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -31,21 +31,13 @@ export class DefaultCipherFormService implements CipherFormService { } async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise { - // Passing the original cipher is important here as it is responsible for appending to password history const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const encrypted = await this.cipherService.encrypt( - cipher, - activeUserId, - null, - null, - config.originalCipher ?? null, - ); - const encryptedCipher = encrypted.cipher; let savedCipher: Cipher; // Creating a new cipher if (cipher.id == null) { + const encrypted = await this.cipherService.encrypt(cipher, activeUserId); savedCipher = await this.cipherService.createWithServer(encrypted, config.admin); return await this.cipherService.decrypt(savedCipher, activeUserId); } @@ -61,16 +53,37 @@ export class DefaultCipherFormService implements CipherFormService { // Call shareWithServer if the owner is changing from a user to an organization if (config.originalCipher.organizationId === null && cipher.organizationId != null) { + // shareWithServer expects the cipher to have no organizationId set + const organizationId = cipher.organizationId as OrganizationId; + cipher.organizationId = null; + savedCipher = await this.cipherService.shareWithServer( cipher, - cipher.organizationId, + organizationId, cipher.collectionIds, activeUserId, + config.originalCipher, ); // If the collectionIds are the same, update the cipher normally } else if (isSetEqual(originalCollectionIds, newCollectionIds)) { + const encrypted = await this.cipherService.encrypt( + cipher, + activeUserId, + null, + null, + config.originalCipher, + ); savedCipher = await this.cipherService.updateWithServer(encrypted, config.admin); } else { + const encrypted = await this.cipherService.encrypt( + cipher, + activeUserId, + null, + null, + config.originalCipher, + ); + const encryptedCipher = encrypted.cipher; + // Updating a cipher with collection changes is not supported with a single request currently // First update the cipher with the original collectionIds encryptedCipher.collectionIds = config.originalCipher.collectionIds; diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index a2ade0e885c..e65dd62500e 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -67,12 +67,12 @@ - + - + { }); describe("history", () => { - const password1 = { password: "bad-password-1", lastUsedDate: new Date("09/13/2004") }; - const password2 = { password: "bad-password-2", lastUsedDate: new Date("02/01/2004") }; + const password1 = { + password: "bad-password-1", + lastUsedDate: new Date("09/13/2004"), + } as PasswordHistoryView; + const password2 = { + password: "bad-password-2", + lastUsedDate: new Date("02/01/2004"), + } as PasswordHistoryView; beforeEach(async () => { mockCipher.passwordHistory = [password1, password2]; diff --git a/package-lock.json b/package-lock.json index e6d4a0b9b89..b9f661f6915 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.225", + "@bitwarden/sdk-internal": "0.2.0-main.227", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4604,9 +4604,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.225", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.225.tgz", - "integrity": "sha512-bhSFNX584GPJ9wMBYff1d18/Hfj+o+D4E1l3uDLZNXRI9s7w919AQWqJ0xUy1vh8gpkLJovkf64HQGqs0OiQQA==", + "version": "0.2.0-main.227", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.227.tgz", + "integrity": "sha512-afOsl9jwi1qyX/tF4bYP3EWXgc8oMgnCA0hPPh+AJpn7GgoAPCi+WXaJkbBPwRpxZFKEpwt3oLRNTvtkECvFJw==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index b7923a92f6f..ac00e674310 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.225", + "@bitwarden/sdk-internal": "0.2.0-main.227", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From b54944da416925c90fabc0479d2cc20b4c3f32da Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 22 Jul 2025 12:35:55 +0200 Subject: [PATCH 006/179] Deprecate encstring's decrypt function (#15703) --- libs/common/src/key-management/crypto/models/enc-string.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/key-management/crypto/models/enc-string.ts b/libs/common/src/key-management/crypto/models/enc-string.ts index 1ff98d1b6b6..3478ced0cf3 100644 --- a/libs/common/src/key-management/crypto/models/enc-string.ts +++ b/libs/common/src/key-management/crypto/models/enc-string.ts @@ -153,6 +153,10 @@ export class EncString implements Encrypted { return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length; } + /** + * @deprecated - This function is deprecated. Use EncryptService.decryptString instead. + * @returns - The decrypted string, or `[error: cannot decrypt]` if decryption fails. + */ async decrypt( orgId: string | null, key: SymmetricCryptoKey | null = null, From 481910b82374631454a173816f4b24ba2336437d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 22 Jul 2025 13:03:04 +0200 Subject: [PATCH 007/179] Fix breaking sdk change and update to 231 (#15617) --- .../src/platform/services/sdk/default-sdk.service.ts | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 1dfdfd207c0..c12fbe2dbb2 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -228,6 +228,7 @@ export class DefaultSdkService implements SdkService { }, privateKey, signingKey: undefined, + securityState: undefined, }); // We initialize the org crypto even if the org_keys are diff --git a/package-lock.json b/package-lock.json index b9f661f6915..e44797997f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.227", + "@bitwarden/sdk-internal": "0.2.0-main.231", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4604,9 +4604,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.227", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.227.tgz", - "integrity": "sha512-afOsl9jwi1qyX/tF4bYP3EWXgc8oMgnCA0hPPh+AJpn7GgoAPCi+WXaJkbBPwRpxZFKEpwt3oLRNTvtkECvFJw==", + "version": "0.2.0-main.231", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.231.tgz", + "integrity": "sha512-fDKB/RFVvkRPWlhL/qhPAdJDjD1EpFjpEjjpY0v5QNGalh6NCztOr1OcMc4kvipPp4g+epZjs3SPN38K6R+7zw==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index ac00e674310..089ef3342e9 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.227", + "@bitwarden/sdk-internal": "0.2.0-main.231", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From 2a07b952ef5c633ea4b470f64039cb13c02c0aa4 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 22 Jul 2025 06:32:00 -0700 Subject: [PATCH 008/179] [PM-24000] Convert string date values to Date objects for CipherExport types (#15715) --- libs/common/src/models/export/cipher.export.ts | 7 ++++--- libs/common/src/models/export/password-history.export.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index ad6d8b7a609..d343621328c 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -53,6 +53,7 @@ export class CipherExport { view.notes = req.notes; view.favorite = req.favorite; view.reprompt = req.reprompt ?? CipherRepromptType.None; + view.key = req.key != null ? new EncString(req.key) : null; if (req.fields != null) { view.fields = req.fields.map((f) => FieldExport.toView(f)); @@ -80,9 +81,9 @@ export class CipherExport { view.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toView(ph)); } - view.creationDate = req.creationDate; - view.revisionDate = req.revisionDate; - view.deletedDate = req.deletedDate; + view.creationDate = req.creationDate ? new Date(req.creationDate) : null; + view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null; + view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null; return view; } diff --git a/libs/common/src/models/export/password-history.export.ts b/libs/common/src/models/export/password-history.export.ts index e5a44e4e330..f443a2f4ace 100644 --- a/libs/common/src/models/export/password-history.export.ts +++ b/libs/common/src/models/export/password-history.export.ts @@ -16,7 +16,7 @@ export class PasswordHistoryExport { static toView(req: PasswordHistoryExport, view = new PasswordHistoryView()) { view.password = req.password; - view.lastUsedDate = req.lastUsedDate; + view.lastUsedDate = req.lastUsedDate ? new Date(req.lastUsedDate) : null; return view; } From 5290e0a63beb5ec5f134c996c7e25e3814cbce4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 22 Jul 2025 09:33:34 -0400 Subject: [PATCH 009/179] [PM-19054] configure send with email otp authentication via cli (#15360) --- apps/cli/src/commands/get.command.ts | 10 +- .../src/tools/send/commands/create.command.ts | 10 +- .../src/tools/send/commands/edit.command.ts | 24 ++- apps/cli/src/tools/send/commands/index.ts | 1 + .../tools/send/commands/template.command.ts | 35 ++++ .../src/tools/send/models/send.response.ts | 3 + apps/cli/src/tools/send/send.program.ts | 64 +++--- apps/cli/src/tools/send/util.spec.ts | 194 ++++++++++++++++++ apps/cli/src/tools/send/util.ts | 55 +++++ .../src/tools/send/models/data/send.data.ts | 2 + .../src/tools/send/models/domain/send.spec.ts | 6 +- .../src/tools/send/models/domain/send.ts | 2 + .../tools/send/models/request/send.request.ts | 2 + .../send/models/response/send.response.ts | 2 + .../src/tools/send/models/view/send.view.ts | 1 + .../src/tools/send/services/send.service.ts | 7 +- 16 files changed, 369 insertions(+), 49 deletions(-) create mode 100644 apps/cli/src/tools/send/commands/template.command.ts create mode 100644 apps/cli/src/tools/send/util.spec.ts create mode 100644 apps/cli/src/tools/send/util.ts diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index aa2db7c81ab..b20052fbb53 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -25,7 +25,6 @@ import { LoginExport } from "@bitwarden/common/models/export/login.export"; import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -44,7 +43,6 @@ import { SelectionReadOnly } from "../admin-console/models/selection-read-only"; import { Response } from "../models/response"; import { StringResponse } from "../models/response/string.response"; import { TemplateResponse } from "../models/response/template.response"; -import { SendResponse } from "../tools/send/models/send.response"; import { CliUtils } from "../utils"; import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; @@ -577,11 +575,11 @@ export class GetCommand extends DownloadCommand { case "org-collection": template = OrganizationCollectionRequest.template(); break; - case "send.text": - template = SendResponse.template(SendType.Text); - break; case "send.file": - template = SendResponse.template(SendType.File); + case "send.text": + template = Response.badRequest( + `Invalid template object. Use \`bw send template ${id}\` instead.`, + ); break; default: return Response.badRequest("Unknown template object."); diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts index a9264c50126..d4f544d39b7 100644 --- a/apps/cli/src/tools/send/commands/create.command.ts +++ b/apps/cli/src/tools/send/commands/create.command.ts @@ -76,9 +76,14 @@ export class SendCreateCommand { const filePath = req.file?.fileName ?? options.file; const text = req.text?.text ?? options.text; const hidden = req.text?.hidden ?? options.hidden; - const password = req.password ?? options.password; + const password = req.password ?? options.password ?? undefined; + const emails = req.emails ?? options.emails ?? undefined; const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount; + if (emails !== undefined && password !== undefined) { + return Response.badRequest("--password and --emails are mutually exclusive."); + } + req.key = null; req.maxAccessCount = maxAccessCount; @@ -133,6 +138,7 @@ export class SendCreateCommand { // Add dates from template encSend.deletionDate = sendView.deletionDate; encSend.expirationDate = sendView.expirationDate; + encSend.emails = emails && emails.join(","); await this.sendApiService.save([encSend, fileData]); const newSend = await this.sendService.getFromState(encSend.id); @@ -151,12 +157,14 @@ class Options { text: string; maxAccessCount: number; password: string; + emails: Array; hidden: boolean; constructor(passedOptions: Record) { this.file = passedOptions?.file; this.text = passedOptions?.text; this.password = passedOptions?.password; + this.emails = passedOptions?.email; this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden); this.maxAccessCount = passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null; diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts index ed719b58311..09f89041cc5 100644 --- a/apps/cli/src/tools/send/commands/edit.command.ts +++ b/apps/cli/src/tools/send/commands/edit.command.ts @@ -50,11 +50,21 @@ export class SendEditCommand { const normalizedOptions = new Options(cmdOptions); req.id = normalizedOptions.itemId || req.id; - - if (req.id != null) { - req.id = req.id.toLowerCase(); + if (normalizedOptions.emails) { + req.emails = normalizedOptions.emails; + req.password = undefined; + } else if (normalizedOptions.password) { + req.emails = undefined; + req.password = normalizedOptions.password; + } else if (req.password && (typeof req.password !== "string" || req.password === "")) { + req.password = undefined; } + if (!req.id) { + return Response.error("`itemid` was not provided."); + } + + req.id = req.id.toLowerCase(); const send = await this.sendService.getFromState(req.id); if (send == null) { @@ -76,10 +86,6 @@ export class SendEditCommand { let sendView = await send.decrypt(); sendView = SendResponse.toView(req, sendView); - if (typeof req.password !== "string" || req.password === "") { - req.password = null; - } - try { const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password); // Add dates from template @@ -97,8 +103,12 @@ export class SendEditCommand { class Options { itemId: string; + password: string; + emails: string[]; constructor(passedOptions: Record) { this.itemId = passedOptions?.itemId || passedOptions?.itemid; + this.password = passedOptions.password; + this.emails = passedOptions.email; } } diff --git a/apps/cli/src/tools/send/commands/index.ts b/apps/cli/src/tools/send/commands/index.ts index 645f5c0d1db..452c228dd9b 100644 --- a/apps/cli/src/tools/send/commands/index.ts +++ b/apps/cli/src/tools/send/commands/index.ts @@ -5,3 +5,4 @@ export * from "./get.command"; export * from "./list.command"; export * from "./receive.command"; export * from "./remove-password.command"; +export * from "./template.command"; diff --git a/apps/cli/src/tools/send/commands/template.command.ts b/apps/cli/src/tools/send/commands/template.command.ts new file mode 100644 index 00000000000..c1c2c97b03d --- /dev/null +++ b/apps/cli/src/tools/send/commands/template.command.ts @@ -0,0 +1,35 @@ +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; + +import { Response } from "../../../models/response"; +import { TemplateResponse } from "../../../models/response/template.response"; +import { SendResponse } from "../models/send.response"; + +export class SendTemplateCommand { + constructor() {} + + run(type: string): Response { + let template: SendResponse | undefined; + let response: Response; + + switch (type) { + case "send.text": + case "text": + template = SendResponse.template(SendType.Text); + break; + case "send.file": + case "file": + template = SendResponse.template(SendType.File); + break; + default: + response = Response.badRequest("Unknown template object."); + } + + if (template) { + response = Response.success(new TemplateResponse(template)); + } + + response ??= Response.badRequest("An error occurred while retrieving the template."); + + return response; + } +} diff --git a/apps/cli/src/tools/send/models/send.response.ts b/apps/cli/src/tools/send/models/send.response.ts index 4d680b5c0a1..a0c1d3f83c6 100644 --- a/apps/cli/src/tools/send/models/send.response.ts +++ b/apps/cli/src/tools/send/models/send.response.ts @@ -26,6 +26,7 @@ export class SendResponse implements BaseResponse { req.deletionDate = this.getStandardDeletionDate(deleteInDays); req.expirationDate = null; req.password = null; + req.emails = null; req.disabled = false; req.hideEmail = false; return req; @@ -50,6 +51,7 @@ export class SendResponse implements BaseResponse { view.deletionDate = send.deletionDate; view.expirationDate = send.expirationDate; view.password = send.password; + view.emails = send.emails ?? []; view.disabled = send.disabled; view.hideEmail = send.hideEmail; return view; @@ -87,6 +89,7 @@ export class SendResponse implements BaseResponse { expirationDate: Date; password: string; passwordSet: boolean; + emails?: Array; disabled: boolean; hideEmail: boolean; diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index cbeda188a99..650f448e558 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -4,13 +4,12 @@ import * as fs from "fs"; import * as path from "path"; import * as chalk from "chalk"; -import { program, Command, OptionValues } from "commander"; +import { program, Command, Option, OptionValues } from "commander"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { BaseProgram } from "../../base-program"; -import { GetCommand } from "../../commands/get.command"; import { Response } from "../../models/response"; import { CliUtils } from "../../utils"; @@ -22,10 +21,12 @@ import { SendListCommand, SendReceiveCommand, SendRemovePasswordCommand, + SendTemplateCommand, } from "./commands"; import { SendFileResponse } from "./models/send-file.response"; import { SendTextResponse } from "./models/send-text.response"; import { SendResponse } from "./models/send.response"; +import { parseEmail } from "./util"; const writeLn = CliUtils.writeLn; @@ -48,6 +49,17 @@ export class SendProgram extends BaseProgram { "The number of days in the future to set deletion date, defaults to 7", "7", ) + .addOption( + new Option( + "--password ", + "optional password to access this Send. Can also be specified in JSON.", + ).conflicts("email"), + ) + .option( + "--email ", + "optional emails to access this Send. Can also be specified in JSON.", + parseEmail, + ) .option("-a, --maxAccessCount ", "The amount of max possible accesses.") .option("--hidden", "Hide in web by default. Valid only if --file is not set.") .option( @@ -139,26 +151,9 @@ export class SendProgram extends BaseProgram { return new Command("template") .argument("", "Valid objects are: send.text, send.file") .description("Get json templates for send objects") - .action(async (object) => { - const cmd = new GetCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.collectionService, - this.serviceContainer.totpService, - this.serviceContainer.auditService, - this.serviceContainer.keyService, - this.serviceContainer.encryptService, - this.serviceContainer.searchService, - this.serviceContainer.apiService, - this.serviceContainer.organizationService, - this.serviceContainer.eventCollectionService, - this.serviceContainer.billingAccountProfileStateService, - this.serviceContainer.accountService, - this.serviceContainer.cliRestrictedItemTypesService, - ); - const response = await cmd.run("template", object, null); - this.processResponse(response); - }); + .action((options: OptionValues) => + this.processResponse(new SendTemplateCommand().run(options.object)), + ); } private getCommand(): Command { @@ -208,10 +203,6 @@ export class SendProgram extends BaseProgram { .option("--file ", "file to Send. Can also be specified in parent's JSON.") .option("--text ", "text to Send. Can also be specified in parent's JSON.") .option("--hidden", "text hidden flag. Valid only with the --text option.") - .option( - "--password ", - "optional password to access this Send. Can also be specified in JSON", - ) .on("--help", () => { writeLn(""); writeLn("Note:"); @@ -219,13 +210,13 @@ export class SendProgram extends BaseProgram { writeLn("", true); }) .action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => { - // Work-around to support `--fullObject` option for `send create --fullObject` - // Calling `option('--fullObject', ...)` above won't work due to Commander doesn't like same option - // to be defind on both parent-command and sub-command - const { fullObject = false } = args.parent.opts(); + // subcommands inherit flags from their parent; they cannot override them + const { fullObject = false, email = undefined, password = undefined } = args.parent.opts(); const mergedOptions = { ...options, fullObject: fullObject, + email, + password, }; const response = await this.runCreate(encodedJson, mergedOptions); @@ -247,7 +238,7 @@ export class SendProgram extends BaseProgram { writeLn(" You cannot update a File-type Send's file. Just delete and remake it"); writeLn("", true); }) - .action(async (encodedJson: string, options: OptionValues) => { + .action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => { await this.exitIfLocked(); const getCmd = new SendGetCommand( this.serviceContainer.sendService, @@ -264,7 +255,16 @@ export class SendProgram extends BaseProgram { this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, ); - const response = await cmd.run(encodedJson, options); + + // subcommands inherit flags from their parent; they cannot override them + const { email = undefined, password = undefined } = args.parent.opts(); + const mergedOptions = { + ...options, + email, + password, + }; + + const response = await cmd.run(encodedJson, mergedOptions); this.processResponse(response); }); } diff --git a/apps/cli/src/tools/send/util.spec.ts b/apps/cli/src/tools/send/util.spec.ts new file mode 100644 index 00000000000..2cfc2a1b4c8 --- /dev/null +++ b/apps/cli/src/tools/send/util.spec.ts @@ -0,0 +1,194 @@ +import { parseEmail } from "./util"; + +describe("parseEmail", () => { + describe("single email address parsing", () => { + it("should parse a valid single email address", () => { + const result = parseEmail("test@example.com", []); + expect(result).toEqual(["test@example.com"]); + }); + + it("should parse email with dots in local part", () => { + const result = parseEmail("test.user@example.com", []); + expect(result).toEqual(["test.user@example.com"]); + }); + + it("should parse email with underscores and hyphens", () => { + const result = parseEmail("test_user-name@example.com", []); + expect(result).toEqual(["test_user-name@example.com"]); + }); + + it("should parse email with plus sign", () => { + const result = parseEmail("test+user@example.com", []); + expect(result).toEqual(["test+user@example.com"]); + }); + + it("should parse email with dots and hyphens in domain", () => { + const result = parseEmail("user@test-domain.co.uk", []); + expect(result).toEqual(["user@test-domain.co.uk"]); + }); + + it("should add single email to existing previousInput array", () => { + const result = parseEmail("new@example.com", ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new@example.com"]); + }); + }); + + describe("comma-separated email lists", () => { + it("should parse comma-separated email list", () => { + const result = parseEmail("test@example.com,user@domain.com", []); + expect(result).toEqual(["test@example.com", "user@domain.com"]); + }); + + it("should parse comma-separated emails with spaces", () => { + const result = parseEmail("test@example.com, user@domain.com, admin@site.org", []); + expect(result).toEqual(["test@example.com", "user@domain.com", "admin@site.org"]); + }); + + it("should combine comma-separated emails with previousInput", () => { + const result = parseEmail("new1@example.com,new2@domain.com", ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new1@example.com", "new2@domain.com"]); + }); + + it("should throw error for invalid email in comma-separated list", () => { + expect(() => { + parseEmail("valid@example.com,invalid-email,another@domain.com", []); + }).toThrow("Invalid email address: invalid-email"); + }); + }); + + describe("space-separated email lists", () => { + it("should parse space-separated email list", () => { + const result = parseEmail("test@example.com user@domain.com", []); + expect(result).toEqual(["test@example.com", "user@domain.com"]); + }); + + it("should parse space-separated emails with multiple spaces", () => { + const result = parseEmail("test@example.com user@domain.com admin@site.org", []); + expect(result).toEqual(["test@example.com", "user@domain.com", "admin@site.org"]); + }); + + it("should combine space-separated emails with previousInput", () => { + const result = parseEmail("new1@example.com new2@domain.com", ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new1@example.com", "new2@domain.com"]); + }); + + it("should throw error for invalid email in space-separated list", () => { + expect(() => { + parseEmail("valid@example.com invalid-email another@domain.com", []); + }).toThrow("Invalid email address: invalid-email"); + }); + }); + + describe("JSON array input format", () => { + it("should parse valid JSON array of emails", () => { + const result = parseEmail('["test@example.com", "user@domain.com"]', []); + expect(result).toEqual(["test@example.com", "user@domain.com"]); + }); + + it("should parse single email in JSON array", () => { + const result = parseEmail('["test@example.com"]', []); + expect(result).toEqual(["test@example.com"]); + }); + + it("should parse empty JSON array", () => { + const result = parseEmail("[]", []); + expect(result).toEqual([]); + }); + + it("should combine JSON array with previousInput", () => { + const result = parseEmail('["new1@example.com", "new2@domain.com"]', ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new1@example.com", "new2@domain.com"]); + }); + + it("should throw error for malformed JSON", () => { + expect(() => { + parseEmail('["test@example.com", "user@domain.com"', []); + }).toThrow(); + }); + + it("should throw error for JSON that is not an array", () => { + expect(() => { + parseEmail('{"email": "test@example.com"}', []); + }).toThrow("Invalid email address:"); + }); + + it("should throw error for JSON string instead of array", () => { + expect(() => { + parseEmail('"test@example.com"', []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for JSON number instead of array", () => { + expect(() => { + parseEmail("123", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + }); + + describe("`previousInput` parameter handling", () => { + it("should handle undefined previousInput", () => { + const result = parseEmail("test@example.com", undefined as any); + expect(result).toEqual(["test@example.com"]); + }); + + it("should handle null previousInput", () => { + const result = parseEmail("test@example.com", null as any); + expect(result).toEqual(["test@example.com"]); + }); + + it("should preserve existing emails in previousInput", () => { + const existing = ["existing1@test.com", "existing2@test.com"]; + const result = parseEmail("new@example.com", existing); + expect(result).toEqual(["existing1@test.com", "existing2@test.com", "new@example.com"]); + }); + }); + + describe("error cases and edge conditions", () => { + it("should throw error for empty string input", () => { + expect(() => { + parseEmail("", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should return empty array for whitespace-only input", () => { + const result = parseEmail(" ", []); + expect(result).toEqual([]); + }); + + it("should throw error for invalid single email", () => { + expect(() => { + parseEmail("invalid-email", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for email without @ symbol", () => { + expect(() => { + parseEmail("testexample.com", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for email without domain", () => { + expect(() => { + parseEmail("test@", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for email without local part", () => { + expect(() => { + parseEmail("@example.com", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for input that looks like file path", () => { + expect(() => { + parseEmail("/path/to/file.txt", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for input that looks like URL", () => { + expect(() => { + parseEmail("https://example.com", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + }); +}); diff --git a/apps/cli/src/tools/send/util.ts b/apps/cli/src/tools/send/util.ts new file mode 100644 index 00000000000..bf66f916bbb --- /dev/null +++ b/apps/cli/src/tools/send/util.ts @@ -0,0 +1,55 @@ +/** + * Parses email addresses from various input formats and combines them with previously parsed emails. + * + * Supports: single email, JSON array, comma-separated, or space-separated lists. + * Note: Function signature follows Commander.js option parsing pattern. + * + * @param input - Email input string in any supported format + * @param previousInput - Previously parsed email addresses to append to + * @returns Combined array of email addresses + * @throws {Error} For invalid JSON, non-array JSON, invalid email addresses, or unrecognized format + * + * @example + * parseEmail("user@example.com", []) // ["user@example.com"] + * parseEmail('["user1@example.com", "user2@example.com"]', []) // ["user1@example.com", "user2@example.com"] + * parseEmail("user1@example.com, user2@example.com", []) // ["user1@example.com", "user2@example.com"] + */ +export function parseEmail(input: string, previousInput: string[]) { + let result = previousInput ?? []; + + if (isEmail(input)) { + result.push(input); + } else if (input.startsWith("[")) { + const json = JSON.parse(input); + if (!Array.isArray(json)) { + throw new Error("invalid JSON"); + } + + result = result.concat(json); + } else if (input.includes(",")) { + result = result.concat(parseList(input, ",")); + } else if (input.includes(" ")) { + result = result.concat(parseList(input, " ")); + } else { + throw new Error("`input` must be a single address, a comma-separated list, or a JSON array"); + } + + return result; +} + +function isEmail(input: string) { + return !!input && !!input.match(/^([\w._+-]+?)@([\w._+-]+?)$/); +} + +function parseList(value: string, separator: string) { + const parts = value + .split(separator) + .map((v) => v.trim()) + .filter((v) => !!v.length); + const invalid = parts.find((v) => !isEmail(v)); + if (invalid) { + throw new Error(`Invalid email address: ${invalid}`); + } + + return parts; +} diff --git a/libs/common/src/tools/send/models/data/send.data.ts b/libs/common/src/tools/send/models/data/send.data.ts index e4df5e48dce..2c6377de0c9 100644 --- a/libs/common/src/tools/send/models/data/send.data.ts +++ b/libs/common/src/tools/send/models/data/send.data.ts @@ -21,6 +21,7 @@ export class SendData { expirationDate: string; deletionDate: string; password: string; + emails: string; disabled: boolean; hideEmail: boolean; @@ -41,6 +42,7 @@ export class SendData { this.expirationDate = response.expirationDate; this.deletionDate = response.deletionDate; this.password = response.password; + this.emails = response.emails; this.disabled = response.disable; this.hideEmail = response.hideEmail; diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 8df9a144108..e9b0ae7b3b8 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -29,14 +29,15 @@ describe("Send", () => { text: "encText", hidden: true, }, - file: null, + file: null!, key: "encKey", - maxAccessCount: null, + maxAccessCount: null!, accessCount: 10, revisionDate: "2022-01-31T12:00:00.000Z", expirationDate: "2022-01-31T12:00:00.000Z", deletionDate: "2022-01-31T12:00:00.000Z", password: "password", + emails: null!, disabled: false, hideEmail: true, }; @@ -86,6 +87,7 @@ describe("Send", () => { expirationDate: new Date("2022-01-31T12:00:00.000Z"), deletionDate: new Date("2022-01-31T12:00:00.000Z"), password: "password", + emails: null!, disabled: false, hideEmail: true, }); diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 89fe92c2c7b..48057aedd2d 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -27,6 +27,7 @@ export class Send extends Domain { expirationDate: Date; deletionDate: Date; password: string; + emails: string; disabled: boolean; hideEmail: boolean; @@ -53,6 +54,7 @@ export class Send extends Domain { this.maxAccessCount = obj.maxAccessCount; this.accessCount = obj.accessCount; this.password = obj.password; + this.emails = obj.emails; this.disabled = obj.disabled; this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null; diff --git a/libs/common/src/tools/send/models/request/send.request.ts b/libs/common/src/tools/send/models/request/send.request.ts index 9e4f1e14837..f7e3ff26d7f 100644 --- a/libs/common/src/tools/send/models/request/send.request.ts +++ b/libs/common/src/tools/send/models/request/send.request.ts @@ -17,6 +17,7 @@ export class SendRequest { text: SendTextApi; file: SendFileApi; password: string; + emails: string; disabled: boolean; hideEmail: boolean; @@ -30,6 +31,7 @@ export class SendRequest { this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null; this.key = send.key != null ? send.key.encryptedString : null; this.password = send.password; + this.emails = send.emails; this.disabled = send.disabled; this.hideEmail = send.hideEmail; diff --git a/libs/common/src/tools/send/models/response/send.response.ts b/libs/common/src/tools/send/models/response/send.response.ts index 76550f5cdfd..5c6bd4dc1a6 100644 --- a/libs/common/src/tools/send/models/response/send.response.ts +++ b/libs/common/src/tools/send/models/response/send.response.ts @@ -20,6 +20,7 @@ export class SendResponse extends BaseResponse { expirationDate: string; deletionDate: string; password: string; + emails: string; disable: boolean; hideEmail: boolean; @@ -37,6 +38,7 @@ export class SendResponse extends BaseResponse { this.expirationDate = this.getResponseProperty("ExpirationDate"); this.deletionDate = this.getResponseProperty("DeletionDate"); this.password = this.getResponseProperty("Password"); + this.emails = this.getResponseProperty("Emails"); this.disable = this.getResponseProperty("Disabled") || false; this.hideEmail = this.getResponseProperty("HideEmail") || false; diff --git a/libs/common/src/tools/send/models/view/send.view.ts b/libs/common/src/tools/send/models/view/send.view.ts index 2c269892a6f..54657b12438 100644 --- a/libs/common/src/tools/send/models/view/send.view.ts +++ b/libs/common/src/tools/send/models/view/send.view.ts @@ -26,6 +26,7 @@ export class SendView implements View { deletionDate: Date = null; expirationDate: Date = null; password: string = null; + emails: string[] = []; disabled = false; hideEmail = false; diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 57463b3b42b..6e2b4391c96 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -74,7 +74,12 @@ export class SendService implements InternalSendServiceAbstraction { model.key = key.material; model.cryptoKey = key.derivedKey; } - if (password != null) { + + const hasEmails = (model.emails?.length ?? 0) > 0; + if (hasEmails) { + send.emails = model.emails.join(","); + send.password = null; + } else if (password != null) { // Note: Despite being called key, the passwordKey is not used for encryption. // It is used as a static proof that the client knows the password, and has the encryption key. const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( From 96f31aac3adcd3abcc917c523bcb7f671ab76ffa Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:58:17 +0100 Subject: [PATCH 010/179] [PM 18701]Optional payment modal after signup (#15384) * Implement the planservice * Add the pricing component and service * Add the change plan type service * resolve the unit test issues * Move the changeSubscriptionFrequency endpoint * Rename planservice to plancardservice * Remove unused and correct typos * Resolve the double asignment * resolve the unit test failing * Remove default payment setting to card * remove unnecessary check * Property initialPaymentMethod has no initializer * move the logic to service * Move estimate tax to pricing service * Refactor thr pricing summary component * Resolve the lint unit test error * Add changes for auto modal * Remove custom role for sm * Resolve the blank member page issue * Changes on the pricing display --- .../members/members.component.html | 5 + .../members/members.component.ts | 20 + .../organizations/members/members.module.ts | 2 + .../organization-payment-method.component.ts | 12 +- .../app/billing/services/plan-card.service.ts | 59 +++ .../services/pricing-summary.service.ts | 155 ++++++++ .../billing/shared/billing-shared.module.ts | 6 + .../shared/plan-card/plan-card.component.html | 45 +++ .../shared/plan-card/plan-card.component.ts | 68 ++++ .../pricing-summary.component.html | 259 +++++++++++++ .../pricing-summary.component.ts | 48 +++ .../trial-payment-dialog.component.html | 117 ++++++ .../trial-payment-dialog.component.ts | 365 ++++++++++++++++++ ...ganization-free-trial-warning.component.ts | 20 +- .../services/organization-warnings.service.ts | 34 +- apps/web/src/locales/en/messages.json | 29 +- ...ization-billing-api.service.abstraction.ts | 6 + .../request/change-plan-frequency.request.ts | 9 + .../organization-billing-api.service.ts | 14 + 19 files changed, 1264 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/app/billing/services/plan-card.service.ts create mode 100644 apps/web/src/app/billing/services/pricing-summary.service.ts create mode 100644 apps/web/src/app/billing/shared/plan-card/plan-card.component.html create mode 100644 apps/web/src/app/billing/shared/plan-card/plan-card.component.ts create mode 100644 apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html create mode 100644 apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts create mode 100644 apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html create mode 100644 apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts create mode 100644 libs/common/src/billing/models/request/change-plan-frequency.request.ts diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 962191021e8..49946806efc 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -1,3 +1,8 @@ + + protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, private configService: ConfigService, private organizationUserService: OrganizationUserService, + private organizationWarningsService: OrganizationWarningsService, ) { super( apiService, @@ -247,6 +250,13 @@ export class MembersComponent extends BaseMembersComponent this.showUserManagementControls$ = organization$.pipe( map((organization) => organization.canManageUsers), ); + organization$ + .pipe( + takeUntilDestroyed(), + tap((org) => (this.organization = org)), + switchMap((org) => this.organizationWarningsService.showInactiveSubscriptionDialog$(org)), + ) + .subscribe(); } async getUsers(): Promise { @@ -932,4 +942,14 @@ export class MembersComponent extends BaseMembersComponent .getCheckedUsers() .every((member) => member.managedByOrganization && validStatuses.includes(member.status)); } + + async navigateToPaymentMethod() { + const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( + FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, + ); + const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; + await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], { + state: { launchPaymentModalAutomatically: true }, + }); + } } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 98431758d2f..5f626d44161 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -5,6 +5,7 @@ import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-s import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { ScrollLayoutDirective } from "@bitwarden/components"; +import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components"; import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; @@ -29,6 +30,7 @@ import { MembersComponent } from "./members.component"; ScrollingModule, PasswordStrengthV2Component, ScrollLayoutDirective, + OrganizationFreeTrialWarningComponent, ], declarations: [ BulkConfirmDialogComponent, diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 9b144fe59a7..aa7bf5e5d11 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -36,6 +36,10 @@ import { AdjustPaymentDialogComponent, AdjustPaymentDialogResultType, } from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component"; +import { + TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, + TrialPaymentDialogComponent, +} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; import { FreeTrial } from "../../types/free-trial"; @Component({ @@ -212,15 +216,15 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { }; changePayment = async () => { - const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { + const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { data: { - initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, - productTier: this.organization?.productTierType, + subscription: this.organizationSubscriptionResponse, + productTierType: this.organization?.productTierType, }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustPaymentDialogResultType.Submitted) { + if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); diff --git a/apps/web/src/app/billing/services/plan-card.service.ts b/apps/web/src/app/billing/services/plan-card.service.ts new file mode 100644 index 00000000000..25974a428fd --- /dev/null +++ b/apps/web/src/app/billing/services/plan-card.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; + +@Injectable({ providedIn: "root" }) +export class PlanCardService { + constructor(private apiService: ApiService) {} + + async getCadenceCards( + currentPlan: PlanResponse, + subscription: OrganizationSubscriptionResponse, + isSecretsManagerTrial: boolean, + ) { + const plans = await this.apiService.getPlans(); + + const filteredPlans = plans.data.filter((plan) => !!plan.PasswordManager); + + const result = + filteredPlans?.filter( + (plan) => + plan.productTier === currentPlan.productTier && !plan.disabled && !plan.legacyYear, + ) || []; + + const planCards = result.map((plan) => { + let costPerMember = 0; + + if (plan.PasswordManager.basePrice) { + costPerMember = plan.isAnnual + ? plan.PasswordManager.basePrice / 12 + : plan.PasswordManager.basePrice; + } else if (!plan.PasswordManager.basePrice && plan.PasswordManager.hasAdditionalSeatsOption) { + const secretsManagerCost = subscription.useSecretsManager + ? plan.SecretsManager.seatPrice + : 0; + const passwordManagerCost = isSecretsManagerTrial ? 0 : plan.PasswordManager.seatPrice; + costPerMember = (secretsManagerCost + passwordManagerCost) / (plan.isAnnual ? 12 : 1); + } + + const percentOff = subscription.customerDiscount?.percentOff ?? 0; + + const discount = + (percentOff === 0 && plan.isAnnual) || isSecretsManagerTrial ? 20 : percentOff; + + return { + title: plan.isAnnual ? "Annually" : "Monthly", + costPerMember, + discount, + isDisabled: false, + isSelected: plan.isAnnual, + isAnnual: plan.isAnnual, + productTier: plan.productTier, + }; + }); + + return planCards.reverse(); + } +} diff --git a/apps/web/src/app/billing/services/pricing-summary.service.ts b/apps/web/src/app/billing/services/pricing-summary.service.ts new file mode 100644 index 00000000000..0b048b379d8 --- /dev/null +++ b/apps/web/src/app/billing/services/pricing-summary.service.ts @@ -0,0 +1,155 @@ +import { Injectable } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information"; +import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; + +import { PricingSummaryData } from "../shared/pricing-summary/pricing-summary.component"; + +@Injectable({ + providedIn: "root", +}) +export class PricingSummaryService { + private estimatedTax: number = 0; + + constructor(private taxService: TaxServiceAbstraction) {} + + async getPricingSummaryData( + plan: PlanResponse, + sub: OrganizationSubscriptionResponse, + organization: Organization, + selectedInterval: PlanInterval, + taxInformation: TaxInformation, + isSecretsManagerTrial: boolean, + ): Promise { + // Calculation helpers + const passwordManagerSeatTotal = + plan.PasswordManager?.hasAdditionalSeatsOption && !isSecretsManagerTrial + ? plan.PasswordManager.seatPrice * Math.abs(sub?.seats || 0) + : 0; + + const secretsManagerSeatTotal = plan.SecretsManager?.hasAdditionalSeatsOption + ? plan.SecretsManager.seatPrice * Math.abs(sub?.smSeats || 0) + : 0; + + const additionalServiceAccount = this.getAdditionalServiceAccount(plan, sub); + + const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption + ? plan.PasswordManager.additionalStoragePricePerGb * + (sub?.maxStorageGb ? sub.maxStorageGb - 1 : 0) + : 0; + + const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0; + + const additionalServiceAccountTotal = + plan.SecretsManager?.hasAdditionalServiceAccountOption && additionalServiceAccount > 0 + ? plan.SecretsManager.additionalPricePerServiceAccount * additionalServiceAccount + : 0; + + let passwordManagerSubtotal = plan.PasswordManager?.basePrice || 0; + if (plan.PasswordManager?.hasAdditionalSeatsOption) { + passwordManagerSubtotal += passwordManagerSeatTotal; + } + if (plan.PasswordManager?.hasPremiumAccessOption) { + passwordManagerSubtotal += plan.PasswordManager.premiumAccessOptionPrice; + } + + const secretsManagerSubtotal = plan.SecretsManager + ? (plan.SecretsManager.basePrice || 0) + + secretsManagerSeatTotal + + additionalServiceAccountTotal + : 0; + + const totalAppliedDiscount = 0; + const discountPercentageFromSub = isSecretsManagerTrial + ? 0 + : (sub?.customerDiscount?.percentOff ?? 0); + const discountPercentage = 20; + const acceptingSponsorship = false; + const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0; + + this.estimatedTax = await this.getEstimatedTax(organization, plan, sub, taxInformation); + + const total = organization?.useSecretsManager + ? passwordManagerSubtotal + + additionalStorageTotal + + secretsManagerSubtotal + + this.estimatedTax + : passwordManagerSubtotal + additionalStorageTotal + this.estimatedTax; + + return { + selectedPlanInterval: selectedInterval === PlanInterval.Annually ? "year" : "month", + passwordManagerSeats: + plan.productTier === ProductTierType.Families ? plan.PasswordManager.baseSeats : sub?.seats, + passwordManagerSeatTotal, + secretsManagerSeatTotal, + additionalStorageTotal, + additionalStoragePriceMonthly, + additionalServiceAccountTotal, + totalAppliedDiscount, + secretsManagerSubtotal, + passwordManagerSubtotal, + total, + organization, + sub, + selectedPlan: plan, + selectedInterval, + discountPercentageFromSub, + discountPercentage, + acceptingSponsorship, + additionalServiceAccount, + storageGb, + isSecretsManagerTrial, + estimatedTax: this.estimatedTax, + }; + } + + async getEstimatedTax( + organization: Organization, + currentPlan: PlanResponse, + sub: OrganizationSubscriptionResponse, + taxInformation: TaxInformation, + ) { + if (!taxInformation || !taxInformation.country || !taxInformation.postalCode) { + return 0; + } + + const request: PreviewOrganizationInvoiceRequest = { + organizationId: organization.id, + passwordManager: { + additionalStorage: 0, + plan: currentPlan?.type, + seats: sub.seats, + }, + taxInformation: { + postalCode: taxInformation.postalCode, + country: taxInformation.country, + taxId: taxInformation.taxId, + }, + }; + + if (organization.useSecretsManager) { + request.secretsManager = { + seats: sub.smSeats ?? 0, + additionalMachineAccounts: + (sub.smServiceAccounts ?? 0) - (sub.plan.SecretsManager?.baseServiceAccount ?? 0), + }; + } + const invoiceResponse = await this.taxService.previewOrganizationInvoice(request); + return invoiceResponse.taxAmount; + } + + getAdditionalServiceAccount(plan: PlanResponse, sub: OrganizationSubscriptionResponse): number { + if (!plan || !plan.SecretsManager) { + return 0; + } + const baseServiceAccount = plan.SecretsManager?.baseServiceAccount || 0; + const usedServiceAccounts = sub?.smServiceAccounts || 0; + const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts; + return additionalServiceAccounts <= 0 ? Math.abs(additionalServiceAccounts) : 0; + } +} diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 9a69755b209..7322f047551 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -12,10 +12,13 @@ import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; import { PaymentComponent } from "./payment/payment.component"; import { PaymentMethodComponent } from "./payment-method.component"; +import { PlanCardComponent } from "./plan-card/plan-card.component"; +import { PricingSummaryComponent } from "./pricing-summary/pricing-summary.component"; import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component"; import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component"; import { TaxInfoComponent } from "./tax-info.component"; +import { TrialPaymentDialogComponent } from "./trial-payment-dialog/trial-payment-dialog.component"; import { UpdateLicenseDialogComponent } from "./update-license-dialog.component"; import { UpdateLicenseComponent } from "./update-license.component"; import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-account.component"; @@ -41,6 +44,9 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac AdjustStorageDialogComponent, IndividualSelfHostingLicenseUploaderComponent, OrganizationSelfHostingLicenseUploaderComponent, + TrialPaymentDialogComponent, + PlanCardComponent, + PricingSummaryComponent, ], exports: [ SharedModule, diff --git a/apps/web/src/app/billing/shared/plan-card/plan-card.component.html b/apps/web/src/app/billing/shared/plan-card/plan-card.component.html new file mode 100644 index 00000000000..08fd3b435f6 --- /dev/null +++ b/apps/web/src/app/billing/shared/plan-card/plan-card.component.html @@ -0,0 +1,45 @@ +@let isFocused = plan().isSelected; +@let isRecommended = plan().isAnnual; + + +
+ @if (isRecommended) { +
+ {{ "recommended" | i18n }} +
+ } +
+

+ {{ plan().title }} + + + {{ "upgradeDiscount" | i18n: plan().discount }} +

+ + {{ plan().costPerMember | currency: "$" }} + /{{ "monthPerMember" | i18n }} + +
+
+
diff --git a/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts b/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts new file mode 100644 index 00000000000..9e3f03a5e7d --- /dev/null +++ b/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts @@ -0,0 +1,68 @@ +import { Component, input, output } from "@angular/core"; + +import { ProductTierType } from "@bitwarden/common/billing/enums"; + +export interface PlanCard { + title: string; + costPerMember: number; + discount?: number; + isDisabled: boolean; + isAnnual: boolean; + isSelected: boolean; + productTier: ProductTierType; +} + +@Component({ + selector: "app-plan-card", + templateUrl: "./plan-card.component.html", + standalone: false, +}) +export class PlanCardComponent { + plan = input.required(); + productTiers = ProductTierType; + + cardClicked = output(); + + getPlanCardContainerClasses(): string[] { + const isSelected = this.plan().isSelected; + const isDisabled = this.plan().isDisabled; + if (isDisabled) { + return [ + "tw-cursor-not-allowed", + "tw-bg-secondary-100", + "tw-font-normal", + "tw-bg-blur", + "tw-text-muted", + "tw-block", + "tw-rounded", + ]; + } + + return isSelected + ? [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-primary-600", + "tw-border-2", + "tw-rounded-lg", + "hover:tw-border-primary-700", + "focus:tw-border-3", + "focus:tw-border-primary-700", + "focus:tw-rounded-lg", + ] + : [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-secondary-300", + "hover:tw-border-text-main", + "focus:tw-border-2", + "focus:tw-border-primary-700", + ]; + } +} diff --git a/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html new file mode 100644 index 00000000000..855b83bdb2d --- /dev/null +++ b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.html @@ -0,0 +1,259 @@ + +
+

+ {{ "total" | i18n }}: + {{ summaryData.total - summaryData.totalAppliedDiscount | currency: "USD" : "$" }} USD + / {{ summaryData.selectedPlanInterval | i18n }} + +

+
+ + + +
+ + + + + + + + + + + + + + +

{{ "passwordManager" | i18n }}

+ + + +

+ + + + {{ summaryData.passwordManagerSeats }} {{ "members" | i18n }} × + {{ + (summaryData.selectedPlan.isAnnual + ? summaryData.selectedPlan.PasswordManager.basePrice / 12 + : summaryData.selectedPlan.PasswordManager.basePrice + ) | currency: "$" + }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ "basePrice" | i18n }}: + {{ summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + + + + {{ + summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" + }} + {{ "freeWithSponsorship" | i18n }} + + + {{ summaryData.selectedPlan.PasswordManager.basePrice | currency: "$" }} + + +

+
+ + + +

+ + {{ "additionalUsers" | i18n }}: + {{ summaryData.passwordManagerSeats || 0 }}  + {{ + "members" | i18n + }} + × + {{ summaryData.selectedPlan.PasswordManager.seatPrice | currency: "$" }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ summaryData.passwordManagerSeatTotal | currency: "$" }} + + + {{ "freeForOneYear" | i18n }} + +

+
+ + + +

+ + {{ summaryData.storageGb }} {{ "additionalStorageGbMessage" | i18n }} + × + {{ summaryData.additionalStoragePriceMonthly | currency: "$" }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + + + {{ summaryData.additionalStorageTotal | currency: "$" }} + + + {{ + summaryData.storageGb * + summaryData.selectedPlan.PasswordManager.additionalStoragePricePerGb + | currency: "$" + }} + + + +

+
+
+
+ + + + +

{{ "secretsManager" | i18n }}

+ + + +

+ + + + {{ summaryData.sub?.smSeats }} {{ "members" | i18n }} × + {{ + (summaryData.selectedPlan.isAnnual + ? summaryData.selectedPlan.SecretsManager.basePrice / 12 + : summaryData.selectedPlan.SecretsManager.basePrice + ) | currency: "$" + }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ "basePrice" | i18n }}: + {{ summaryData.selectedPlan.SecretsManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + + + {{ summaryData.selectedPlan.SecretsManager.basePrice | currency: "$" }} + +

+
+ + + +

+ + {{ "additionalUsers" | i18n }}: + {{ summaryData.sub?.smSeats || 0 }}  + {{ + "members" | i18n + }} + × + {{ summaryData.selectedPlan.SecretsManager.seatPrice | currency: "$" }} + /{{ summaryData.selectedPlanInterval | i18n }} + + + {{ summaryData.secretsManagerSeatTotal | currency: "$" }} + +

+
+ + + +

+ + {{ summaryData.additionalServiceAccount }} + {{ "serviceAccounts" | i18n | lowercase }} + × + {{ + summaryData.selectedPlan?.SecretsManager?.additionalPricePerServiceAccount + | currency: "$" + }} + /{{ summaryData.selectedPlanInterval | i18n }} + + {{ summaryData.additionalServiceAccountTotal | currency: "$" }} +

+
+
+
+ + + +

+ + {{ + "providerDiscount" | i18n: this.summaryData.discountPercentageFromSub | lowercase + }} + + + {{ summaryData.totalAppliedDiscount | currency: "$" }} + +

+
+
+
+ + +
+ +

+ {{ "estimatedTax" | i18n }} + {{ summaryData.estimatedTax | currency: "USD" : "$" }} +

+
+
+ +
+ +

+ {{ "total" | i18n }} + + {{ summaryData.total - summaryData.totalAppliedDiscount | currency: "USD" : "$" }} + / {{ summaryData.selectedPlanInterval | i18n }} + +

+
+
+
+
diff --git a/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts new file mode 100644 index 00000000000..d4fdf35b743 --- /dev/null +++ b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts @@ -0,0 +1,48 @@ +import { Component, Input } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { PlanInterval } from "@bitwarden/common/billing/enums"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; + +export interface PricingSummaryData { + selectedPlanInterval: string; + passwordManagerSeats: number; + passwordManagerSeatTotal: number; + secretsManagerSeatTotal: number; + additionalStorageTotal: number; + additionalStoragePriceMonthly: number; + additionalServiceAccountTotal: number; + totalAppliedDiscount: number; + secretsManagerSubtotal: number; + passwordManagerSubtotal: number; + total: number; + organization?: Organization; + sub?: OrganizationSubscriptionResponse; + selectedPlan?: PlanResponse; + selectedInterval?: PlanInterval; + discountPercentageFromSub?: number; + discountPercentage?: number; + acceptingSponsorship?: boolean; + additionalServiceAccount?: number; + totalOpened?: boolean; + storageGb?: number; + isSecretsManagerTrial?: boolean; + estimatedTax?: number; +} + +@Component({ + selector: "app-pricing-summary", + templateUrl: "./pricing-summary.component.html", + standalone: false, +}) +export class PricingSummaryComponent { + @Input() summaryData!: PricingSummaryData; + planIntervals = PlanInterval; + + toggleTotalOpened(): void { + if (this.summaryData) { + this.summaryData.totalOpened = !this.summaryData.totalOpened; + } + } +} diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html new file mode 100644 index 00000000000..dbd2899c9e0 --- /dev/null +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.html @@ -0,0 +1,117 @@ + + + {{ "subscribetoEnterprise" | i18n: currentPlanName }} + + +
+

{{ "subscribeEnterpriseSubtitle" | i18n: currentPlanName }}

+ + + +
    +
  • + + {{ "includeEnterprisePolicies" | i18n }} +
  • +
  • + + {{ "passwordLessSso" | i18n }} +
  • +
  • + + {{ "accountRecovery" | i18n }} +
  • +
  • + + {{ "customRoles" | i18n }} +
  • +
  • + + {{ "unlimitedSecretsAndProjects" | i18n }} +
  • +
+ +
    +
  • + + {{ "secureDataSharing" | i18n }} +
  • +
  • + + {{ "eventLogMonitoring" | i18n }} +
  • +
  • + + {{ "directoryIntegration" | i18n }} +
  • +
  • + + {{ "unlimitedSecretsAndProjects" | i18n }} +
  • +
+ +
    +
  • + + {{ "premiumAccounts" | i18n }} +
  • +
  • + + {{ "unlimitedSharing" | i18n }} +
  • +
  • + + {{ "createUnlimitedCollections" | i18n }} +
  • +
+
+ +
+
+

{{ "selectAPlan" | i18n }}

+
+ + +
+ @for (planCard of planCards(); track $index) { + + } +
+
+
+ + +

{{ "paymentMethod" | i18n }}

+ + + + + + +
+
+ + + + + +
diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts new file mode 100644 index 00000000000..ca51ae80e1f --- /dev/null +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts @@ -0,0 +1,365 @@ +import { Component, EventEmitter, Inject, OnInit, Output, signal, ViewChild } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { PaymentMethodType, PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; +import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + DIALOG_DATA, + DialogConfig, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; + +import { PlanCardService } from "../../services/plan-card.service"; +import { PaymentComponent } from "../payment/payment.component"; +import { PlanCard } from "../plan-card/plan-card.component"; +import { PricingSummaryData } from "../pricing-summary/pricing-summary.component"; + +import { PricingSummaryService } from "./../../services/pricing-summary.service"; + +type TrialPaymentDialogParams = { + organizationId: string; + subscription: OrganizationSubscriptionResponse; + productTierType: ProductTierType; + initialPaymentMethod?: PaymentMethodType; +}; + +export const TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE = { + CLOSED: "closed", + SUBMITTED: "submitted", +} as const; + +export type TrialPaymentDialogResultType = + (typeof TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE)[keyof typeof TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE]; + +interface OnSuccessArgs { + organizationId: string; +} + +@Component({ + selector: "app-trial-payment-dialog", + templateUrl: "./trial-payment-dialog.component.html", + standalone: false, +}) +export class TrialPaymentDialogComponent implements OnInit { + @ViewChild(PaymentComponent) paymentComponent!: PaymentComponent; + @ViewChild(ManageTaxInformationComponent) taxComponent!: ManageTaxInformationComponent; + + currentPlan!: PlanResponse; + currentPlanName!: string; + productTypes = ProductTierType; + organization!: Organization; + organizationId!: string; + sub!: OrganizationSubscriptionResponse; + selectedInterval: PlanInterval = PlanInterval.Annually; + + planCards = signal([]); + plans!: ListResponse; + + @Output() onSuccess = new EventEmitter(); + protected initialPaymentMethod: PaymentMethodType; + protected taxInformation!: TaxInformation; + protected readonly ResultType = TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE; + pricingSummaryData!: PricingSummaryData; + + constructor( + @Inject(DIALOG_DATA) private dialogParams: TrialPaymentDialogParams, + private dialogRef: DialogRef, + private organizationService: OrganizationService, + private i18nService: I18nService, + private organizationApiService: OrganizationApiServiceAbstraction, + private accountService: AccountService, + private planCardService: PlanCardService, + private pricingSummaryService: PricingSummaryService, + private apiService: ApiService, + private toastService: ToastService, + private billingApiService: BillingApiServiceAbstraction, + private organizationBillingApiServiceAbstraction: OrganizationBillingApiServiceAbstraction, + ) { + this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card; + } + + async ngOnInit(): Promise { + this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); + this.sub = + this.dialogParams.subscription ?? + (await this.organizationApiService.getSubscription(this.dialogParams.organizationId)); + this.organizationId = this.dialogParams.organizationId; + this.currentPlan = this.sub?.plan; + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + if (!userId) { + throw new Error("User ID is required"); + } + const organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + if (!organization) { + throw new Error("Organization not found"); + } + this.organization = organization; + + const planCards = await this.planCardService.getCadenceCards( + this.currentPlan, + this.sub, + this.isSecretsManagerTrial(), + ); + + this.planCards.set(planCards); + + if (!this.selectedInterval) { + this.selectedInterval = planCards.find((card) => card.isSelected)?.isAnnual + ? PlanInterval.Annually + : PlanInterval.Monthly; + } + + const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); + this.taxInformation = TaxInformation.from(taxInfo); + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.currentPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + + this.plans = await this.apiService.getPlans(); + } + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => dialogService.open(TrialPaymentDialogComponent, dialogConfig); + + async setSelected(planCard: PlanCard) { + this.selectedInterval = planCard.isAnnual ? PlanInterval.Annually : PlanInterval.Monthly; + + this.planCards.update((planCards) => { + return planCards.map((planCard) => { + if (planCard.isSelected) { + return { + ...planCard, + isSelected: false, + }; + } else { + return { + ...planCard, + isSelected: true, + }; + } + }); + }); + + await this.selectPlan(); + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.currentPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + } + + protected async selectPlan() { + if ( + this.selectedInterval === PlanInterval.Monthly && + this.currentPlan.productTier == ProductTierType.Families + ) { + return; + } + + const filteredPlans = this.plans.data.filter( + (plan) => + plan.productTier === this.currentPlan.productTier && + plan.isAnnual === (this.selectedInterval === PlanInterval.Annually), + ); + if (filteredPlans.length > 0) { + this.currentPlan = filteredPlans[0]; + } + try { + await this.refreshSalesTax(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const translatedMessage = this.i18nService.t(errorMessage); + this.toastService.showToast({ + title: "", + variant: "error", + message: !translatedMessage || translatedMessage === "" ? errorMessage : translatedMessage, + }); + } + } + + protected get showTaxIdField(): boolean { + switch (this.currentPlan.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } + } + + private async refreshSalesTax(): Promise { + if ( + this.taxInformation === undefined || + !this.taxInformation.country || + !this.taxInformation.postalCode + ) { + return; + } + + const request: PreviewOrganizationInvoiceRequest = { + organizationId: this.organizationId, + passwordManager: { + additionalStorage: 0, + plan: this.currentPlan?.type, + seats: this.sub.seats, + }, + taxInformation: { + postalCode: this.taxInformation.postalCode, + country: this.taxInformation.country, + taxId: this.taxInformation.taxId, + }, + }; + + if (this.organization.useSecretsManager) { + request.secretsManager = { + seats: this.sub.smSeats ?? 0, + additionalMachineAccounts: + (this.sub.smServiceAccounts ?? 0) - + (this.sub.plan.SecretsManager?.baseServiceAccount ?? 0), + }; + } + + this.pricingSummaryData = await this.pricingSummaryService.getPricingSummaryData( + this.currentPlan, + this.sub, + this.organization, + this.selectedInterval, + this.taxInformation, + this.isSecretsManagerTrial(), + ); + } + + async taxInformationChanged(event: TaxInformation) { + this.taxInformation = event; + this.toggleBankAccount(); + await this.refreshSalesTax(); + } + + toggleBankAccount = () => { + this.paymentComponent.showBankAccount = this.taxInformation.country === "US"; + + if ( + !this.paymentComponent.showBankAccount && + this.paymentComponent.selected === PaymentMethodType.BankAccount + ) { + this.paymentComponent.select(PaymentMethodType.Card); + } + }; + + isSecretsManagerTrial(): boolean { + return ( + this.sub?.subscription?.items?.some((item) => + this.sub?.customerDiscount?.appliesTo?.includes(item.productId), + ) ?? false + ); + } + + async onSubscribe(): Promise { + if (!this.taxComponent.validate()) { + this.taxComponent.markAllAsTouched(); + } + try { + await this.updateOrganizationPaymentMethod( + this.organizationId, + this.paymentComponent, + this.taxInformation, + ); + + if (this.currentPlan.type !== this.sub.planType) { + const changePlanRequest = new ChangePlanFrequencyRequest(); + changePlanRequest.newPlanType = this.currentPlan.type; + await this.organizationBillingApiServiceAbstraction.changeSubscriptionFrequency( + this.organizationId, + changePlanRequest, + ); + } + + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("updatedPaymentMethod"), + }); + + this.onSuccess.emit({ organizationId: this.organizationId }); + this.dialogRef.close(TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED); + } catch (error) { + const msg = + typeof error === "object" && error !== null && "message" in error + ? (error as { message: string }).message + : String(error); + this.toastService.showToast({ + variant: "error", + title: undefined, + message: this.i18nService.t(msg) || msg, + }); + } + } + + private async updateOrganizationPaymentMethod( + organizationId: string, + paymentComponent: PaymentComponent, + taxInformation: TaxInformation, + ): Promise { + const paymentSource = await paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(taxInformation); + + await this.billingApiService.updateOrganizationPaymentMethod(organizationId, request); + } + + resolvePlanName(productTier: ProductTierType): string { + switch (productTier) { + case ProductTierType.Enterprise: + return this.i18nService.t("planNameEnterprise"); + case ProductTierType.Free: + return this.i18nService.t("planNameFree"); + case ProductTierType.Families: + return this.i18nService.t("planNameFamilies"); + case ProductTierType.Teams: + return this.i18nService.t("planNameTeams"); + case ProductTierType.TeamsStarter: + return this.i18nService.t("planNameTeamsStarter"); + default: + return this.i18nService.t("planNameFree"); + } + } +} diff --git a/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts b/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts index 074358537b6..a7ce53c9998 100644 --- a/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts +++ b/apps/web/src/app/billing/warnings/components/organization-free-trial-warning.component.ts @@ -1,8 +1,10 @@ import { AsyncPipe } from "@angular/common"; -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { Observable } from "rxjs"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Observable, Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -37,16 +39,28 @@ import { OrganizationFreeTrialWarning } from "../types"; `, imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe], }) -export class OrganizationFreeTrialWarningComponent implements OnInit { +export class OrganizationFreeTrialWarningComponent implements OnInit, OnDestroy { @Input({ required: true }) organization!: Organization; @Output() clicked = new EventEmitter(); warning$!: Observable; + private destroy$ = new Subject(); constructor(private organizationWarningsService: OrganizationWarningsService) {} ngOnInit() { this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization); + this.organizationWarningsService + .refreshWarningsForOrganization$(this.organization.id as OrganizationId) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.refresh(); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } refresh = () => { diff --git a/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts index fa53992afe0..78c17a5d384 100644 --- a/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/warnings/services/organization-warnings.service.ts @@ -1,6 +1,7 @@ +import { Location } from "@angular/common"; import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; -import { filter, from, lastValueFrom, map, Observable, switchMap, takeWhile } from "rxjs"; +import { filter, from, lastValueFrom, map, Observable, Subject, switchMap, takeWhile } from "rxjs"; import { take } from "rxjs/operators"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -10,10 +11,15 @@ import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/r import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { openChangePlanDialog } from "../../organizations/change-plan-dialog.component"; +import { + TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, + TrialPaymentDialogComponent, +} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; import { OrganizationFreeTrialWarning, OrganizationResellerRenewalWarning } from "../types"; const format = (date: Date) => @@ -26,6 +32,7 @@ const format = (date: Date) => @Injectable({ providedIn: "root" }) export class OrganizationWarningsService { private cache$ = new Map>(); + private refreshWarnings$ = new Subject(); constructor( private configService: ConfigService, @@ -34,6 +41,8 @@ export class OrganizationWarningsService { private organizationApiService: OrganizationApiServiceAbstraction, private organizationBillingApiService: OrganizationBillingApiServiceAbstraction, private router: Router, + private location: Location, + protected syncService: SyncService, ) {} getFreeTrialWarning$ = ( @@ -174,10 +183,33 @@ export class OrganizationWarningsService { }); break; } + case "add_payment_method_optional_trial": { + const organizationSubscriptionResponse = + await this.organizationApiService.getSubscription(organization.id); + + const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { + data: { + organizationId: organization.id, + subscription: organizationSubscriptionResponse, + productTierType: organization?.productTierType, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { + this.refreshWarnings$.next(organization.id as OrganizationId); + } + } } }), ); + refreshWarningsForOrganization$(organizationId: OrganizationId): Observable { + return this.refreshWarnings$.pipe( + filter((id) => id === organizationId), + map((): void => void 0), + ); + } + private getResponse$ = ( organization: Organization, bypassCache: boolean = false, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d5ded3c75ea..9c9ecc79721 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -568,6 +568,9 @@ "cancel": { "message": "Cancel" }, + "later": { + "message": "Later" + }, "canceled": { "message": "Canceled" }, @@ -4630,6 +4633,9 @@ "receiveMarketingEmailsV2": { "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, + "subscribe": { + "message": "Subscribe" + }, "unsubscribe": { "message": "Unsubscribe" }, @@ -10900,5 +10906,26 @@ "example": "12-3456789" } } + }, + "subscribetoEnterprise": { + "message": "Subscribe to $PLAN$", + "placeholders": { + "plan": { + "content": "$1", + "example": "Teams" + } + } + }, + "subscribeEnterpriseSubtitle": { + "message": "Your 7-day $PLAN$ trial starts today. Add a payment method now to continue using these features after your trial ends: ", + "placeholders": { + "plan": { + "content": "$1", + "example": "Teams" + } + } + }, + "unlimitedSecretsAndProjects": { + "message": "Unlimited secrets and projects" } -} +} \ No newline at end of file diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts index 4975da0d7d2..29301e626b9 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -1,3 +1,4 @@ +import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request"; import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; import { @@ -28,4 +29,9 @@ export abstract class OrganizationBillingApiServiceAbstraction { organizationKey: string; }, ) => Promise; + + abstract changeSubscriptionFrequency: ( + organizationId: string, + request: ChangePlanFrequencyRequest, + ) => Promise; } diff --git a/libs/common/src/billing/models/request/change-plan-frequency.request.ts b/libs/common/src/billing/models/request/change-plan-frequency.request.ts new file mode 100644 index 00000000000..70b77181663 --- /dev/null +++ b/libs/common/src/billing/models/request/change-plan-frequency.request.ts @@ -0,0 +1,9 @@ +import { PlanType } from "../../enums"; + +export class ChangePlanFrequencyRequest { + newPlanType: PlanType; + + constructor(newPlanType?: PlanType) { + this.newPlanType = newPlanType!; + } +} diff --git a/libs/common/src/billing/services/organization/organization-billing-api.service.ts b/libs/common/src/billing/services/organization/organization-billing-api.service.ts index 1189316a487..e9456f61026 100644 --- a/libs/common/src/billing/services/organization/organization-billing-api.service.ts +++ b/libs/common/src/billing/services/organization/organization-billing-api.service.ts @@ -1,3 +1,4 @@ +import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request"; import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response"; import { ApiService } from "../../../abstractions/api.service"; @@ -83,4 +84,17 @@ export class OrganizationBillingApiService implements OrganizationBillingApiServ return response as string; } + + async changeSubscriptionFrequency( + organizationId: string, + request: ChangePlanFrequencyRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/organizations/" + organizationId + "/billing/change-frequency", + request, + true, + false, + ); + } } From da6fb82fd8ed68a910f2558373d0e5cfcdbc0162 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:21:19 -0500 Subject: [PATCH 011/179] [deps] AC: Update core-js to v3.44.0 (#15284) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index ca7af3ec596..54855d72104 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -69,7 +69,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", diff --git a/package-lock.json b/package-lock.json index e44797997f1..01a9ea8c09c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -205,7 +205,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", @@ -17512,9 +17512,9 @@ } }, "node_modules/core-js": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", - "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", + "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", "hasInstallScript": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 089ef3342e9..2cb60a6afd1 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.42.0", + "core-js": "3.44.0", "form-data": "4.0.2", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", From e99abb49ec26e983fa76b2bb114d6c330d0a9ce6 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:30:50 -0500 Subject: [PATCH 012/179] [PM-23621] Require userId for initAccount on the key-service (#15684) * require userID for initAccount on key service * add unit test coverage * update consumer --- .../login-decryption-options.component.ts | 2 +- .../src/abstractions/key.service.ts | 7 +- libs/key-management/src/key.service.spec.ts | 101 ++++++++++++++++++ libs/key-management/src/key.service.ts | 14 ++- 4 files changed, 112 insertions(+), 12 deletions(-) diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index bbdc0106786..a2018817fed 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -249,7 +249,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { } try { - const { publicKey, privateKey } = await this.keyService.initAccount(); + const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId); const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); await this.apiService.postAccountKeys(keysRequest); diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index c843d8dc872..3c0d6c8a138 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -386,11 +386,12 @@ export abstract class KeyService { /** * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! + * @param userId The user id of the target user. * @returns The user's newly created public key, private key, and encrypted private key - * - * @throws An error if there is no user currently active. + * @throws An error if the userId is null or undefined. + * @throws An error if the user already has a user key. */ - abstract initAccount(): Promise<{ + abstract initAccount(userId: UserId): Promise<{ userKey: UserKey; publicKey: string; privateKey: EncString; diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 395d668de9f..7a033792c79 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -1047,4 +1047,105 @@ describe("keyService", () => { }); }); }); + + describe("initAccount", () => { + let userKey: UserKey; + let mockPublicKey: string; + let mockPrivateKey: EncString; + + beforeEach(() => { + userKey = makeSymmetricCryptoKey(64); + mockPublicKey = "mockPublicKey"; + mockPrivateKey = makeEncString("mockPrivateKey"); + + keyGenerationService.createKey.mockResolvedValue(userKey); + jest.spyOn(keyService, "makeKeyPair").mockResolvedValue([mockPublicKey, mockPrivateKey]); + jest.spyOn(keyService, "setUserKey").mockResolvedValue(); + }); + + test.each([null as unknown as UserId, undefined as unknown as UserId])( + "throws when the provided userId is %s", + async (userId) => { + await expect(keyService.initAccount(userId)).rejects.toThrow("UserId is required."); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + }, + ); + + it("throws when user already has a user key", async () => { + const existingUserKey = makeSymmetricCryptoKey(64); + stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(existingUserKey); + + await expect(keyService.initAccount(mockUserId)).rejects.toThrow( + "Cannot initialize account, keys already exist.", + ); + expect(logService.error).toHaveBeenCalledWith( + "Tried to initialize account with existing user key.", + ); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + }); + + it("throws when private key creation fails", async () => { + // Simulate failure + const invalidPrivateKey = new EncString( + "2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=", + ); + invalidPrivateKey.encryptedString = null as unknown as EncryptedString; + jest.spyOn(keyService, "makeKeyPair").mockResolvedValue([mockPublicKey, invalidPrivateKey]); + + await expect(keyService.initAccount(mockUserId)).rejects.toThrow( + "Failed to create valid private key.", + ); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + }); + + it("successfully initializes account with new keys", async () => { + const keyCreationSize = 512; + const privateKeyState = stateProvider.singleUser.getFake( + mockUserId, + USER_ENCRYPTED_PRIVATE_KEY, + ); + + const result = await keyService.initAccount(mockUserId); + + expect(keyGenerationService.createKey).toHaveBeenCalledWith(keyCreationSize); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); + expect(privateKeyState.nextMock).toHaveBeenCalledWith(mockPrivateKey.encryptedString); + expect(result).toEqual({ + userKey: userKey, + publicKey: mockPublicKey, + privateKey: mockPrivateKey, + }); + }); + }); + + describe("makeKeyPair", () => { + test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])( + "throws when the provided key is %s", + async (key) => { + await expect(keyService.makeKeyPair(key)).rejects.toThrow( + "'key' is a required parameter and must be non-null.", + ); + }, + ); + + it("generates a key pair and returns public key and encrypted private key", async () => { + const mockKey = new SymmetricCryptoKey(new Uint8Array(64)); + const mockKeyPair: [Uint8Array, Uint8Array] = [new Uint8Array(256), new Uint8Array(256)]; + const mockPublicKeyB64 = "mockPublicKeyB64"; + const mockPrivateKeyEncString = makeEncString("encryptedPrivateKey"); + + cryptoFunctionService.rsaGenerateKeyPair.mockResolvedValue(mockKeyPair); + jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(mockPublicKeyB64); + encryptService.wrapDecapsulationKey.mockResolvedValue(mockPrivateKeyEncString); + + const [publicKey, privateKey] = await keyService.makeKeyPair(mockKey); + + expect(cryptoFunctionService.rsaGenerateKeyPair).toHaveBeenCalledWith(2048); + expect(Utils.fromBufferToB64).toHaveBeenCalledWith(mockKeyPair[0]); + expect(encryptService.wrapDecapsulationKey).toHaveBeenCalledWith(mockKeyPair[1], mockKey); + expect(publicKey).toBe(mockPublicKeyB64); + expect(privateKey).toBe(mockPrivateKeyEncString); + }); + }); }); diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index fca080252f6..0f4b101d9b2 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -661,19 +661,17 @@ export class DefaultKeyService implements KeyServiceAbstraction { * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! */ - async initAccount(): Promise<{ + async initAccount(userId: UserId): Promise<{ userKey: UserKey; publicKey: string; privateKey: EncString; }> { - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - - if (activeUserId == null) { - throw new Error("Cannot initilize an account if one is not active."); + if (userId == null) { + throw new Error("UserId is required."); } // Verify user key doesn't exist - const existingUserKey = await this.getUserKey(activeUserId); + const existingUserKey = await this.getUserKey(userId); if (existingUserKey != null) { this.logService.error("Tried to initialize account with existing user key."); @@ -686,9 +684,9 @@ export class DefaultKeyService implements KeyServiceAbstraction { throw new Error("Failed to create valid private key."); } - await this.setUserKey(userKey, activeUserId); + await this.setUserKey(userKey, userId); await this.stateProvider - .getUser(activeUserId, USER_ENCRYPTED_PRIVATE_KEY) + .getUser(userId, USER_ENCRYPTED_PRIVATE_KEY) .update(() => privateKey.encryptedString!); return { From a563e6d91000f453f5cbd8f904f397dccc96d058 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:47:25 -0400 Subject: [PATCH 013/179] Add `messaging` & `messaging-internal` libraries (#15711) --- .github/CODEOWNERS | 2 + .../sync/sync-service.listener.spec.ts | 4 +- .../abstractions/messaging.service.ts | 2 +- libs/common/src/platform/messaging/index.ts | 5 +- .../common/src/platform/messaging/internal.ts | 6 +- .../messaging/subject-message.sender.spec.ts | 65 ------------------- libs/messaging-internal/README.md | 5 ++ libs/messaging-internal/eslint.config.mjs | 3 + libs/messaging-internal/jest.config.js | 10 +++ libs/messaging-internal/package.json | 11 ++++ libs/messaging-internal/project.json | 33 ++++++++++ .../src}/helpers.spec.ts | 5 +- .../src}/helpers.ts | 8 +-- libs/messaging-internal/src/index.ts | 5 ++ .../src/messaging-internal.spec.ts | 8 +++ .../src/subject-message.sender.spec.ts | 59 +++++++++++++++++ .../src}/subject-message.sender.ts | 6 +- libs/messaging-internal/tsconfig.eslint.json | 6 ++ libs/messaging-internal/tsconfig.json | 13 ++++ libs/messaging-internal/tsconfig.lib.json | 10 +++ libs/messaging-internal/tsconfig.spec.json | 10 +++ libs/messaging/README.md | 5 ++ libs/messaging/eslint.config.mjs | 3 + libs/messaging/jest.config.js | 10 +++ libs/messaging/package.json | 11 ++++ libs/messaging/project.json | 33 ++++++++++ libs/messaging/src/index.ts | 4 ++ libs/messaging/src/is-external-message.ts | 5 ++ .../src}/message.listener.spec.ts | 26 ++++---- .../src}/message.listener.ts | 0 .../src}/message.sender.ts | 0 libs/messaging/src/messaging.spec.ts | 8 +++ .../messaging => messaging/src}/types.ts | 0 libs/messaging/tsconfig.eslint.json | 6 ++ libs/messaging/tsconfig.json | 13 ++++ libs/messaging/tsconfig.lib.json | 16 +++++ libs/messaging/tsconfig.spec.json | 17 +++++ package-lock.json | 17 +++++ tsconfig.base.json | 2 + 39 files changed, 347 insertions(+), 105 deletions(-) delete mode 100644 libs/common/src/platform/messaging/subject-message.sender.spec.ts create mode 100644 libs/messaging-internal/README.md create mode 100644 libs/messaging-internal/eslint.config.mjs create mode 100644 libs/messaging-internal/jest.config.js create mode 100644 libs/messaging-internal/package.json create mode 100644 libs/messaging-internal/project.json rename libs/{common/src/platform/messaging => messaging-internal/src}/helpers.spec.ts (90%) rename libs/{common/src/platform/messaging => messaging-internal/src}/helpers.ts (65%) create mode 100644 libs/messaging-internal/src/index.ts create mode 100644 libs/messaging-internal/src/messaging-internal.spec.ts create mode 100644 libs/messaging-internal/src/subject-message.sender.spec.ts rename libs/{common/src/platform/messaging => messaging-internal/src}/subject-message.sender.ts (77%) create mode 100644 libs/messaging-internal/tsconfig.eslint.json create mode 100644 libs/messaging-internal/tsconfig.json create mode 100644 libs/messaging-internal/tsconfig.lib.json create mode 100644 libs/messaging-internal/tsconfig.spec.json create mode 100644 libs/messaging/README.md create mode 100644 libs/messaging/eslint.config.mjs create mode 100644 libs/messaging/jest.config.js create mode 100644 libs/messaging/package.json create mode 100644 libs/messaging/project.json create mode 100644 libs/messaging/src/index.ts create mode 100644 libs/messaging/src/is-external-message.ts rename libs/{common/src/platform/messaging => messaging/src}/message.listener.spec.ts (55%) rename libs/{common/src/platform/messaging => messaging/src}/message.listener.ts (100%) rename libs/{common/src/platform/messaging => messaging/src}/message.sender.ts (100%) create mode 100644 libs/messaging/src/messaging.spec.ts rename libs/{common/src/platform/messaging => messaging/src}/types.ts (100%) create mode 100644 libs/messaging/tsconfig.eslint.json create mode 100644 libs/messaging/tsconfig.json create mode 100644 libs/messaging/tsconfig.lib.json create mode 100644 libs/messaging/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d7fec2a5ea..203c7ae7607 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -94,6 +94,8 @@ libs/platform @bitwarden/team-platform-dev libs/storage-core @bitwarden/team-platform-dev libs/logging @bitwarden/team-platform-dev libs/storage-test-utils @bitwarden/team-platform-dev +libs/messaging @bitwarden/team-platform-dev +libs/messaging-internal @bitwarden/team-platform-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files diff --git a/apps/browser/src/platform/sync/sync-service.listener.spec.ts b/apps/browser/src/platform/sync/sync-service.listener.spec.ts index dc0674a7ae5..383586c0cd0 100644 --- a/apps/browser/src/platform/sync/sync-service.listener.spec.ts +++ b/apps/browser/src/platform/sync/sync-service.listener.spec.ts @@ -3,10 +3,8 @@ import { Subject, firstValueFrom } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { tagAsExternal } from "@bitwarden/common/platform/messaging/helpers"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { tagAsExternal } from "@bitwarden/messaging-internal"; import { FullSyncMessage } from "./foreground-sync.service"; import { FULL_SYNC_FINISHED, SyncServiceListener } from "./sync-service.listener"; diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index f24279f932a..3520d9352ef 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ // Export the new message sender as the legacy MessagingService to minimize changes in the initial PR, // team specific PR's will come after. -export { MessageSender as MessagingService } from "../messaging/message.sender"; +export { MessageSender as MessagingService } from "@bitwarden/messaging"; diff --git a/libs/common/src/platform/messaging/index.ts b/libs/common/src/platform/messaging/index.ts index a9b4eca5ae8..5d452f32b31 100644 --- a/libs/common/src/platform/messaging/index.ts +++ b/libs/common/src/platform/messaging/index.ts @@ -1,4 +1 @@ -export { MessageListener } from "./message.listener"; -export { MessageSender } from "./message.sender"; -export { Message, CommandDefinition } from "./types"; -export { isExternalMessage } from "./helpers"; +export * from "@bitwarden/messaging"; diff --git a/libs/common/src/platform/messaging/internal.ts b/libs/common/src/platform/messaging/internal.ts index 08763d48bc5..9fe261f2264 100644 --- a/libs/common/src/platform/messaging/internal.ts +++ b/libs/common/src/platform/messaging/internal.ts @@ -1,5 +1 @@ -// Built in implementations -export { SubjectMessageSender } from "./subject-message.sender"; - -// Helpers meant to be used only by other implementations -export { tagAsExternal, getCommand } from "./helpers"; +export * from "@bitwarden/messaging-internal"; diff --git a/libs/common/src/platform/messaging/subject-message.sender.spec.ts b/libs/common/src/platform/messaging/subject-message.sender.spec.ts deleted file mode 100644 index 4278fca7bc1..00000000000 --- a/libs/common/src/platform/messaging/subject-message.sender.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Subject } from "rxjs"; - -import { subscribeTo } from "../../../spec/observable-tracker"; - -import { SubjectMessageSender } from "./internal"; -import { MessageSender } from "./message.sender"; -import { Message, CommandDefinition } from "./types"; - -describe("SubjectMessageSender", () => { - const subject = new Subject>(); - const subjectObservable = subject.asObservable(); - - const sut: MessageSender = new SubjectMessageSender(subject); - - describe("send", () => { - it("will send message with command from message definition", async () => { - const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); - - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send(commandDefinition, { test: 1 }); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); - }); - - it("will send message with command from normal string", async () => { - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send("myCommand", { test: 1 }); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); - }); - - it("will send message with object even if payload not given", async () => { - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send("myCommand"); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); - }); - - it.each([null, undefined])( - "will send message with object even if payload is null-ish (%s)", - async (payloadValue) => { - const tracker = subscribeTo(subjectObservable); - const pausePromise = tracker.pauseUntilReceived(1); - - sut.send("myCommand", payloadValue); - - await pausePromise; - - expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); - }, - ); - }); -}); diff --git a/libs/messaging-internal/README.md b/libs/messaging-internal/README.md new file mode 100644 index 00000000000..a2f36138ad7 --- /dev/null +++ b/libs/messaging-internal/README.md @@ -0,0 +1,5 @@ +# messaging-internal + +Owned by: platform + +Internal details to accompany @bitwarden/messaging this library should not be consumed in non-platform code. diff --git a/libs/messaging-internal/eslint.config.mjs b/libs/messaging-internal/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/messaging-internal/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/messaging-internal/jest.config.js b/libs/messaging-internal/jest.config.js new file mode 100644 index 00000000000..152244f6603 --- /dev/null +++ b/libs/messaging-internal/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "messaging-internal", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/messaging-internal", +}; diff --git a/libs/messaging-internal/package.json b/libs/messaging-internal/package.json new file mode 100644 index 00000000000..7a0a13d2d67 --- /dev/null +++ b/libs/messaging-internal/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/messaging-internal", + "version": "0.0.1", + "description": "Internal details to accompany @bitwarden/messaging this library should not be consumed in non-platform code.", + "private": true, + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "GPL-3.0", + "author": "platform" +} diff --git a/libs/messaging-internal/project.json b/libs/messaging-internal/project.json new file mode 100644 index 00000000000..ad55cde5c20 --- /dev/null +++ b/libs/messaging-internal/project.json @@ -0,0 +1,33 @@ +{ + "name": "messaging-internal", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/messaging-internal/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/messaging-internal", + "main": "libs/messaging-internal/src/index.ts", + "tsConfig": "libs/messaging-internal/tsconfig.lib.json", + "assets": ["libs/messaging-internal/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/messaging-internal/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/messaging-internal/jest.config.js" + } + } + } +} diff --git a/libs/common/src/platform/messaging/helpers.spec.ts b/libs/messaging-internal/src/helpers.spec.ts similarity index 90% rename from libs/common/src/platform/messaging/helpers.spec.ts rename to libs/messaging-internal/src/helpers.spec.ts index 8839a542ffc..5a97ff959cc 100644 --- a/libs/common/src/platform/messaging/helpers.spec.ts +++ b/libs/messaging-internal/src/helpers.spec.ts @@ -1,7 +1,8 @@ import { Subject, firstValueFrom } from "rxjs"; -import { getCommand, isExternalMessage, tagAsExternal } from "./helpers"; -import { Message, CommandDefinition } from "./types"; +import { CommandDefinition, isExternalMessage, Message } from "@bitwarden/messaging"; + +import { getCommand, tagAsExternal } from "./helpers"; describe("helpers", () => { describe("getCommand", () => { diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/messaging-internal/src/helpers.ts similarity index 65% rename from libs/common/src/platform/messaging/helpers.ts rename to libs/messaging-internal/src/helpers.ts index e7521ea42a2..00231b455b7 100644 --- a/libs/common/src/platform/messaging/helpers.ts +++ b/libs/messaging-internal/src/helpers.ts @@ -1,6 +1,6 @@ import { map } from "rxjs"; -import { CommandDefinition } from "./types"; +import { CommandDefinition, EXTERNAL_SOURCE_TAG } from "@bitwarden/messaging"; export const getCommand = ( commandDefinition: CommandDefinition> | string, @@ -12,12 +12,6 @@ export const getCommand = ( } }; -export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); - -export const isExternalMessage = (message: Record) => { - return message?.[EXTERNAL_SOURCE_TAG] === true; -}; - export const tagAsExternal = >() => { return map((message: T) => { return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); diff --git a/libs/messaging-internal/src/index.ts b/libs/messaging-internal/src/index.ts new file mode 100644 index 00000000000..08763d48bc5 --- /dev/null +++ b/libs/messaging-internal/src/index.ts @@ -0,0 +1,5 @@ +// Built in implementations +export { SubjectMessageSender } from "./subject-message.sender"; + +// Helpers meant to be used only by other implementations +export { tagAsExternal, getCommand } from "./helpers"; diff --git a/libs/messaging-internal/src/messaging-internal.spec.ts b/libs/messaging-internal/src/messaging-internal.spec.ts new file mode 100644 index 00000000000..b2b50a218bd --- /dev/null +++ b/libs/messaging-internal/src/messaging-internal.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("messaging-internal", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/messaging-internal/src/subject-message.sender.spec.ts b/libs/messaging-internal/src/subject-message.sender.spec.ts new file mode 100644 index 00000000000..e3e5305d1b2 --- /dev/null +++ b/libs/messaging-internal/src/subject-message.sender.spec.ts @@ -0,0 +1,59 @@ +import { bufferCount, firstValueFrom, Subject } from "rxjs"; + +import { CommandDefinition, Message } from "@bitwarden/messaging"; + +import { SubjectMessageSender } from "./subject-message.sender"; + +describe("SubjectMessageSender", () => { + const subject = new Subject>(); + const subjectObservable = subject.asObservable(); + + const sut = new SubjectMessageSender(subject); + + describe("send", () => { + it("will send message with command from message definition", async () => { + const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send(commandDefinition, { test: 1 }); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with command from normal string", async () => { + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send("myCommand", { test: 1 }); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with object even if payload not given", async () => { + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send("myCommand"); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand" }); + }); + + it.each([null, undefined])( + "will send message with object even if payload is null-ish (%s)", + async (payloadValue) => { + const emissionsPromise = firstValueFrom(subjectObservable.pipe(bufferCount(1))); + + sut.send("myCommand", payloadValue); + + const emissions = await emissionsPromise; + + expect(emissions[0]).toEqual({ command: "myCommand" }); + }, + ); + }); +}); diff --git a/libs/common/src/platform/messaging/subject-message.sender.ts b/libs/messaging-internal/src/subject-message.sender.ts similarity index 77% rename from libs/common/src/platform/messaging/subject-message.sender.ts rename to libs/messaging-internal/src/subject-message.sender.ts index 170f8a24c6f..e8df5913b01 100644 --- a/libs/common/src/platform/messaging/subject-message.sender.ts +++ b/libs/messaging-internal/src/subject-message.sender.ts @@ -1,8 +1,8 @@ import { Subject } from "rxjs"; -import { getCommand } from "./internal"; -import { MessageSender } from "./message.sender"; -import { Message, CommandDefinition } from "./types"; +import { CommandDefinition, Message, MessageSender } from "@bitwarden/messaging"; + +import { getCommand } from "./helpers"; export class SubjectMessageSender implements MessageSender { constructor(private readonly messagesSubject: Subject>>) {} diff --git a/libs/messaging-internal/tsconfig.eslint.json b/libs/messaging-internal/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/messaging-internal/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/messaging-internal/tsconfig.json b/libs/messaging-internal/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/messaging-internal/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/messaging-internal/tsconfig.lib.json b/libs/messaging-internal/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/messaging-internal/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/messaging-internal/tsconfig.spec.json b/libs/messaging-internal/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/messaging-internal/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/messaging/README.md b/libs/messaging/README.md new file mode 100644 index 00000000000..98eb96a5a40 --- /dev/null +++ b/libs/messaging/README.md @@ -0,0 +1,5 @@ +# messaging + +Owned by: platform + +Services for sending and recieving messages from different contexts of the same application. diff --git a/libs/messaging/eslint.config.mjs b/libs/messaging/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/messaging/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/messaging/jest.config.js b/libs/messaging/jest.config.js new file mode 100644 index 00000000000..f0450499e31 --- /dev/null +++ b/libs/messaging/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "messaging", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/messaging", +}; diff --git a/libs/messaging/package.json b/libs/messaging/package.json new file mode 100644 index 00000000000..01c8d7cb0e7 --- /dev/null +++ b/libs/messaging/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/messaging", + "version": "0.0.1", + "description": "Services for sending and recieving messages from different contexts of the same application.", + "private": true, + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "GPL-3.0", + "author": "platform" +} diff --git a/libs/messaging/project.json b/libs/messaging/project.json new file mode 100644 index 00000000000..f00e0bd2dc9 --- /dev/null +++ b/libs/messaging/project.json @@ -0,0 +1,33 @@ +{ + "name": "messaging", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/messaging/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/messaging", + "main": "libs/messaging/src/index.ts", + "tsConfig": "libs/messaging/tsconfig.lib.json", + "assets": ["libs/messaging/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/messaging/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/messaging/jest.config.js" + } + } + } +} diff --git a/libs/messaging/src/index.ts b/libs/messaging/src/index.ts new file mode 100644 index 00000000000..9090ff581c1 --- /dev/null +++ b/libs/messaging/src/index.ts @@ -0,0 +1,4 @@ +export { MessageListener } from "./message.listener"; +export { MessageSender } from "./message.sender"; +export { Message, CommandDefinition } from "./types"; +export { isExternalMessage, EXTERNAL_SOURCE_TAG } from "./is-external-message"; diff --git a/libs/messaging/src/is-external-message.ts b/libs/messaging/src/is-external-message.ts new file mode 100644 index 00000000000..46775cb14d6 --- /dev/null +++ b/libs/messaging/src/is-external-message.ts @@ -0,0 +1,5 @@ +export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); + +export const isExternalMessage = (message: Record) => { + return message?.[EXTERNAL_SOURCE_TAG] === true; +}; diff --git a/libs/common/src/platform/messaging/message.listener.spec.ts b/libs/messaging/src/message.listener.spec.ts similarity index 55% rename from libs/common/src/platform/messaging/message.listener.spec.ts rename to libs/messaging/src/message.listener.spec.ts index 98bbf1fdc82..19787c6feae 100644 --- a/libs/common/src/platform/messaging/message.listener.spec.ts +++ b/libs/messaging/src/message.listener.spec.ts @@ -1,6 +1,4 @@ -import { Subject } from "rxjs"; - -import { subscribeTo } from "../../../spec/observable-tracker"; +import { bufferCount, firstValueFrom, Subject } from "rxjs"; import { MessageListener } from "./message.listener"; import { Message, CommandDefinition } from "./types"; @@ -13,35 +11,33 @@ describe("MessageListener", () => { describe("allMessages$", () => { it("runs on all nexts", async () => { - const tracker = subscribeTo(sut.allMessages$); - - const pausePromise = tracker.pauseUntilReceived(2); + const emissionsPromise = firstValueFrom(sut.allMessages$.pipe(bufferCount(2))); subject.next({ command: "command1", test: 1 }); subject.next({ command: "command2", test: 2 }); - await pausePromise; + const emissions = await emissionsPromise; - expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 }); - expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 }); + expect(emissions[0]).toEqual({ command: "command1", test: 1 }); + expect(emissions[1]).toEqual({ command: "command2", test: 2 }); }); }); describe("messages$", () => { it("runs on only my commands", async () => { - const tracker = subscribeTo(sut.messages$(testCommandDefinition)); - - const pausePromise = tracker.pauseUntilReceived(2); + const emissionsPromise = firstValueFrom( + sut.messages$(testCommandDefinition).pipe(bufferCount(2)), + ); subject.next({ command: "notMyCommand", test: 1 }); subject.next({ command: "myCommand", test: 2 }); subject.next({ command: "myCommand", test: 3 }); subject.next({ command: "notMyCommand", test: 4 }); - await pausePromise; + const emissions = await emissionsPromise; - expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 }); - expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 }); + expect(emissions[0]).toEqual({ command: "myCommand", test: 2 }); + expect(emissions[1]).toEqual({ command: "myCommand", test: 3 }); }); }); }); diff --git a/libs/common/src/platform/messaging/message.listener.ts b/libs/messaging/src/message.listener.ts similarity index 100% rename from libs/common/src/platform/messaging/message.listener.ts rename to libs/messaging/src/message.listener.ts diff --git a/libs/common/src/platform/messaging/message.sender.ts b/libs/messaging/src/message.sender.ts similarity index 100% rename from libs/common/src/platform/messaging/message.sender.ts rename to libs/messaging/src/message.sender.ts diff --git a/libs/messaging/src/messaging.spec.ts b/libs/messaging/src/messaging.spec.ts new file mode 100644 index 00000000000..170b24750c5 --- /dev/null +++ b/libs/messaging/src/messaging.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("messaging", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/common/src/platform/messaging/types.ts b/libs/messaging/src/types.ts similarity index 100% rename from libs/common/src/platform/messaging/types.ts rename to libs/messaging/src/types.ts diff --git a/libs/messaging/tsconfig.eslint.json b/libs/messaging/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/messaging/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/messaging/tsconfig.json b/libs/messaging/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/messaging/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/messaging/tsconfig.lib.json b/libs/messaging/tsconfig.lib.json new file mode 100644 index 00000000000..1f3b89d988e --- /dev/null +++ b/libs/messaging/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": [ + "src/**/*.ts", + "../messaging-internal/src/subject-message.sender.spec.ts", + "../messaging-internal/src/subject-message.sender.ts", + "../messaging-internal/src/helpers.spec.ts", + "../messaging-internal/src/helpers.ts" + ], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/messaging/tsconfig.spec.json b/libs/messaging/tsconfig.spec.json new file mode 100644 index 00000000000..2e5b192faff --- /dev/null +++ b/libs/messaging/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + "../messaging-internal/src/subject-message.sender.spec.ts", + "../messaging-internal/src/helpers.spec.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index 01a9ea8c09c..fbbc4c25b44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -352,6 +352,15 @@ "version": "0.0.1", "license": "GPL-3.0" }, + "libs/messaging": { + "name": "@bitwarden/messaging", + "version": "0.0.1", + "license": "GPL-3.0" + }, + "libs/messaging-internal": { + "version": "0.0.1", + "license": "GPL-3.0" + }, "libs/node": { "name": "@bitwarden/node", "version": "0.0.0", @@ -4591,6 +4600,14 @@ "resolved": "libs/logging", "link": true }, + "node_modules/@bitwarden/messaging": { + "resolved": "libs/messaging", + "link": true + }, + "node_modules/@bitwarden/messaging-internal": { + "resolved": "libs/messaging-internal", + "link": true + }, "node_modules/@bitwarden/node": { "resolved": "libs/node", "link": true diff --git a/tsconfig.base.json b/tsconfig.base.json index c462ab97d37..478fce4bfd8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -38,6 +38,8 @@ "@bitwarden/key-management": ["./libs/key-management/src"], "@bitwarden/key-management-ui": ["./libs/key-management-ui/src"], "@bitwarden/logging": ["libs/logging/src"], + "@bitwarden/messaging": ["libs/messaging/src/index.ts"], + "@bitwarden/messaging-internal": ["libs/messaging-internal/src/index.ts"], "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/nx-plugin": ["libs/nx-plugin/src/index.ts"], "@bitwarden/platform": ["./libs/platform/src"], From 9839087b00a3de91e8b4a87cce4d9e74a201d95b Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:30:22 -0500 Subject: [PATCH 014/179] Only return ciphers when they exist. (#15716) conditionals within the template are checking for an empty array rather than an empty ciphers property. --- .../vault-list-items-container.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 5fc1c43210c..5a08ed3002b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -146,14 +146,16 @@ export class VaultListItemsContainerComponent implements AfterViewInit { ciphers: PopupCipherViewLike[]; }[] >(() => { + const ciphers = this.ciphers(); + // Not grouping by type, return a single group with all ciphers - if (!this.groupByType()) { - return [{ ciphers: this.ciphers() }]; + if (!this.groupByType() && ciphers.length > 0) { + return [{ ciphers }]; } const groups: Record = {}; - this.ciphers().forEach((cipher) => { + ciphers.forEach((cipher) => { let groupKey = "all"; switch (CipherViewLikeUtils.getType(cipher)) { case CipherType.Card: From 6aa59d5ba795a5ddd513a913ce4ea93129c65946 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:46:02 -0400 Subject: [PATCH 015/179] [BRE-831] Fixing PR target permissions (#15729) --- .github/workflows/build-browser-target.yml | 3 ++- .github/workflows/build-desktop-target.yml | 3 ++- .github/workflows/build-web-target.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml index ef3beef4b8b..e89a41c1009 100644 --- a/.github/workflows/build-browser-target.yml +++ b/.github/workflows/build-browser-target.yml @@ -38,6 +38,7 @@ jobs: uses: ./.github/workflows/build-browser.yml secrets: inherit permissions: - contents: read + contents: write + pull-requests: write id-token: write diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml index 31ac819a3e6..96a0e6880f8 100644 --- a/.github/workflows/build-desktop-target.yml +++ b/.github/workflows/build-desktop-target.yml @@ -38,6 +38,7 @@ jobs: uses: ./.github/workflows/build-desktop.yml secrets: inherit permissions: - contents: read + contents: write + pull-requests: write id-token: write diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml index b1055885400..2f9e201ac60 100644 --- a/.github/workflows/build-web-target.yml +++ b/.github/workflows/build-web-target.yml @@ -37,7 +37,8 @@ jobs: uses: ./.github/workflows/build-web.yml secrets: inherit permissions: - contents: read + contents: write + pull-requests: write id-token: write security-events: write From 8aeeb92958bb6bd9396444ba08ccfdd52b730390 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 18:48:00 +0200 Subject: [PATCH 016/179] [PM-24030] Migrate abstract services in libs/common strict TS (#15727) Migrates the abstract classes in libs/common to be strict ts compatible. Primarily by adding abstract to every field and converting it to a function syntax instead of lambda. --- libs/common/src/abstractions/api.service.ts | 446 ++++++++++-------- .../event/event-collection.service.ts | 10 +- .../event/event-upload.service.ts | 4 +- .../org-domain-api.service.abstraction.ts | 22 +- .../org-domain.service.abstraction.ts | 16 +- .../organization-api.service.abstraction.ts | 94 ++-- .../organization.service.abstraction.ts | 19 +- .../abstractions/provider.service.ts | 10 +- .../provider-api.service.abstraction.ts | 24 +- .../src/auth/abstractions/account.service.ts | 14 +- .../abstractions/anonymous-hub.service.ts | 6 +- .../src/auth/abstractions/avatar.service.ts | 4 +- .../devices-api.service.abstraction.ts | 24 +- .../src/auth/abstractions/token.service.ts | 60 ++- ...er-verification-api.service.abstraction.ts | 10 +- .../user-verification.service.abstraction.ts | 22 +- .../webauthn-login-api.service.abstraction.ts | 6 +- ...authn-login-prf-key.service.abstraction.ts | 6 +- .../webauthn-login.service.abstraction.ts | 10 +- ...account-billing-api.service.abstraction.ts | 11 +- .../billing-account-profile-state.service.ts | 2 - .../billing-api.service.abstraction.ts | 67 ++- .../organization-billing.service.ts | 20 +- .../device-trust.service.abstraction.ts | 32 +- .../vault-timeout-settings.service.ts | 18 +- .../abstractions/vault-timeout.service.ts | 8 +- ...fido2-authenticator.service.abstraction.ts | 12 +- .../fido2/fido2-client.service.abstraction.ts | 12 +- ...ido2-user-interface.service.abstraction.ts | 22 +- .../platform/abstractions/state.service.ts | 30 +- .../password-strength.service.abstraction.ts | 8 +- .../services/send-api.service.abstraction.ts | 37 +- .../send/services/send.service.abstraction.ts | 28 +- .../src/vault/abstractions/cipher.service.ts | 2 - .../file-upload/cipher-file-upload.service.ts | 6 +- .../folder/folder-api.service.abstraction.ts | 13 +- .../folder/folder.service.abstraction.ts | 32 +- .../src/vault/abstractions/search.service.ts | 22 +- .../vault-settings/vault-settings.service.ts | 20 +- 39 files changed, 595 insertions(+), 614 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 4969e87f1c6..015a742c1ac 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { @@ -128,7 +126,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher * of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service. */ export abstract class ApiService { - send: ( + abstract send( method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, @@ -136,196 +134,225 @@ export abstract class ApiService { hasResponse: boolean, apiUrl?: string | null, alterHeaders?: (headers: Headers) => void, - ) => Promise; + ): Promise; - postIdentityToken: ( + abstract postIdentityToken( request: | PasswordTokenRequest | SsoTokenRequest | UserApiTokenRequest | WebAuthnLoginTokenRequest, - ) => Promise< + ): Promise< IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse >; - refreshIdentityToken: () => Promise; + abstract refreshIdentityToken(): Promise; - getProfile: () => Promise; - getUserSubscription: () => Promise; - getTaxInfo: () => Promise; - putProfile: (request: UpdateProfileRequest) => Promise; - putAvatar: (request: UpdateAvatarRequest) => Promise; - putTaxInfo: (request: TaxInfoUpdateRequest) => Promise; - postPrelogin: (request: PreloginRequest) => Promise; - postEmailToken: (request: EmailTokenRequest) => Promise; - postEmail: (request: EmailRequest) => Promise; - postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise; - postSecurityStamp: (request: SecretVerificationRequest) => Promise; - getAccountRevisionDate: () => Promise; - postPasswordHint: (request: PasswordHintRequest) => Promise; - postPremium: (data: FormData) => Promise; - postReinstatePremium: () => Promise; - postAccountStorage: (request: StorageRequest) => Promise; - postAccountPayment: (request: PaymentRequest) => Promise; - postAccountLicense: (data: FormData) => Promise; - postAccountKeys: (request: KeysRequest) => Promise; - postAccountVerifyEmail: () => Promise; - postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; - postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; - postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; - postAccountKdf: (request: KdfRequest) => Promise; - postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise; - postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise; - postConvertToKeyConnector: () => Promise; + abstract getProfile(): Promise; + abstract getUserSubscription(): Promise; + abstract getTaxInfo(): Promise; + abstract putProfile(request: UpdateProfileRequest): Promise; + abstract putAvatar(request: UpdateAvatarRequest): Promise; + abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise; + abstract postPrelogin(request: PreloginRequest): Promise; + abstract postEmailToken(request: EmailTokenRequest): Promise; + abstract postEmail(request: EmailRequest): Promise; + abstract postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise; + abstract postSecurityStamp(request: SecretVerificationRequest): Promise; + abstract getAccountRevisionDate(): Promise; + abstract postPasswordHint(request: PasswordHintRequest): Promise; + abstract postPremium(data: FormData): Promise; + abstract postReinstatePremium(): Promise; + abstract postAccountStorage(request: StorageRequest): Promise; + abstract postAccountPayment(request: PaymentRequest): Promise; + abstract postAccountLicense(data: FormData): Promise; + abstract postAccountKeys(request: KeysRequest): Promise; + abstract postAccountVerifyEmail(): Promise; + abstract postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise; + abstract postAccountRecoverDelete(request: DeleteRecoverRequest): Promise; + abstract postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise; + abstract postAccountKdf(request: KdfRequest): Promise; + abstract postUserApiKey(id: string, request: SecretVerificationRequest): Promise; + abstract postUserRotateApiKey( + id: string, + request: SecretVerificationRequest, + ): Promise; + abstract postConvertToKeyConnector(): Promise; //passwordless - getAuthRequest: (id: string) => Promise; - putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise; - getAuthRequests: () => Promise>; - getLastAuthRequest: () => Promise; + abstract getAuthRequest(id: string): Promise; + abstract putAuthRequest( + id: string, + request: PasswordlessAuthRequest, + ): Promise; + abstract getAuthRequests(): Promise>; + abstract getLastAuthRequest(): Promise; - getUserBillingHistory: () => Promise; - getUserBillingPayment: () => Promise; + abstract getUserBillingHistory(): Promise; + abstract getUserBillingPayment(): Promise; - getCipher: (id: string) => Promise; - getFullCipherDetails: (id: string) => Promise; - getCipherAdmin: (id: string) => Promise; - getAttachmentData: ( + abstract getCipher(id: string): Promise; + abstract getFullCipherDetails(id: string): Promise; + abstract getCipherAdmin(id: string): Promise; + abstract getAttachmentData( cipherId: string, attachmentId: string, emergencyAccessId?: string, - ) => Promise; - getAttachmentDataAdmin: (cipherId: string, attachmentId: string) => Promise; - getCiphersOrganization: (organizationId: string) => Promise>; - postCipher: (request: CipherRequest) => Promise; - postCipherCreate: (request: CipherCreateRequest) => Promise; - postCipherAdmin: (request: CipherCreateRequest) => Promise; - putCipher: (id: string, request: CipherRequest) => Promise; - putPartialCipher: (id: string, request: CipherPartialRequest) => Promise; - putCipherAdmin: (id: string, request: CipherRequest) => Promise; - deleteCipher: (id: string) => Promise; - deleteCipherAdmin: (id: string) => Promise; - deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; - deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; - putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; - putShareCipher: (id: string, request: CipherShareRequest) => Promise; - putShareCiphers: (request: CipherBulkShareRequest) => Promise>; - putCipherCollections: ( + ): Promise; + abstract getAttachmentDataAdmin( + cipherId: string, + attachmentId: string, + ): Promise; + abstract getCiphersOrganization(organizationId: string): Promise>; + abstract postCipher(request: CipherRequest): Promise; + abstract postCipherCreate(request: CipherCreateRequest): Promise; + abstract postCipherAdmin(request: CipherCreateRequest): Promise; + abstract putCipher(id: string, request: CipherRequest): Promise; + abstract putPartialCipher(id: string, request: CipherPartialRequest): Promise; + abstract putCipherAdmin(id: string, request: CipherRequest): Promise; + abstract deleteCipher(id: string): Promise; + abstract deleteCipherAdmin(id: string): Promise; + abstract deleteManyCiphers(request: CipherBulkDeleteRequest): Promise; + abstract deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise; + abstract putMoveCiphers(request: CipherBulkMoveRequest): Promise; + abstract putShareCipher(id: string, request: CipherShareRequest): Promise; + abstract putShareCiphers(request: CipherBulkShareRequest): Promise>; + abstract putCipherCollections( id: string, request: CipherCollectionsRequest, - ) => Promise; - putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; - postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise; - putDeleteCipher: (id: string) => Promise; - putDeleteCipherAdmin: (id: string) => Promise; - putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; - putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; - putRestoreCipher: (id: string) => Promise; - putRestoreCipherAdmin: (id: string) => Promise; - putRestoreManyCiphers: ( + ): Promise; + abstract putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise; + abstract postPurgeCiphers( + request: SecretVerificationRequest, + organizationId?: string, + ): Promise; + abstract putDeleteCipher(id: string): Promise; + abstract putDeleteCipherAdmin(id: string): Promise; + abstract putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise; + abstract putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise; + abstract putRestoreCipher(id: string): Promise; + abstract putRestoreCipherAdmin(id: string): Promise; + abstract putRestoreManyCiphers( request: CipherBulkRestoreRequest, - ) => Promise>; - putRestoreManyCiphersAdmin: ( + ): Promise>; + abstract putRestoreManyCiphersAdmin( request: CipherBulkRestoreRequest, - ) => Promise>; + ): Promise>; - postCipherAttachment: ( + abstract postCipherAttachment( id: string, request: AttachmentRequest, - ) => Promise; - deleteCipherAttachment: (id: string, attachmentId: string) => Promise; - deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise; - postShareCipherAttachment: ( + ): Promise; + abstract deleteCipherAttachment(id: string, attachmentId: string): Promise; + abstract deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise; + abstract postShareCipherAttachment( id: string, attachmentId: string, data: FormData, organizationId: string, - ) => Promise; - renewAttachmentUploadUrl: ( + ): Promise; + abstract renewAttachmentUploadUrl( id: string, attachmentId: string, - ) => Promise; - postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise; + ): Promise; + abstract postAttachmentFile(id: string, attachmentId: string, data: FormData): Promise; - getUserCollections: () => Promise>; - getCollections: (organizationId: string) => Promise>; - getCollectionUsers: (organizationId: string, id: string) => Promise; - getCollectionAccessDetails: ( + abstract getUserCollections(): Promise>; + abstract getCollections(organizationId: string): Promise>; + abstract getCollectionUsers( organizationId: string, id: string, - ) => Promise; - getManyCollectionsWithAccessDetails: ( + ): Promise; + abstract getCollectionAccessDetails( + organizationId: string, + id: string, + ): Promise; + abstract getManyCollectionsWithAccessDetails( orgId: string, - ) => Promise>; - postCollection: ( + ): Promise>; + abstract postCollection( organizationId: string, request: CollectionRequest, - ) => Promise; - putCollection: ( + ): Promise; + abstract putCollection( organizationId: string, id: string, request: CollectionRequest, - ) => Promise; - deleteCollection: (organizationId: string, id: string) => Promise; - deleteManyCollections: (organizationId: string, collectionIds: string[]) => Promise; + ): Promise; + abstract deleteCollection(organizationId: string, id: string): Promise; + abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise; - getGroupUsers: (organizationId: string, id: string) => Promise; - deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; - - getSync: () => Promise; - - getSettingsDomains: () => Promise; - putSettingsDomains: (request: UpdateDomainsRequest) => Promise; - - getTwoFactorProviders: () => Promise>; - getTwoFactorOrganizationProviders: ( + abstract getGroupUsers(organizationId: string, id: string): Promise; + abstract deleteGroupUser( organizationId: string, - ) => Promise>; - getTwoFactorAuthenticator: ( + id: string, + organizationUserId: string, + ): Promise; + + abstract getSync(): Promise; + + abstract getSettingsDomains(): Promise; + abstract putSettingsDomains(request: UpdateDomainsRequest): Promise; + + abstract getTwoFactorProviders(): Promise>; + abstract getTwoFactorOrganizationProviders( + organizationId: string, + ): Promise>; + abstract getTwoFactorAuthenticator( request: SecretVerificationRequest, - ) => Promise; - getTwoFactorEmail: (request: SecretVerificationRequest) => Promise; - getTwoFactorDuo: (request: SecretVerificationRequest) => Promise; - getTwoFactorOrganizationDuo: ( + ): Promise; + abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise; + abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise; + abstract getTwoFactorOrganizationDuo( organizationId: string, request: SecretVerificationRequest, - ) => Promise; - getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise; - getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise; - getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise; - getTwoFactorRecover: (request: SecretVerificationRequest) => Promise; - putTwoFactorAuthenticator: ( + ): Promise; + abstract getTwoFactorYubiKey( + request: SecretVerificationRequest, + ): Promise; + abstract getTwoFactorWebAuthn( + request: SecretVerificationRequest, + ): Promise; + abstract getTwoFactorWebAuthnChallenge( + request: SecretVerificationRequest, + ): Promise; + abstract getTwoFactorRecover( + request: SecretVerificationRequest, + ): Promise; + abstract putTwoFactorAuthenticator( request: UpdateTwoFactorAuthenticatorRequest, - ) => Promise; - deleteTwoFactorAuthenticator: ( + ): Promise; + abstract deleteTwoFactorAuthenticator( request: DisableTwoFactorAuthenticatorRequest, - ) => Promise; - putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise; - putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise; - putTwoFactorOrganizationDuo: ( + ): Promise; + abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise; + abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise; + abstract putTwoFactorOrganizationDuo( organizationId: string, request: UpdateTwoFactorDuoRequest, - ) => Promise; - putTwoFactorYubiKey: ( + ): Promise; + abstract putTwoFactorYubiKey( request: UpdateTwoFactorYubikeyOtpRequest, - ) => Promise; - putTwoFactorWebAuthn: ( + ): Promise; + abstract putTwoFactorWebAuthn( request: UpdateTwoFactorWebAuthnRequest, - ) => Promise; - deleteTwoFactorWebAuthn: ( + ): Promise; + abstract deleteTwoFactorWebAuthn( request: UpdateTwoFactorWebAuthnDeleteRequest, - ) => Promise; - putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise; - putTwoFactorOrganizationDisable: ( + ): Promise; + abstract putTwoFactorDisable( + request: TwoFactorProviderRequest, + ): Promise; + abstract putTwoFactorOrganizationDisable( organizationId: string, request: TwoFactorProviderRequest, - ) => Promise; - postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; - postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; - getDeviceVerificationSettings: () => Promise; - putDeviceVerificationSettings: ( + ): Promise; + abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise; + abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; + abstract getDeviceVerificationSettings(): Promise; + abstract putDeviceVerificationSettings( request: DeviceVerificationRequest, - ) => Promise; + ): Promise; - getCloudCommunicationsEnabled: () => Promise; + abstract getCloudCommunicationsEnabled(): Promise; abstract getOrganizationConnection( id: string, type: OrganizationConnectionType, @@ -340,136 +367,147 @@ export abstract class ApiService { configType: { new (response: any): TConfig }, organizationConnectionId: string, ): Promise>; - deleteOrganizationConnection: (id: string) => Promise; - getPlans: () => Promise>; + abstract deleteOrganizationConnection(id: string): Promise; + abstract getPlans(): Promise>; - getProviderUsers: (providerId: string) => Promise>; - getProviderUser: (providerId: string, id: string) => Promise; - postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise; - postProviderUserReinvite: (providerId: string, id: string) => Promise; - postManyProviderUserReinvite: ( + abstract getProviderUsers( + providerId: string, + ): Promise>; + abstract getProviderUser(providerId: string, id: string): Promise; + abstract postProviderUserInvite( + providerId: string, + request: ProviderUserInviteRequest, + ): Promise; + abstract postProviderUserReinvite(providerId: string, id: string): Promise; + abstract postManyProviderUserReinvite( providerId: string, request: ProviderUserBulkRequest, - ) => Promise>; - postProviderUserAccept: ( + ): Promise>; + abstract postProviderUserAccept( providerId: string, id: string, request: ProviderUserAcceptRequest, - ) => Promise; - postProviderUserConfirm: ( + ): Promise; + abstract postProviderUserConfirm( providerId: string, id: string, request: ProviderUserConfirmRequest, - ) => Promise; - postProviderUsersPublicKey: ( + ): Promise; + abstract postProviderUsersPublicKey( providerId: string, request: ProviderUserBulkRequest, - ) => Promise>; - postProviderUserBulkConfirm: ( + ): Promise>; + abstract postProviderUserBulkConfirm( providerId: string, request: ProviderUserBulkConfirmRequest, - ) => Promise>; - putProviderUser: ( + ): Promise>; + abstract putProviderUser( providerId: string, id: string, request: ProviderUserUpdateRequest, - ) => Promise; - deleteProviderUser: (organizationId: string, id: string) => Promise; - deleteManyProviderUsers: ( + ): Promise; + abstract deleteProviderUser(organizationId: string, id: string): Promise; + abstract deleteManyProviderUsers( providerId: string, request: ProviderUserBulkRequest, - ) => Promise>; - getProviderClients: ( + ): Promise>; + abstract getProviderClients( providerId: string, - ) => Promise>; - postProviderAddOrganization: ( + ): Promise>; + abstract postProviderAddOrganization( providerId: string, request: ProviderAddOrganizationRequest, - ) => Promise; - postProviderCreateOrganization: ( + ): Promise; + abstract postProviderCreateOrganization( providerId: string, request: ProviderOrganizationCreateRequest, - ) => Promise; - deleteProviderOrganization: (providerId: string, organizationId: string) => Promise; + ): Promise; + abstract deleteProviderOrganization(providerId: string, organizationId: string): Promise; - getEvents: (start: string, end: string, token: string) => Promise>; - getEventsCipher: ( + abstract getEvents( + start: string, + end: string, + token: string, + ): Promise>; + abstract getEventsCipher( id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsOrganization: ( + ): Promise>; + abstract getEventsOrganization( id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsOrganizationUser: ( + ): Promise>; + abstract getEventsOrganizationUser( organizationId: string, id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsProvider: ( + ): Promise>; + abstract getEventsProvider( id: string, start: string, end: string, token: string, - ) => Promise>; - getEventsProviderUser: ( + ): Promise>; + abstract getEventsProviderUser( providerId: string, id: string, start: string, end: string, token: string, - ) => Promise>; + ): Promise>; /** * Posts events for a user * @param request The array of events to upload * @param userId The optional user id the events belong to. If no user id is provided the active user id is used. */ - postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise; + abstract postEventsCollect(request: EventRequest[], userId?: UserId): Promise; - deleteSsoUser: (organizationId: string) => Promise; - getSsoUserIdentifier: () => Promise; + abstract deleteSsoUser(organizationId: string): Promise; + abstract getSsoUserIdentifier(): Promise; - getUserPublicKey: (id: string) => Promise; + abstract getUserPublicKey(id: string): Promise; - getHibpBreach: (username: string) => Promise; + abstract getHibpBreach(username: string): Promise; - postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise; - postSetupPayment: () => Promise; + abstract postBitPayInvoice(request: BitPayInvoiceRequest): Promise; + abstract postSetupPayment(): Promise; - getActiveBearerToken: () => Promise; - fetch: (request: Request) => Promise; - nativeFetch: (request: Request) => Promise; + abstract getActiveBearerToken(): Promise; + abstract fetch(request: Request): Promise; + abstract nativeFetch(request: Request): Promise; - preValidateSso: (identifier: string) => Promise; + abstract preValidateSso(identifier: string): Promise; - postCreateSponsorship: ( + abstract postCreateSponsorship( sponsorshipOrgId: string, request: OrganizationSponsorshipCreateRequest, - ) => Promise; - getSponsorshipSyncStatus: ( + ): Promise; + abstract getSponsorshipSyncStatus( sponsoredOrgId: string, - ) => Promise; - deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise; - postPreValidateSponsorshipToken: ( + ): Promise; + abstract deleteRemoveSponsorship(sponsoringOrgId: string): Promise; + abstract postPreValidateSponsorshipToken( sponsorshipToken: string, - ) => Promise; - postRedeemSponsorship: ( + ): Promise; + abstract postRedeemSponsorship( sponsorshipToken: string, request: OrganizationSponsorshipRedeemRequest, - ) => Promise; + ): Promise; - getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise; - postUserKeyToKeyConnector: ( + abstract getMasterKeyFromKeyConnector( + keyConnectorUrl: string, + ): Promise; + abstract postUserKeyToKeyConnector( keyConnectorUrl: string, request: KeyConnectorUserKeyRequest, - ) => Promise; - getKeyConnectorAlive: (keyConnectorUrl: string) => Promise; - getOrganizationExport: (organizationId: string) => Promise; + ): Promise; + abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise; + abstract getOrganizationExport(organizationId: string): Promise; } diff --git a/libs/common/src/abstractions/event/event-collection.service.ts b/libs/common/src/abstractions/event/event-collection.service.ts index 6ca94d93a62..4f06b76c5eb 100644 --- a/libs/common/src/abstractions/event/event-collection.service.ts +++ b/libs/common/src/abstractions/event/event-collection.service.ts @@ -1,18 +1,16 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EventType } from "../../enums"; import { CipherView } from "../../vault/models/view/cipher.view"; export abstract class EventCollectionService { - collectMany: ( + abstract collectMany( eventType: EventType, ciphers: CipherView[], uploadImmediately?: boolean, - ) => Promise; - collect: ( + ): Promise; + abstract collect( eventType: EventType, cipherId?: string, uploadImmediately?: boolean, organizationId?: string, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/abstractions/event/event-upload.service.ts b/libs/common/src/abstractions/event/event-upload.service.ts index af2e7a77e7f..352c7cb0255 100644 --- a/libs/common/src/abstractions/event/event-upload.service.ts +++ b/libs/common/src/abstractions/event/event-upload.service.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UserId } from "../../types/guid"; export abstract class EventUploadService { - uploadEvents: (userId?: UserId) => Promise; + abstract uploadEvents(userId?: UserId): Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts index 5a393ed1996..b1452c1359b 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../../models/response/list.response"; import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request"; @@ -8,19 +6,19 @@ import { OrganizationDomainResponse } from "./responses/organization-domain.resp import { VerifiedOrganizationDomainSsoDetailsResponse } from "./responses/verified-organization-domain-sso-details.response"; export abstract class OrgDomainApiServiceAbstraction { - getAllByOrgId: (orgId: string) => Promise>; - getByOrgIdAndOrgDomainId: ( + abstract getAllByOrgId(orgId: string): Promise>; + abstract getByOrgIdAndOrgDomainId( orgId: string, orgDomainId: string, - ) => Promise; - post: ( + ): Promise; + abstract post( orgId: string, orgDomain: OrganizationDomainRequest, - ) => Promise; - verify: (orgId: string, orgDomainId: string) => Promise; - delete: (orgId: string, orgDomainId: string) => Promise; - getClaimedOrgDomainByEmail: (email: string) => Promise; - getVerifiedOrgDomainsByEmail: ( + ): Promise; + abstract verify(orgId: string, orgDomainId: string): Promise; + abstract delete(orgId: string, orgDomainId: string): Promise; + abstract getClaimedOrgDomainByEmail(email: string): Promise; + abstract getVerifiedOrgDomainsByEmail( email: string, - ) => Promise>; + ): Promise>; } diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts index 05a0b6d722f..7f08d226d15 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain.service.abstraction.ts @@ -1,22 +1,20 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { OrganizationDomainResponse } from "./responses/organization-domain.response"; export abstract class OrgDomainServiceAbstraction { - orgDomains$: Observable; + abstract orgDomains$: Observable; - get: (orgDomainId: string) => OrganizationDomainResponse; + abstract get(orgDomainId: string): OrganizationDomainResponse; - copyDnsTxt: (dnsTxt: string) => void; + abstract copyDnsTxt(dnsTxt: string): void; } // Note: this separate class is designed to hold methods that are not // meant to be used in components (e.g., data write methods) export abstract class OrgDomainInternalServiceAbstraction extends OrgDomainServiceAbstraction { - upsert: (orgDomains: OrganizationDomainResponse[]) => void; - replace: (orgDomains: OrganizationDomainResponse[]) => void; - clearCache: () => void; - delete: (orgDomainIds: string[]) => void; + abstract upsert(orgDomains: OrganizationDomainResponse[]): void; + abstract replace(orgDomains: OrganizationDomainResponse[]): void; + abstract clearCache(): void; + abstract delete(orgDomainIds: string[]): void; } diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 000d1655416..10626d6758f 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request"; import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; @@ -34,60 +32,66 @@ import { OrganizationKeysResponse } from "../../models/response/organization-key import { OrganizationResponse } from "../../models/response/organization.response"; import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response"; -export class OrganizationApiServiceAbstraction { - get: (id: string) => Promise; - getBilling: (id: string) => Promise; - getBillingHistory: (id: string) => Promise; - getSubscription: (id: string) => Promise; - getLicense: (id: string, installationId: string) => Promise; - getAutoEnrollStatus: (identifier: string) => Promise; - create: (request: OrganizationCreateRequest) => Promise; - createWithoutPayment: ( +export abstract class OrganizationApiServiceAbstraction { + abstract get(id: string): Promise; + abstract getBilling(id: string): Promise; + abstract getBillingHistory(id: string): Promise; + abstract getSubscription(id: string): Promise; + abstract getLicense(id: string, installationId: string): Promise; + abstract getAutoEnrollStatus(identifier: string): Promise; + abstract create(request: OrganizationCreateRequest): Promise; + abstract createWithoutPayment( request: OrganizationNoPaymentMethodCreateRequest, - ) => Promise; - createLicense: (data: FormData) => Promise; - save: (id: string, request: OrganizationUpdateRequest) => Promise; - updatePayment: (id: string, request: PaymentRequest) => Promise; - upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; - updatePasswordManagerSeats: ( + ): Promise; + abstract createLicense(data: FormData): Promise; + abstract save(id: string, request: OrganizationUpdateRequest): Promise; + abstract updatePayment(id: string, request: PaymentRequest): Promise; + abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise; + abstract updatePasswordManagerSeats( id: string, request: OrganizationSubscriptionUpdateRequest, - ) => Promise; - updateSecretsManagerSubscription: ( + ): Promise; + abstract updateSecretsManagerSubscription( id: string, request: OrganizationSmSubscriptionUpdateRequest, - ) => Promise; - updateSeats: (id: string, request: SeatRequest) => Promise; - updateStorage: (id: string, request: StorageRequest) => Promise; - verifyBank: (id: string, request: VerifyBankRequest) => Promise; - reinstate: (id: string) => Promise; - leave: (id: string) => Promise; - delete: (id: string, request: SecretVerificationRequest) => Promise; - deleteUsingToken: ( + ): Promise; + abstract updateSeats(id: string, request: SeatRequest): Promise; + abstract updateStorage(id: string, request: StorageRequest): Promise; + abstract verifyBank(id: string, request: VerifyBankRequest): Promise; + abstract reinstate(id: string): Promise; + abstract leave(id: string): Promise; + abstract delete(id: string, request: SecretVerificationRequest): Promise; + abstract deleteUsingToken( organizationId: string, request: OrganizationVerifyDeleteRecoverRequest, - ) => Promise; - updateLicense: (id: string, data: FormData) => Promise; - importDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; - getOrCreateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise; - getApiKeyInformation: ( + ): Promise; + abstract updateLicense(id: string, data: FormData): Promise; + abstract importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise; + abstract getOrCreateApiKey( + id: string, + request: OrganizationApiKeyRequest, + ): Promise; + abstract getApiKeyInformation( id: string, organizationApiKeyType?: OrganizationApiKeyType, - ) => Promise>; - rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise; - getTaxInfo: (id: string) => Promise; - updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise; - getKeys: (id: string) => Promise; - updateKeys: (id: string, request: OrganizationKeysRequest) => Promise; - getSso: (id: string) => Promise; - updateSso: (id: string, request: OrganizationSsoRequest) => Promise; - selfHostedSyncLicense: (id: string) => Promise; - subscribeToSecretsManager: ( + ): Promise>; + abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise; + abstract getTaxInfo(id: string): Promise; + abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise; + abstract getKeys(id: string): Promise; + abstract updateKeys( + id: string, + request: OrganizationKeysRequest, + ): Promise; + abstract getSso(id: string): Promise; + abstract updateSso(id: string, request: OrganizationSsoRequest): Promise; + abstract selfHostedSyncLicense(id: string): Promise; + abstract subscribeToSecretsManager( id: string, request: SecretsManagerSubscribeRequest, - ) => Promise; - updateCollectionManagement: ( + ): Promise; + abstract updateCollectionManagement( id: string, request: OrganizationCollectionManagementUpdateRequest, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index 05c214ece13..770cfd0011d 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { map, Observable } from "rxjs"; import { UserId } from "../../../types/guid"; @@ -68,20 +66,20 @@ export abstract class OrganizationService { * Publishes state for all organizations under the specified user. * @returns An observable list of organizations */ - organizations$: (userId: UserId) => Observable; + abstract organizations$(userId: UserId): Observable; // @todo Clean these up. Continuing to expand them is not recommended. // @see https://bitwarden.atlassian.net/browse/AC-2252 - memberOrganizations$: (userId: UserId) => Observable; + abstract memberOrganizations$(userId: UserId): Observable; /** * Emits true if the user can create or manage a Free Bitwarden Families sponsorship. */ - canManageSponsorships$: (userId: UserId) => Observable; + abstract canManageSponsorships$(userId: UserId): Observable; /** * Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available. */ - familySponsorshipAvailable$: (userId: UserId) => Observable; - hasOrganizations: (userId: UserId) => Observable; + abstract familySponsorshipAvailable$(userId: UserId): Observable; + abstract hasOrganizations(userId: UserId): Observable; } /** @@ -96,7 +94,7 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio * @param organization The organization state being saved. * @param userId The userId to replace state for. */ - upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise; + abstract upsert(OrganizationData: OrganizationData, userId: UserId): Promise; /** * Replaces state for the entire registered organization list for the specified user. @@ -107,5 +105,8 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio * user. * @param userId The userId to replace state for. */ - replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise; + abstract replace( + organizations: { [id: string]: OrganizationData }, + userId: UserId, + ): Promise; } diff --git a/libs/common/src/admin-console/abstractions/provider.service.ts b/libs/common/src/admin-console/abstractions/provider.service.ts index 0cd21174ea1..340156020ff 100644 --- a/libs/common/src/admin-console/abstractions/provider.service.ts +++ b/libs/common/src/admin-console/abstractions/provider.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; @@ -7,8 +5,8 @@ import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; export abstract class ProviderService { - get$: (id: string) => Observable; - get: (id: string) => Promise; - getAll: () => Promise; - save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise; + abstract get$(id: string): Observable; + abstract get(id: string): Promise; + abstract getAll(): Promise; + abstract save(providers: { [id: string]: ProviderData }, userId?: UserId): Promise; } diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts index ffe79f0ad3b..f998fdc8ab7 100644 --- a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; @@ -7,21 +5,23 @@ import { ProviderUpdateRequest } from "../../models/request/provider/provider-up import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; import { ProviderResponse } from "../../models/response/provider/provider.response"; -export class ProviderApiServiceAbstraction { - postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; - getProvider: (id: string) => Promise; - putProvider: (id: string, request: ProviderUpdateRequest) => Promise; - providerRecoverDeleteToken: ( +export abstract class ProviderApiServiceAbstraction { + abstract postProviderSetup(id: string, request: ProviderSetupRequest): Promise; + abstract getProvider(id: string): Promise; + abstract putProvider(id: string, request: ProviderUpdateRequest): Promise; + abstract providerRecoverDeleteToken( organizationId: string, request: ProviderVerifyRecoverDeleteRequest, - ) => Promise; - deleteProvider: (id: string) => Promise; - getProviderAddableOrganizations: (providerId: string) => Promise; - addOrganizationToProvider: ( + ): Promise; + abstract deleteProvider(id: string): Promise; + abstract getProviderAddableOrganizations( + providerId: string, + ): Promise; + abstract addOrganizationToProvider( providerId: string, request: { key: string; organizationId: string; }, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 1686eefda06..a3dabeecf8a 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; @@ -35,20 +33,20 @@ export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { } export abstract class AccountService { - accounts$: Observable>; + abstract accounts$: Observable>; - activeAccount$: Observable; + abstract activeAccount$: Observable; /** * Observable of the last activity time for each account. */ - accountActivity$: Observable>; + abstract accountActivity$: Observable>; /** Observable of the new device login verification property for the account. */ - accountVerifyNewDeviceLogin$: Observable; + abstract accountVerifyNewDeviceLogin$: Observable; /** Account list in order of descending recency */ - sortedUserIds$: Observable; + abstract sortedUserIds$: Observable; /** Next account that is not the current active account */ - nextUpAccount$: Observable; + abstract nextUpAccount$: Observable; /** * Updates the `accounts$` observable with the new account data. * diff --git a/libs/common/src/auth/abstractions/anonymous-hub.service.ts b/libs/common/src/auth/abstractions/anonymous-hub.service.ts index 8e705d67bfe..624a3a04d53 100644 --- a/libs/common/src/auth/abstractions/anonymous-hub.service.ts +++ b/libs/common/src/auth/abstractions/anonymous-hub.service.ts @@ -1,6 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export abstract class AnonymousHubService { - createHubConnection: (token: string) => Promise; - stopHubConnection: () => Promise; + abstract createHubConnection(token: string): Promise; + abstract stopHubConnection(): Promise; } diff --git a/libs/common/src/auth/abstractions/avatar.service.ts b/libs/common/src/auth/abstractions/avatar.service.ts index 89729aa3712..bd2c382e610 100644 --- a/libs/common/src/auth/abstractions/avatar.service.ts +++ b/libs/common/src/auth/abstractions/avatar.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; @@ -9,7 +7,7 @@ export abstract class AvatarService { * An observable monitoring the active user's avatar color. * The observable updates when the avatar color changes. */ - avatarColor$: Observable; + abstract avatarColor$: Observable; /** * Sets the avatar color of the active user * diff --git a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts index cf6cdaefd85..54971a443b7 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -1,47 +1,45 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../models/response/list.response"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request"; import { ProtectedDeviceResponse } from "../models/response/protected-device.response"; export abstract class DevicesApiServiceAbstraction { - getKnownDevice: (email: string, deviceIdentifier: string) => Promise; + abstract getKnownDevice(email: string, deviceIdentifier: string): Promise; - getDeviceByIdentifier: (deviceIdentifier: string) => Promise; + abstract getDeviceByIdentifier(deviceIdentifier: string): Promise; - getDevices: () => Promise>; + abstract getDevices(): Promise>; - updateTrustedDeviceKeys: ( + abstract updateTrustedDeviceKeys( deviceIdentifier: string, devicePublicKeyEncryptedUserKey: string, userKeyEncryptedDevicePublicKey: string, deviceKeyEncryptedDevicePrivateKey: string, - ) => Promise; + ): Promise; - updateTrust: ( + abstract updateTrust( updateDevicesTrustRequestModel: UpdateDevicesTrustRequest, deviceIdentifier: string, - ) => Promise; + ): Promise; - getDeviceKeys: (deviceIdentifier: string) => Promise; + abstract getDeviceKeys(deviceIdentifier: string): Promise; /** * Notifies the server that the device has a device key, but didn't receive any associated decryption keys. * Note: For debugging purposes only. * @param deviceIdentifier - current device identifier */ - postDeviceTrustLoss: (deviceIdentifier: string) => Promise; + abstract postDeviceTrustLoss(deviceIdentifier: string): Promise; /** * Deactivates a device * @param deviceId - The device ID */ - deactivateDevice: (deviceId: string) => Promise; + abstract deactivateDevice(deviceId: string): Promise; /** * Removes trust from a list of devices * @param deviceIds - The device IDs to be untrusted */ - untrustDevices: (deviceIds: string[]) => Promise; + abstract untrustDevices(deviceIds: string[]): Promise; } diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 0c8db6fdcd1..2139f32fca2 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { VaultTimeout, VaultTimeoutAction } from "../../key-management/vault-timeout"; @@ -27,20 +25,20 @@ export abstract class TokenService { * * @returns A promise that resolves with the SetTokensResult containing the tokens that were set. */ - setTokens: ( + abstract setTokens( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, refreshToken?: string, clientIdClientSecret?: [string, string], - ) => Promise; + ): Promise; /** * Clears the access token, refresh token, API Key Client ID, and API Key Client Secret out of memory, disk, and secure storage if supported. * @param userId The optional user id to clear the tokens for; if not provided, the active user id is used. * @returns A promise that resolves when the tokens have been cleared. */ - clearTokens: (userId?: UserId) => Promise; + abstract clearTokens(userId?: UserId): Promise; /** * Sets the access token in memory or disk based on the given vaultTimeoutAction and vaultTimeout @@ -51,11 +49,11 @@ export abstract class TokenService { * @param vaultTimeout The timeout for the vault. * @returns A promise that resolves with the access token that has been set. */ - setAccessToken: ( + abstract setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, - ) => Promise; + ): Promise; // TODO: revisit having this public clear method approach once the state service is fully deprecated. /** @@ -67,21 +65,21 @@ export abstract class TokenService { * pass in the vaultTimeoutAction and vaultTimeout. * This avoids a circular dependency between the StateService, TokenService, and VaultTimeoutSettingsService. */ - clearAccessToken: (userId?: UserId) => Promise; + abstract clearAccessToken(userId?: UserId): Promise; /** * Gets the access token * @param userId - The optional user id to get the access token for; if not provided, the active user is used. * @returns A promise that resolves with the access token or null. */ - getAccessToken: (userId?: UserId) => Promise; + abstract getAccessToken(userId?: UserId): Promise; /** * Gets the refresh token. * @param userId - The optional user id to get the refresh token for; if not provided, the active user is used. * @returns A promise that resolves with the refresh token or null. */ - getRefreshToken: (userId?: UserId) => Promise; + abstract getRefreshToken(userId?: UserId): Promise; /** * Sets the API Key Client ID for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout. @@ -90,18 +88,18 @@ export abstract class TokenService { * @param vaultTimeout The timeout for the vault. * @returns A promise that resolves with the API Key Client ID that has been set. */ - setClientId: ( + abstract setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, userId?: UserId, - ) => Promise; + ): Promise; /** * Gets the API Key Client ID for the active user. * @returns A promise that resolves with the API Key Client ID or undefined */ - getClientId: (userId?: UserId) => Promise; + abstract getClientId(userId?: UserId): Promise; /** * Sets the API Key Client Secret for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout. @@ -110,18 +108,18 @@ export abstract class TokenService { * @param vaultTimeout The timeout for the vault. * @returns A promise that resolves with the client secret that has been set. */ - setClientSecret: ( + abstract setClientSecret( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: VaultTimeout, userId?: UserId, - ) => Promise; + ): Promise; /** * Gets the API Key Client Secret for the active user. * @returns A promise that resolves with the API Key Client Secret or undefined */ - getClientSecret: (userId?: UserId) => Promise; + abstract getClientSecret(userId?: UserId): Promise; /** * Sets the two factor token for the given email in global state. @@ -131,21 +129,21 @@ export abstract class TokenService { * @param twoFactorToken The two factor token to set. * @returns A promise that resolves when the two factor token has been set. */ - setTwoFactorToken: (email: string, twoFactorToken: string) => Promise; + abstract setTwoFactorToken(email: string, twoFactorToken: string): Promise; /** * Gets the two factor token for the given email. * @param email The email to get the two factor token for. * @returns A promise that resolves with the two factor token for the given email or null if it isn't found. */ - getTwoFactorToken: (email: string) => Promise; + abstract getTwoFactorToken(email: string): Promise; /** * Clears the two factor token for the given email out of global state. * @param email The email to clear the two factor token for. * @returns A promise that resolves when the two factor token has been cleared. */ - clearTwoFactorToken: (email: string) => Promise; + abstract clearTwoFactorToken(email: string): Promise; /** * Decodes the access token. @@ -153,13 +151,13 @@ export abstract class TokenService { * If null, the currently active user's token is used. * @returns A promise that resolves with the decoded access token. */ - decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise; + abstract decodeAccessToken(tokenOrUserId?: string | UserId): Promise; /** * Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration * @returns A promise that resolves with the expiration date for the access token. */ - getTokenExpirationDate: () => Promise; + abstract getTokenExpirationDate(): Promise; /** * Calculates the adjusted time in seconds until the access token expires, considering an optional offset. @@ -170,58 +168,58 @@ export abstract class TokenService { * based on the actual expiration. * @returns {Promise} Promise resolving to the adjusted seconds remaining. */ - tokenSecondsRemaining: (offsetSeconds?: number) => Promise; + abstract tokenSecondsRemaining(offsetSeconds?: number): Promise; /** * Checks if the access token needs to be refreshed. * @param {number} [minutes=5] - Optional number of minutes before the access token expires to consider refreshing it. * @returns A promise that resolves with a boolean indicating if the access token needs to be refreshed. */ - tokenNeedsRefresh: (minutes?: number) => Promise; + abstract tokenNeedsRefresh(minutes?: number): Promise; /** * Gets the user id for the active user from the access token. * @returns A promise that resolves with the user id for the active user. * @deprecated Use AccountService.activeAccount$ instead. */ - getUserId: () => Promise; + abstract getUserId(): Promise; /** * Gets the email for the active user from the access token. * @returns A promise that resolves with the email for the active user. * @deprecated Use AccountService.activeAccount$ instead. */ - getEmail: () => Promise; + abstract getEmail(): Promise; /** * Gets the email verified status for the active user from the access token. * @returns A promise that resolves with the email verified status for the active user. */ - getEmailVerified: () => Promise; + abstract getEmailVerified(): Promise; /** * Gets the name for the active user from the access token. * @returns A promise that resolves with the name for the active user. * @deprecated Use AccountService.activeAccount$ instead. */ - getName: () => Promise; + abstract getName(): Promise; /** * Gets the issuer for the active user from the access token. * @returns A promise that resolves with the issuer for the active user. */ - getIssuer: () => Promise; + abstract getIssuer(): Promise; /** * Gets whether or not the user authenticated via an external mechanism. * @param userId The optional user id to check for external authN status; if not provided, the active user is used. * @returns A promise that resolves with a boolean representing the user's external authN status. */ - getIsExternal: (userId: UserId) => Promise; + abstract getIsExternal(userId: UserId): Promise; /** Gets the active or passed in user's security stamp */ - getSecurityStamp: (userId?: UserId) => Promise; + abstract getSecurityStamp(userId?: UserId): Promise; /** Sets the security stamp for the active or passed in user */ - setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; + abstract setSecurityStamp(securityStamp: string, userId?: UserId): Promise; } diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts index 42abc794061..275df417df2 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification-api.service.abstraction.ts @@ -1,13 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { VerifyOTPRequest } from "../../models/request/verify-otp.request"; import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response"; export abstract class UserVerificationApiServiceAbstraction { - postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise; - postAccountRequestOTP: () => Promise; - postAccountVerifyPassword: ( + abstract postAccountVerifyOTP(request: VerifyOTPRequest): Promise; + abstract postAccountRequestOTP(): Promise; + abstract postAccountVerifyPassword( request: SecretVerificationRequest, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts index 2d39854f8d9..d9749d9467c 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UserId } from "../../../types/guid"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; import { UserVerificationOptions } from "../../types/user-verification-options"; @@ -16,9 +14,9 @@ export abstract class UserVerificationService { * @param verificationType Type of verification to restrict the options to * @returns Available verification options for the user */ - getAvailableVerificationOptions: ( + abstract getAvailableVerificationOptions( verificationType: keyof UserVerificationOptions, - ) => Promise; + ): Promise; /** * Create a new request model to be used for server-side verification * @param verification User-supplied verification data (Master Password or OTP) @@ -26,11 +24,11 @@ export abstract class UserVerificationService { * @param alreadyHashed Whether the master password is already hashed * @throws Error if the verification data is invalid */ - buildRequest: ( + abstract buildRequest( verification: Verification, requestClass?: new () => T, alreadyHashed?: boolean, - ) => Promise; + ): Promise; /** * Verifies the user using the provided verification data. * PIN or biometrics are verified client-side. @@ -39,11 +37,11 @@ export abstract class UserVerificationService { * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) * @throws Error if the verification data is invalid or the verification fails */ - verifyUser: (verification: Verification) => Promise; + abstract verifyUser(verification: Verification): Promise; /** * Request a one-time password (OTP) to be sent to the user's email */ - requestOTP: () => Promise; + abstract requestOTP(): Promise; /** * Check if user has master password or can only use passwordless technologies to log in * Note: This only checks the server, not the local state @@ -51,13 +49,13 @@ export abstract class UserVerificationService { * @returns True if the user has a master password * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead */ - hasMasterPassword: (userId?: string) => Promise; + abstract hasMasterPassword(userId?: string): Promise; /** * Check if the user has a master password and has used it during their current session * @param userId The user id to check. If not provided, the current user id used * @returns True if the user has a master password and has used it in the current session */ - hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise; + abstract hasMasterPasswordAndMasterKeyHash(userId?: string): Promise; /** * Verifies the user using the provided master password. * Attempts to verify client-side first, then server-side if necessary. @@ -68,9 +66,9 @@ export abstract class UserVerificationService { * @throws Error if the master password is invalid * @returns An object containing the master key, and master password policy options if verified on server. */ - verifyUserByMasterPassword: ( + abstract verifyUserByMasterPassword( verification: MasterPasswordVerification, userId: UserId, email: string, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts index ca87710d22f..1e0fc124755 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login-api.service.abstraction.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CredentialAssertionOptionsResponse } from "../../services/webauthn-login/response/credential-assertion-options.response"; -export class WebAuthnLoginApiServiceAbstraction { - getCredentialAssertionOptions: () => Promise; +export abstract class WebAuthnLoginApiServiceAbstraction { + abstract getCredentialAssertionOptions(): Promise; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts index 5de89313ecc..d47b7ccbcef 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PrfKey } from "../../../types/key"; /** @@ -9,11 +7,11 @@ export abstract class WebAuthnLoginPrfKeyServiceAbstraction { /** * Get the salt used to generate the PRF-output used when logging in with WebAuthn. */ - getLoginWithPrfSalt: () => Promise; + abstract getLoginWithPrfSalt(): Promise; /** * Create a symmetric key from the PRF-output by stretching it. * This should be used as `ExternalKey` with `RotateableKeySet`. */ - createSymmetricKeyFromPrf: (prf: ArrayBuffer) => Promise; + abstract createSymmetricKeyFromPrf(prf: ArrayBuffer): Promise; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts index 8e6ffae27a8..c482b1a214e 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AuthResult } from "../../models/domain/auth-result"; import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion.view"; @@ -14,7 +12,7 @@ export abstract class WebAuthnLoginServiceAbstraction { * (whether FIDO2 user verification is required, the relying party id, timeout duration for the process to complete, etc.) * for the authenticator. */ - getCredentialAssertionOptions: () => Promise; + abstract getCredentialAssertionOptions(): Promise; /** * Asserts the credential. This involves user interaction with the authenticator @@ -27,9 +25,9 @@ export abstract class WebAuthnLoginServiceAbstraction { * @returns {WebAuthnLoginCredentialAssertionView} The assertion obtained from the authenticator. * If the assertion is not successfully obtained, it returns undefined. */ - assertCredential: ( + abstract assertCredential( credentialAssertionOptions: WebAuthnLoginCredentialAssertionOptionsView, - ) => Promise; + ): Promise; /** * Logs the user in using the assertion obtained from the authenticator. @@ -39,5 +37,5 @@ export abstract class WebAuthnLoginServiceAbstraction { * @param {WebAuthnLoginCredentialAssertionView} assertion - The assertion obtained from the authenticator * that needs to be validated for login. */ - logIn: (assertion: WebAuthnLoginCredentialAssertionView) => Promise; + abstract logIn(assertion: WebAuthnLoginCredentialAssertionView): Promise; } diff --git a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts index e0e8b7377c5..0f28e728ea2 100644 --- a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts @@ -1,11 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { BillingInvoiceResponse, BillingTransactionResponse, } from "../../models/response/billing.response"; -export class AccountBillingApiServiceAbstraction { - getBillingInvoices: (status?: string, startAfter?: string) => Promise; - getBillingTransactions: (startAfter?: string) => Promise; +export abstract class AccountBillingApiServiceAbstraction { + abstract getBillingInvoices( + status?: string, + startAfter?: string, + ): Promise; + abstract getBillingTransactions(startAfter?: string): Promise; } diff --git a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts index a4253226880..de9642f9194 100644 --- a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../../types/guid"; diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 21089933a59..2f3fe9125db 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,6 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore - import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; @@ -20,78 +17,78 @@ import { PaymentMethodResponse } from "../models/response/payment-method.respons import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { - cancelOrganizationSubscription: ( + abstract cancelOrganizationSubscription( organizationId: string, request: SubscriptionCancellationRequest, - ) => Promise; + ): Promise; - cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; + abstract cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise; - createProviderClientOrganization: ( + abstract createProviderClientOrganization( providerId: string, request: CreateClientOrganizationRequest, - ) => Promise; + ): Promise; - createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise; + abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise; - getOrganizationBillingMetadata: ( + abstract getOrganizationBillingMetadata( organizationId: string, - ) => Promise; + ): Promise; - getOrganizationPaymentMethod: (organizationId: string) => Promise; + abstract getOrganizationPaymentMethod(organizationId: string): Promise; - getPlans: () => Promise>; + abstract getPlans(): Promise>; - getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise; + abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise; - getProviderClientOrganizations: ( + abstract getProviderClientOrganizations( providerId: string, - ) => Promise>; + ): Promise>; - getProviderInvoices: (providerId: string) => Promise; + abstract getProviderInvoices(providerId: string): Promise; - getProviderSubscription: (providerId: string) => Promise; + abstract getProviderSubscription(providerId: string): Promise; - getProviderTaxInformation: (providerId: string) => Promise; + abstract getProviderTaxInformation(providerId: string): Promise; - updateOrganizationPaymentMethod: ( + abstract updateOrganizationPaymentMethod( organizationId: string, request: UpdatePaymentMethodRequest, - ) => Promise; + ): Promise; - updateOrganizationTaxInformation: ( + abstract updateOrganizationTaxInformation( organizationId: string, request: ExpandedTaxInfoUpdateRequest, - ) => Promise; + ): Promise; - updateProviderClientOrganization: ( + abstract updateProviderClientOrganization( providerId: string, organizationId: string, request: UpdateClientOrganizationRequest, - ) => Promise; + ): Promise; - updateProviderPaymentMethod: ( + abstract updateProviderPaymentMethod( providerId: string, request: UpdatePaymentMethodRequest, - ) => Promise; + ): Promise; - updateProviderTaxInformation: ( + abstract updateProviderTaxInformation( providerId: string, request: ExpandedTaxInfoUpdateRequest, - ) => Promise; + ): Promise; - verifyOrganizationBankAccount: ( + abstract verifyOrganizationBankAccount( organizationId: string, request: VerifyBankAccountRequest, - ) => Promise; + ): Promise; - verifyProviderBankAccount: ( + abstract verifyProviderBankAccount( providerId: string, request: VerifyBankAccountRequest, - ) => Promise; + ): Promise; - restartSubscription: ( + abstract restartSubscription( organizationId: string, request: OrganizationCreateRequest, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 58c537c99cc..113b55465a7 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -49,20 +47,22 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - getPaymentSource: (organizationId: string) => Promise; + abstract getPaymentSource(organizationId: string): Promise; - purchaseSubscription: (subscription: SubscriptionInformation) => Promise; - - purchaseSubscriptionNoPaymentMethod: ( + abstract purchaseSubscription( subscription: SubscriptionInformation, - ) => Promise; + ): Promise; - startFree: (subscription: SubscriptionInformation) => Promise; + abstract purchaseSubscriptionNoPaymentMethod( + subscription: SubscriptionInformation, + ): Promise; - restartSubscription: ( + abstract startFree(subscription: SubscriptionInformation): Promise; + + abstract restartSubscription( organizationId: string, subscription: SubscriptionInformation, - ) => Promise; + ): Promise; /** * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. diff --git a/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts b/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts index d688c7f366b..2bc99e5e5c2 100644 --- a/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts +++ b/libs/common/src/key-management/device-trust/abstractions/device-trust.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { OtherDeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request"; @@ -15,51 +13,51 @@ export abstract class DeviceTrustServiceAbstraction { * by Platform * @description Checks if the device trust feature is supported for the active user. */ - supportsDeviceTrust$: Observable; + abstract supportsDeviceTrust$: Observable; /** * Emits when a device has been trusted. This emission is specifically for the purpose of notifying * the consuming component to display a toast informing the user the device has been trusted. */ - deviceTrusted$: Observable; + abstract deviceTrusted$: Observable; /** * @description Checks if the device trust feature is supported for the given user. */ - supportsDeviceTrustByUserId$: (userId: UserId) => Observable; + abstract supportsDeviceTrustByUserId$(userId: UserId): Observable; /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset */ - getShouldTrustDevice: (userId: UserId) => Promise; - setShouldTrustDevice: (userId: UserId, value: boolean) => Promise; + abstract getShouldTrustDevice(userId: UserId): Promise; + abstract setShouldTrustDevice(userId: UserId, value: boolean): Promise; - trustDeviceIfRequired: (userId: UserId) => Promise; + abstract trustDeviceIfRequired(userId: UserId): Promise; - trustDevice: (userId: UserId) => Promise; + abstract trustDevice(userId: UserId): Promise; /** Retrieves the device key if it exists from state or secure storage if supported for the active user. */ - getDeviceKey: (userId: UserId) => Promise; - decryptUserKeyWithDeviceKey: ( + abstract getDeviceKey(userId: UserId): Promise; + abstract decryptUserKeyWithDeviceKey( userId: UserId, encryptedDevicePrivateKey: EncString, encryptedUserKey: EncString, deviceKey: DeviceKey, - ) => Promise; - rotateDevicesTrust: ( + ): Promise; + abstract rotateDevicesTrust( userId: UserId, newUserKey: UserKey, masterPasswordHash: string, - ) => Promise; + ): Promise; /** * Notifies the server that the device has a device key, but didn't receive any associated decryption keys. * Note: For debugging purposes only. */ - recordDeviceTrustLoss: () => Promise; - getRotatedData: ( + abstract recordDeviceTrustLoss(): Promise; + abstract getRotatedData( oldUserKey: UserKey, newUserKey: UserKey, userId: UserId, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts index 9ff362e4009..bcbf0029199 100644 --- a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout-settings.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { UserId } from "../../../types/guid"; @@ -13,11 +11,11 @@ export abstract class VaultTimeoutSettingsService { * @param vaultTimeoutAction The vault timeout action * @param userId The user id to set the data for. */ - setVaultTimeoutOptions: ( + abstract setVaultTimeoutOptions( userId: UserId, vaultTimeout: VaultTimeout, vaultTimeoutAction: VaultTimeoutAction, - ) => Promise; + ): Promise; /** * Get the available vault timeout actions for the current user @@ -25,13 +23,13 @@ export abstract class VaultTimeoutSettingsService { * **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes * @param userId The user id to check. If not provided, the current user is used */ - availableVaultTimeoutActions$: (userId?: string) => Observable; + abstract availableVaultTimeoutActions$(userId?: string): Observable; /** * Evaluates the user's available vault timeout actions and returns a boolean representing * if the user can lock or not */ - canLock: (userId: string) => Promise; + abstract canLock(userId: string): Promise; /** * Gets the vault timeout action for the given user id. The returned value is @@ -41,7 +39,7 @@ export abstract class VaultTimeoutSettingsService { * A new action will be emitted if the current state changes or if the user's policy changes and the new policy affects the action. * @param userId - the user id to get the vault timeout action for */ - getVaultTimeoutActionByUserId$: (userId: string) => Observable; + abstract getVaultTimeoutActionByUserId$(userId: string): Observable; /** * Get the vault timeout for the given user id. The returned value is calculated based on the current state @@ -50,14 +48,14 @@ export abstract class VaultTimeoutSettingsService { * A new timeout will be emitted if the current state changes or if the user's policy changes and the new policy affects the timeout. * @param userId The user id to get the vault timeout for */ - getVaultTimeoutByUserId$: (userId: string) => Observable; + abstract getVaultTimeoutByUserId$(userId: string): Observable; /** * Has the user enabled unlock with Biometric. * @param userId The user id to check. If not provided, the current user is used * @returns boolean true if biometric lock is set */ - isBiometricLockSet: (userId?: string) => Promise; + abstract isBiometricLockSet(userId?: string): Promise; - clear: (userId: UserId) => Promise; + abstract clear(userId: UserId): Promise; } diff --git a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts index cb07c7d193a..1c88a5c51ea 100644 --- a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export abstract class VaultTimeoutService { - checkVaultTimeout: () => Promise; - lock: (userId?: string) => Promise; - logOut: (userId?: string) => Promise; + abstract checkVaultTimeout(): Promise; + abstract lock(userId?: string): Promise; + abstract logOut(userId?: string): Promise; } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index fd3453198e6..c34c4b835cf 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; /** @@ -17,11 +15,11 @@ export abstract class Fido2AuthenticatorService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the new credential and an attestation signature. **/ - makeCredential: ( + abstract makeCredential( params: Fido2AuthenticatorMakeCredentialsParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; /** * Generate an assertion using an existing credential as describe in: @@ -31,11 +29,11 @@ export abstract class Fido2AuthenticatorService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the asserted credential and an assertion signature. */ - getAssertion: ( + abstract getAssertion( params: Fido2AuthenticatorGetAssertionParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; /** * Discover credentials for a given Relying Party @@ -43,7 +41,7 @@ export abstract class Fido2AuthenticatorService { * @param rpId The Relying Party's ID * @returns A promise that resolves with an array of discoverable credentials */ - silentCredentialDiscovery: (rpId: string) => Promise; + abstract silentCredentialDiscovery(rpId: string): Promise; } // FIXME: update to use a const object instead of a typescript enum diff --git a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts index 55d9cce8049..f1ad26673fd 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore export const UserRequestedFallbackAbortReason = "UserRequestedFallback"; export type UserVerification = "discouraged" | "preferred" | "required"; @@ -16,7 +14,7 @@ export type UserVerification = "discouraged" | "preferred" | "required"; * and for returning the results of the latter operations to the Web Authentication API's callers. */ export abstract class Fido2ClientService { - isFido2FeatureEnabled: (hostname: string, origin: string) => Promise; + abstract isFido2FeatureEnabled(hostname: string, origin: string): Promise; /** * Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source. @@ -26,11 +24,11 @@ export abstract class Fido2ClientService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the new credential. */ - createCredential: ( + abstract createCredential( params: CreateCredentialParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; /** * Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s consent. @@ -41,11 +39,11 @@ export abstract class Fido2ClientService { * @param abortController An AbortController that can be used to abort the operation. * @returns A promise that resolves with the asserted credential. */ - assertCredential: ( + abstract assertCredential( params: AssertCredentialParams, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; } /** diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 1f871f6c70f..28b199da78f 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore /** * Parameters used to ask the user to confirm the creation of a new credential. */ @@ -69,11 +67,11 @@ export abstract class Fido2UserInterfaceService { * @param fallbackSupported Whether or not the browser natively supports WebAuthn. * @param abortController An abort controller that can be used to cancel/close the session. */ - newSession: ( + abstract newSession( fallbackSupported: boolean, window: ParentWindowReference, abortController?: AbortController, - ) => Promise; + ): Promise; } export abstract class Fido2UserInterfaceSession { @@ -84,9 +82,9 @@ export abstract class Fido2UserInterfaceSession { * @param abortController An abort controller that can be used to cancel/close the session. * @returns The ID of the cipher that contains the credentials the user picked. If not cipher was picked, return cipherId = undefined to to let the authenticator throw the error. */ - pickCredential: ( + abstract pickCredential( params: PickCredentialParams, - ) => Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId: string; userVerified: boolean }>; /** * Ask the user to confirm the creation of a new credential. @@ -95,30 +93,30 @@ export abstract class Fido2UserInterfaceSession { * @param abortController An abort controller that can be used to cancel/close the session. * @returns The ID of the cipher where the new credential should be saved. */ - confirmNewCredential: ( + abstract confirmNewCredential( params: NewCredentialParams, - ) => Promise<{ cipherId: string; userVerified: boolean }>; + ): Promise<{ cipherId: string; userVerified: boolean }>; /** * Make sure that the vault is unlocked. * This will open a window and ask the user to login or unlock the vault if necessary. */ - ensureUnlockedVault: () => Promise; + abstract ensureUnlockedVault(): Promise; /** * Inform the user that the operation was cancelled because their vault contains excluded credentials. * * @param existingCipherIds The IDs of the excluded credentials. */ - informExcludedCredential: (existingCipherIds: string[]) => Promise; + abstract informExcludedCredential(existingCipherIds: string[]): Promise; /** * Inform the user that the operation was cancelled because their vault does not contain any useable credentials. */ - informCredentialNotFound: (abortController?: AbortController) => Promise; + abstract informCredentialNotFound(abortController?: AbortController): Promise; /** * Close the session, including any windows that may be open. */ - close: () => void; + abstract close(): void; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index e4dbe76d7e4..4c1c000284e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { BiometricKey } from "../../auth/types/biometric-key"; import { Account } from "../models/domain/account"; import { StorageOptions } from "../models/domain/storage-options"; @@ -19,47 +17,47 @@ export type InitOptions = { }; export abstract class StateService { - addAccount: (account: T) => Promise; - clean: (options?: StorageOptions) => Promise; - init: (initOptions?: InitOptions) => Promise; + abstract addAccount(account: T): Promise; + abstract clean(options?: StorageOptions): Promise; + abstract init(initOptions?: InitOptions): Promise; /** * Gets the user's auto key */ - getUserKeyAutoUnlock: (options?: StorageOptions) => Promise; + abstract getUserKeyAutoUnlock(options?: StorageOptions): Promise; /** * Sets the user's auto key */ - setUserKeyAutoUnlock: (value: string | null, options?: StorageOptions) => Promise; + abstract setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise; /** * Gets the user's biometric key */ - getUserKeyBiometric: (options?: StorageOptions) => Promise; + abstract getUserKeyBiometric(options?: StorageOptions): Promise; /** * Checks if the user has a biometric key available */ - hasUserKeyBiometric: (options?: StorageOptions) => Promise; + abstract hasUserKeyBiometric(options?: StorageOptions): Promise; /** * Sets the user's biometric key */ - setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise; + abstract setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise; /** * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService */ - setEnableDuckDuckGoBrowserIntegration: ( + abstract setEnableDuckDuckGoBrowserIntegration( value: boolean, options?: StorageOptions, - ) => Promise; - getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise; - setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; + ): Promise; + abstract getDuckDuckGoSharedKey(options?: StorageOptions): Promise; + abstract setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise; /** * @deprecated Use `TokenService.hasAccessToken$()` or `AuthService.authStatusFor$` instead. */ - getIsAuthenticated: (options?: StorageOptions) => Promise; + abstract getIsAuthenticated(options?: StorageOptions): Promise; /** * @deprecated Use `AccountService.activeAccount$` instead. */ - getUserId: (options?: StorageOptions) => Promise; + abstract getUserId(options?: StorageOptions): Promise; } diff --git a/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts b/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts index a49a6d481b5..ccc47d487a4 100644 --- a/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts +++ b/libs/common/src/tools/password-strength/password-strength.service.abstraction.ts @@ -1,7 +1,9 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ZXCVBNResult } from "zxcvbn"; export abstract class PasswordStrengthServiceAbstraction { - getPasswordStrength: (password: string, email?: string, userInputs?: string[]) => ZXCVBNResult; + abstract getPasswordStrength( + password: string, + email?: string, + userInputs?: string[], + ): ZXCVBNResult; } diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index 570f3e746a0..80c4410af11 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { ListResponse } from "../../../models/response/list.response"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { Send } from "../models/domain/send"; @@ -12,26 +10,29 @@ import { SendResponse } from "../models/response/send.response"; import { SendAccessView } from "../models/view/send-access.view"; export abstract class SendApiService { - getSend: (id: string) => Promise; - postSendAccess: ( + abstract getSend(id: string): Promise; + abstract postSendAccess( id: string, request: SendAccessRequest, apiUrl?: string, - ) => Promise; - getSends: () => Promise>; - postSend: (request: SendRequest) => Promise; - postFileTypeSend: (request: SendRequest) => Promise; - postSendFile: (sendId: string, fileId: string, data: FormData) => Promise; - putSend: (id: string, request: SendRequest) => Promise; - putSendRemovePassword: (id: string) => Promise; - deleteSend: (id: string) => Promise; - getSendFileDownloadData: ( + ): Promise; + abstract getSends(): Promise>; + abstract postSend(request: SendRequest): Promise; + abstract postFileTypeSend(request: SendRequest): Promise; + abstract postSendFile(sendId: string, fileId: string, data: FormData): Promise; + abstract putSend(id: string, request: SendRequest): Promise; + abstract putSendRemovePassword(id: string): Promise; + abstract deleteSend(id: string): Promise; + abstract getSendFileDownloadData( send: SendAccessView, request: SendAccessRequest, apiUrl?: string, - ) => Promise; - renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise; - removePassword: (id: string) => Promise; - delete: (id: string) => Promise; - save: (sendData: [Send, EncArrayBuffer]) => Promise; + ): Promise; + abstract renewSendFileUploadUrl( + sendId: string, + fileId: string, + ): Promise; + abstract removePassword(id: string): Promise; + abstract delete(id: string): Promise; + abstract save(sendData: [Send, EncArrayBuffer]): Promise; } diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index f586e39a755..8301172477c 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -16,49 +14,49 @@ import { SendWithIdRequest } from "../models/request/send-with-id.request"; import { SendView } from "../models/view/send.view"; export abstract class SendService implements UserKeyRotationDataProvider { - sends$: Observable; - sendViews$: Observable; + abstract sends$: Observable; + abstract sendViews$: Observable; - encrypt: ( + abstract encrypt( model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey, - ) => Promise<[Send, EncArrayBuffer]>; + ): Promise<[Send, EncArrayBuffer]>; /** * Provides a send for a determined id * updates after a change occurs to the send that matches the id * @param id The id of the desired send * @returns An observable that listens to the value of the desired send */ - get$: (id: string) => Observable; + abstract get$(id: string): Observable; /** * Provides re-encrypted user sends for the key rotation process * @param newUserKey The new user key to use for re-encryption * @throws Error if the new user key is null or undefined * @returns A list of user sends that have been re-encrypted with the new user key */ - getRotatedData: ( + abstract getRotatedData( originalUserKey: UserKey, newUserKey: UserKey, userId: UserId, - ) => Promise; + ): Promise; /** * @deprecated Do not call this, use the sends$ observable collection */ - getAll: () => Promise; + abstract getAll(): Promise; /** * @deprecated Only use in CLI */ - getFromState: (id: string) => Promise; + abstract getFromState(id: string): Promise; /** * @deprecated Only use in CLI */ - getAllDecryptedFromState: (userId: UserId) => Promise; + abstract getAllDecryptedFromState(userId: UserId): Promise; } export abstract class InternalSendService extends SendService { - upsert: (send: SendData | SendData[]) => Promise; - replace: (sends: { [id: string]: SendData }, userId: UserId) => Promise; - delete: (id: string | string[]) => Promise; + abstract upsert(send: SendData | SendData[]): Promise; + abstract replace(sends: { [id: string]: SendData }, userId: UserId): Promise; + abstract delete(id: string | string[]): Promise; } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 2f186369463..2f4fcf0ef51 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. diff --git a/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts b/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts index 812439e2ca9..13c79241e36 100644 --- a/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts +++ b/libs/common/src/vault/abstractions/file-upload/cipher-file-upload.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -7,11 +5,11 @@ import { Cipher } from "../../models/domain/cipher"; import { CipherResponse } from "../../models/response/cipher.response"; export abstract class CipherFileUploadService { - upload: ( + abstract upload( cipher: Cipher, encFileName: EncString, encData: EncArrayBuffer, admin: boolean, dataEncKey: [SymmetricCryptoKey, EncString], - ) => Promise; + ): Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index 1bb4a52e929..1b89f1664ca 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -1,14 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore - import { UserId } from "../../../types/guid"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; -export class FolderApiServiceAbstraction { - save: (folder: Folder, userId: UserId) => Promise; - delete: (id: string, userId: UserId) => Promise; - get: (id: string) => Promise; - deleteAll: (userId: UserId) => Promise; +export abstract class FolderApiServiceAbstraction { + abstract save(folder: Folder, userId: UserId): Promise; + abstract delete(id: string, userId: UserId): Promise; + abstract get(id: string): Promise; + abstract deleteAll(userId: UserId): Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index 7324fe22c8d..e56bfda32a4 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -15,27 +13,27 @@ import { FolderWithIdRequest } from "../../models/request/folder-with-id.request import { FolderView } from "../../models/view/folder.view"; export abstract class FolderService implements UserKeyRotationDataProvider { - folders$: (userId: UserId) => Observable; - folderViews$: (userId: UserId) => Observable; + abstract folders$(userId: UserId): Observable; + abstract folderViews$(userId: UserId): Observable; - clearDecryptedFolderState: (userId: UserId) => Promise; - encrypt: (model: FolderView, key: SymmetricCryptoKey) => Promise; - get: (id: string, userId: UserId) => Promise; - getDecrypted$: (id: string, userId: UserId) => Observable; + abstract clearDecryptedFolderState(userId: UserId): Promise; + abstract encrypt(model: FolderView, key: SymmetricCryptoKey): Promise; + abstract get(id: string, userId: UserId): Promise; + abstract getDecrypted$(id: string, userId: UserId): Observable; /** * @deprecated Use firstValueFrom(folders$) directly instead * @param userId The user id * @returns Promise of folders array */ - getAllFromState: (userId: UserId) => Promise; + abstract getAllFromState(userId: UserId): Promise; /** * @deprecated Only use in CLI! */ - getFromState: (id: string, userId: UserId) => Promise; + abstract getFromState(id: string, userId: UserId): Promise; /** * @deprecated Only use in CLI! */ - getAllDecryptedFromState: (userId: UserId) => Promise; + abstract getAllDecryptedFromState(userId: UserId): Promise; /** * Returns user folders re-encrypted with the new user key. * @param originalUserKey the original user key @@ -44,16 +42,16 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; + ): Promise; } export abstract class InternalFolderService extends FolderService { - upsert: (folder: FolderData | FolderData[], userId: UserId) => Promise; - replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise; - clear: (userId: UserId) => Promise; - delete: (id: string | string[], userId: UserId) => Promise; + abstract upsert(folder: FolderData | FolderData[], userId: UserId): Promise; + abstract replace(folders: { [id: string]: FolderData }, userId: UserId): Promise; + abstract clear(userId: UserId): Promise; + abstract delete(id: string | string[], userId: UserId): Promise; } diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts index ed8bb2c3baf..57f301261c2 100644 --- a/libs/common/src/vault/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { SendView } from "../../tools/send/models/view/send.view"; @@ -8,25 +6,25 @@ import { CipherView } from "../models/view/cipher.view"; import { CipherViewLike } from "../utils/cipher-view-like-utils"; export abstract class SearchService { - indexedEntityId$: (userId: UserId) => Observable; + abstract indexedEntityId$(userId: UserId): Observable; - clearIndex: (userId: UserId) => Promise; - isSearchable: (userId: UserId, query: string) => Promise; - indexCiphers: ( + abstract clearIndex(userId: UserId): Promise; + abstract isSearchable(userId: UserId, query: string): Promise; + abstract indexCiphers( userId: UserId, ciphersToIndex: CipherView[], indexedEntityGuid?: string, - ) => Promise; - searchCiphers: ( + ): Promise; + abstract searchCiphers( userId: UserId, query: string, filter?: ((cipher: C) => boolean) | ((cipher: C) => boolean)[], ciphers?: C[], - ) => Promise; - searchCiphersBasic: ( + ): Promise; + abstract searchCiphersBasic( ciphers: C[], query: string, deleted?: boolean, - ) => C[]; - searchSends: (sends: SendView[], query: string) => SendView[]; + ): C[]; + abstract searchSends(sends: SendView[], query: string): SendView[]; } diff --git a/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts b/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts index ea1e73c2685..01b0011b7f7 100644 --- a/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts +++ b/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; /** * Service for managing vault settings. @@ -9,42 +7,40 @@ export abstract class VaultSettingsService { * An observable monitoring the state of the enable passkeys setting. * The observable updates when the setting changes. */ - enablePasskeys$: Observable; + abstract enablePasskeys$: Observable; /** * An observable monitoring the state of the show cards on the current tab. */ - showCardsCurrentTab$: Observable; + abstract showCardsCurrentTab$: Observable; /** * An observable monitoring the state of the show identities on the current tab. */ - showIdentitiesCurrentTab$: Observable; - /** + abstract showIdentitiesCurrentTab$: Observable; /** * An observable monitoring the state of the click items on the Vault view * for Autofill suggestions. */ - clickItemsToAutofillVaultView$: Observable; - /** + abstract clickItemsToAutofillVaultView$: Observable; /** * Saves the enable passkeys setting to disk. * @param value The new value for the passkeys setting. */ - setEnablePasskeys: (value: boolean) => Promise; + abstract setEnablePasskeys(value: boolean): Promise; /** * Saves the show cards on tab page setting to disk. * @param value The new value for the show cards on tab page setting. */ - setShowCardsCurrentTab: (value: boolean) => Promise; + abstract setShowCardsCurrentTab(value: boolean): Promise; /** * Saves the show identities on tab page setting to disk. * @param value The new value for the show identities on tab page setting. */ - setShowIdentitiesCurrentTab: (value: boolean) => Promise; + abstract setShowIdentitiesCurrentTab(value: boolean): Promise; /** * Saves the click items on vault View for Autofill suggestions to disk. * @param value The new value for the click items on vault View for * Autofill suggestions setting. */ - setClickItemsToAutofillVaultView: (value: boolean) => Promise; + abstract setClickItemsToAutofillVaultView(value: boolean): Promise; } From 78353a988249bf109b4017b71ce8ebcc2393d496 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:04:23 -0400 Subject: [PATCH 017/179] fix(rpm): [PM-527] Remove build id links on rpm build --- apps/desktop/electron-builder.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 832ab9d0bd3..800cdd848a7 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -240,7 +240,8 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "rpm": { - "artifactName": "${productName}-${version}-${arch}.${ext}" + "artifactName": "${productName}-${version}-${arch}.${ext}", + "fpm": ["--rpm-rpmbuild-define", "_build_id_links none"] }, "freebsd": { "artifactName": "${productName}-${version}-${arch}.${ext}" From d0fc9e9a2b1fbf5457736e5d80c5eb271187f638 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:19:26 -0500 Subject: [PATCH 018/179] [PM-19589] Update delete organization user event log message (#15714) * chore: update key and message with new content, refs PM-19589 * chore: update reference to new message key, refs PM-19589 * chore: update message based on product/design review, refs PM-19589 --- apps/web/src/app/core/event.service.ts | 4 ++-- apps/web/src/locales/en/messages.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 14c87181f62..36d591cc390 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -342,9 +342,9 @@ export class EventService { ); break; case EventType.OrganizationUser_Deleted: - msg = this.i18nService.t("deletedUserId", this.formatOrgUserId(ev)); + msg = this.i18nService.t("deletedUserIdEventMessage", this.formatOrgUserId(ev)); humanReadableMsg = this.i18nService.t( - "deletedUserId", + "deletedUserIdEventMessage", this.getShortId(ev.organizationUserId), ); break; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9c9ecc79721..5d7cbd7d479 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10295,8 +10295,8 @@ "organizationUserDeletedDesc": { "message": "The user was removed from the organization and all associated user data has been deleted." }, - "deletedUserId": { - "message": "Deleted user $ID$ - an owner / admin deleted the user account", + "deletedUserIdEventMessage": { + "message": "Deleted user $ID$", "placeholders": { "id": { "content": "$1", From 319528c647ad7e57cbc6f505ab390500696223c0 Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Tue, 22 Jul 2025 14:45:59 -0400 Subject: [PATCH 019/179] Only call activeAccount$ when activeAccountStatus$ is Unlocked. (#15626) --- .../background/auto-submit-login.background.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.ts b/apps/browser/src/autofill/background/auto-submit-login.background.ts index dcafe21b63c..dfdfa0f4d67 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.ts @@ -1,12 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, switchMap } from "rxjs"; +import { filter, firstValueFrom, of, switchMap } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -51,9 +51,14 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr * Initializes the auto-submit login policy. If the policy is not enabled, it * will trigger a removal of any established listeners. */ + async init() { - this.accountService.activeAccount$ + this.authService.activeAccountStatus$ .pipe( + switchMap((value) => + value === AuthenticationStatus.Unlocked ? this.accountService.activeAccount$ : of(null), + ), + filter((account): account is Account => account !== null), getUserId, switchMap((userId) => this.policyService.policiesByType$(PolicyType.AutomaticAppLogIn, userId), From 53aaa2c285261e706bb11914b9ce5b85b589d6b0 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Tue, 22 Jul 2025 15:41:33 -0400 Subject: [PATCH 020/179] Update tsconfig and package json (#15636) --- .../package-lock.json | 16 ++++++++++++++++ .../native-messaging-test-runner/package.json | 6 +++++- .../native-messaging-test-runner/tsconfig.json | 12 ++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 37b8cf96ff3..043393df58b 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -10,7 +10,9 @@ "license": "GPL-3.0", "dependencies": { "@bitwarden/common": "file:../../../libs/common", + "@bitwarden/logging": "dist/libs/logging/src", "@bitwarden/node": "file:../../../libs/node", + "@bitwarden/storage-core": "file:../../../libs/storage-core", "module-alias": "2.2.3", "ts-node": "10.9.2", "uuid": "11.1.0", @@ -31,14 +33,28 @@ "version": "0.0.0", "license": "GPL-3.0" }, + "../../../libs/storage-core": { + "name": "@bitwarden/storage-core", + "version": "0.0.1", + "license": "GPL-3.0" + }, + "dist/libs/logging/src": {}, "node_modules/@bitwarden/common": { "resolved": "../../../libs/common", "link": true }, + "node_modules/@bitwarden/logging": { + "resolved": "dist/libs/logging/src", + "link": true + }, "node_modules/@bitwarden/node": { "resolved": "../../../libs/node", "link": true }, + "node_modules/@bitwarden/storage-core": { + "resolved": "../../../libs/storage-core", + "link": true + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index ea6b1b3e7a8..56e3e4edcf8 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -16,6 +16,8 @@ "dependencies": { "@bitwarden/common": "file:../../../libs/common", "@bitwarden/node": "file:../../../libs/node", + "@bitwarden/storage-core": "file:../../../libs/storage-core", + "@bitwarden/logging": "dist/libs/logging/src", "module-alias": "2.2.3", "ts-node": "10.9.2", "uuid": "11.1.0", @@ -27,6 +29,8 @@ }, "_moduleAliases": { "@bitwarden/common": "dist/libs/common/src", - "@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service" + "@bitwarden/node/services/node-crypto-function.service": "dist/libs/node/src/services/node-crypto-function.service", + "@bitwarden/storage-core": "dist/libs/storage-core/src", + "@bitwarden/logging": "dist/libs/logging/src" } } diff --git a/apps/desktop/native-messaging-test-runner/tsconfig.json b/apps/desktop/native-messaging-test-runner/tsconfig.json index 608e5a3bf4c..dcdf992f986 100644 --- a/apps/desktop/native-messaging-test-runner/tsconfig.json +++ b/apps/desktop/native-messaging-test-runner/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "../tsconfig", "compilerOptions": { + "baseUrl": "./", "outDir": "dist", "target": "es6", "module": "CommonJS", @@ -10,7 +10,15 @@ "sourceMap": false, "declaration": false, "paths": { - "@src/*": ["src/*"] + "@src/*": ["src/*"], + "@bitwarden/user-core": ["../../../libs/user-core/src/index.ts"], + "@bitwarden/storage-core": ["../../../libs/storage-core/src/index.ts"], + "@bitwarden/logging": ["../../../libs/logging/src/index.ts"], + "@bitwarden/admin-console/*": ["../../../libs/admin-console/src/*"], + "@bitwarden/auth/*": ["../../../libs/auth/src/*"], + "@bitwarden/common/*": ["../../../libs/common/src/*"], + "@bitwarden/key-management": ["../../../libs/key-management/src/"], + "@bitwarden/node/*": ["../../../libs/node/src/*"] }, "plugins": [ { From c2bbb7c0312f27bb6a5455d43f693e0ae1918495 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 21:59:42 +0200 Subject: [PATCH 021/179] Migrate vault abstract services to strict ts (#15731) --- .../src/vault/abstractions/deprecated-vault-filter.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts index 9a1a31b6068..30a4c6d4739 100644 --- a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts +++ b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. From 54f0852f1a4c306fb5d087318ac429d7c3f10811 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 22:00:07 +0200 Subject: [PATCH 022/179] Migrate auth abstract services to strict ts (#15732) --- .../set-password-jit.service.abstraction.ts | 4 +-- .../auth-request.service.abstraction.ts | 36 +++++++++---------- .../abstractions/login-strategy.service.ts | 26 +++++++------- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts index da6e9368007..92db88868a2 100644 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts +++ b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey } from "@bitwarden/common/types/key"; import { KdfConfig } from "@bitwarden/key-management"; @@ -31,5 +29,5 @@ export abstract class SetPasswordJitService { * @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey * or newKeyPair could not be created. */ - setPassword: (credentials: SetPasswordCredentials) => Promise; + abstract setPassword(credentials: SetPasswordCredentials): Promise; } diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 7e480c3a69c..9eea3fe7bb0 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; @@ -10,20 +8,20 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { /** Emits an auth request id when an auth request has been approved. */ - authRequestPushNotification$: Observable; + abstract authRequestPushNotification$: Observable; /** * Emits when a login has been approved by an admin. This emission is specifically for the * purpose of notifying the consuming component to display a toast informing the user. */ - adminLoginApproved$: Observable; + abstract adminLoginApproved$: Observable; /** * Returns an admin auth request for the given user if it exists. * @param userId The user id. * @throws If `userId` is not provided. */ - abstract getAdminAuthRequest: (userId: UserId) => Promise; + abstract getAdminAuthRequest(userId: UserId): Promise; /** * Sets an admin auth request for the given user. * Note: use {@link clearAdminAuthRequest} to clear the request. @@ -31,16 +29,16 @@ export abstract class AuthRequestServiceAbstraction { * @param userId The user id. * @throws If `authRequest` or `userId` is not provided. */ - abstract setAdminAuthRequest: ( + abstract setAdminAuthRequest( authRequest: AdminAuthRequestStorable, userId: UserId, - ) => Promise; + ): Promise; /** * Clears an admin auth request for the given user. * @param userId The user id. * @throws If `userId` is not provided. */ - abstract clearAdminAuthRequest: (userId: UserId) => Promise; + abstract clearAdminAuthRequest(userId: UserId): Promise; /** * Gets a list of standard pending auth requests for the user. * @returns An observable of an array of auth request. @@ -61,42 +59,42 @@ export abstract class AuthRequestServiceAbstraction { * approval was successful. * @throws If the auth request is missing an id or key. */ - abstract approveOrDenyAuthRequest: ( + abstract approveOrDenyAuthRequest( approve: boolean, authRequest: AuthRequestResponse, - ) => Promise; + ): Promise; /** * Sets the `UserKey` from an auth request. Auth request must have a `UserKey`. * @param authReqResponse The auth request. * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. * @param userId The ID of the user for whose account we will set the key. */ - abstract setUserKeyAfterDecryptingSharedUserKey: ( + abstract setUserKeyAfterDecryptingSharedUserKey( authReqResponse: AuthRequestResponse, authReqPrivateKey: ArrayBuffer, userId: UserId, - ) => Promise; + ): Promise; /** * Sets the `MasterKey` and `MasterKeyHash` from an auth request. Auth request must have a `MasterKey` and `MasterKeyHash`. * @param authReqResponse The auth request. * @param authReqPrivateKey The private key corresponding to the public key sent in the auth request. * @param userId The ID of the user for whose account we will set the keys. */ - abstract setKeysAfterDecryptingSharedMasterKeyAndHash: ( + abstract setKeysAfterDecryptingSharedMasterKeyAndHash( authReqResponse: AuthRequestResponse, authReqPrivateKey: ArrayBuffer, userId: UserId, - ) => Promise; + ): Promise; /** * Decrypts a `UserKey` from a public key encrypted `UserKey`. * @param pubKeyEncryptedUserKey The public key encrypted `UserKey`. * @param privateKey The private key corresponding to the public key used to encrypt the `UserKey`. * @returns The decrypted `UserKey`. */ - abstract decryptPubKeyEncryptedUserKey: ( + abstract decryptPubKeyEncryptedUserKey( pubKeyEncryptedUserKey: string, privateKey: ArrayBuffer, - ) => Promise; + ): Promise; /** * Decrypts a `MasterKey` and `MasterKeyHash` from a public key encrypted `MasterKey` and `MasterKeyHash`. * @param pubKeyEncryptedMasterKey The public key encrypted `MasterKey`. @@ -104,18 +102,18 @@ export abstract class AuthRequestServiceAbstraction { * @param privateKey The private key corresponding to the public key used to encrypt the `MasterKey` and `MasterKeyHash`. * @returns The decrypted `MasterKey` and `MasterKeyHash`. */ - abstract decryptPubKeyEncryptedMasterKeyAndHash: ( + abstract decryptPubKeyEncryptedMasterKeyAndHash( pubKeyEncryptedMasterKey: string, pubKeyEncryptedMasterKeyHash: string, privateKey: ArrayBuffer, - ) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>; + ): Promise<{ masterKey: MasterKey; masterKeyHash: string }>; /** * Handles incoming auth request push notifications. * @param notification push notification. * @remark We should only be receiving approved push notifications to prevent enumeration. */ - abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void; + abstract sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void; /** * Creates a dash-delimited fingerprint for use in confirming the `AuthRequest` between the requesting and approving device. diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index b0fffae2ab4..64854393240 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -20,60 +18,60 @@ export abstract class LoginStrategyServiceAbstraction { * The current strategy being used to authenticate. * Emits null if the session has timed out. */ - currentAuthType$: Observable; + abstract currentAuthType$: Observable; /** * If the login strategy uses the email address of the user, this * will return it. Otherwise, it will return null. */ - getEmail: () => Promise; + abstract getEmail(): Promise; /** * If the user is logging in with a master password, this will return * the master password hash. Otherwise, it will return null. */ - getMasterPasswordHash: () => Promise; + abstract getMasterPasswordHash(): Promise; /** * If the user is logging in with SSO, this will return * the email auth token. Otherwise, it will return null. * @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken} */ - getSsoEmail2FaSessionToken: () => Promise; + abstract getSsoEmail2FaSessionToken(): Promise; /** * Returns the access code if the user is logging in with an * Auth Request. Otherwise, it will return null. */ - getAccessCode: () => Promise; + abstract getAccessCode(): Promise; /** * Returns the auth request ID if the user is logging in with an * Auth Request. Otherwise, it will return null. */ - getAuthRequestId: () => Promise; + abstract getAuthRequestId(): Promise; /** * Sends a token request to the server using the provided credentials. */ - logIn: ( + abstract logIn( credentials: | UserApiLoginCredentials | PasswordLoginCredentials | SsoLoginCredentials | AuthRequestLoginCredentials | WebAuthnLoginCredentials, - ) => Promise; + ): Promise; /** * Sends a token request to the server with the provided two factor token. * This uses data stored from {@link LoginStrategyServiceAbstraction.logIn}, so that must be called first. * Returns an error if no session data is found. */ - logInTwoFactor: (twoFactor: TokenTwoFactorRequest) => Promise; + abstract logInTwoFactor(twoFactor: TokenTwoFactorRequest): Promise; /** * Creates a master key from the provided master password and email. */ - makePreloginKey: (masterPassword: string, email: string) => Promise; + abstract makePreloginKey(masterPassword: string, email: string): Promise; /** * Emits true if the authentication session has expired. */ - authenticationSessionTimeout$: Observable; + abstract get authenticationSessionTimeout$(): Observable; /** * Sends a token request to the server with the provided device verification OTP. */ - logInNewDeviceVerification: (deviceVerificationOtp: string) => Promise; + abstract logInNewDeviceVerification(deviceVerificationOtp: string): Promise; } From c37965174b4288789f06ff9be425da70c6d92e86 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Jul 2025 22:00:24 +0200 Subject: [PATCH 023/179] Migrate platform owned abstract service to strict ts (#15734) --- .../fido2-active-request-manager.abstraction.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts index ffb78d51bd3..390a6f4e5bd 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-active-request-manager.abstraction.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable, Subject } from "rxjs"; import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; @@ -25,13 +23,13 @@ export interface ActiveRequest { export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>; export abstract class Fido2ActiveRequestManager { - getActiveRequest$: (tabId: number) => Observable; - getActiveRequest: (tabId: number) => ActiveRequest | undefined; - newActiveRequest: ( + abstract getActiveRequest$(tabId: number): Observable; + abstract getActiveRequest(tabId: number): ActiveRequest | undefined; + abstract newActiveRequest( tabId: number, credentials: Fido2CredentialView[], abortController: AbortController, - ) => Promise; - removeActiveRequest: (tabId: number) => void; - removeAllActiveRequests: () => void; + ): Promise; + abstract removeActiveRequest(tabId: number): void; + abstract removeAllActiveRequests(): void; } From 643d0c9a4c8eeafb36bd103e9ff84cfae2cb6c23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:56:08 -0700 Subject: [PATCH 024/179] [deps] Vault: Update form-data to v4.0.4 [SECURITY] (#15712) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 12 +++++++----- package.json | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 54855d72104..0d3c151f012 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -70,7 +70,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", diff --git a/package-lock.json b/package-lock.json index fbbc4c25b44..85bd2a0efb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", @@ -206,7 +206,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", @@ -358,6 +358,7 @@ "license": "GPL-3.0" }, "libs/messaging-internal": { + "name": "@bitwarden/messaging-internal", "version": "0.0.1", "license": "GPL-3.0" }, @@ -21194,14 +21195,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { diff --git a/package.json b/package.json index 2cb60a6afd1..bcf4f326531 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.44.0", - "form-data": "4.0.2", + "form-data": "4.0.4", "https-proxy-agent": "7.0.6", "inquirer": "8.2.6", "jsdom": "26.1.0", From 2f47add6f157db355603c072fbde9cd4881b4ad0 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:08:09 -0500 Subject: [PATCH 025/179] [PM-23596] Redirect to `/setup-extension` (#15641) * remove current redirection from auth code * update timeouts of the web browser interaction * add guard for setup-extension page * decrease timeout to 25ms * avoid redirection for mobile users + add tests * add tests * condense variables * catch error from profile fetch --------- Co-authored-by: Shane Melton --- .../web-registration-finish.service.spec.ts | 22 --- .../web-registration-finish.service.ts | 15 -- apps/web/src/app/core/core.module.ts | 1 - apps/web/src/app/oss-routing.module.ts | 2 + .../add-extension-later-dialog.component.html | 8 +- ...d-extension-later-dialog.component.spec.ts | 14 ++ .../add-extension-later-dialog.component.ts | 17 ++- .../setup-extension.component.spec.ts | 16 +++ .../setup-extension.component.ts | 30 +++- .../setup-extension-redirect.guard.spec.ts | 132 ++++++++++++++++++ .../guards/setup-extension-redirect.guard.ts | 109 +++++++++++++++ .../web-browser-interaction.service.spec.ts | 6 +- .../web-browser-interaction.service.ts | 17 ++- .../default-registration-finish.service.ts | 4 - .../registration-finish.component.ts | 3 +- .../registration-finish.service.ts | 5 - .../src/platform/state/state-definitions.ts | 7 + 17 files changed, 347 insertions(+), 61 deletions(-) create mode 100644 apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts create mode 100644 apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 69a2f27a322..845df89622b 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -12,7 +12,6 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; @@ -30,7 +29,6 @@ describe("WebRegistrationFinishService", () => { let policyApiService: MockProxy; let logService: MockProxy; let policyService: MockProxy; - let configService: MockProxy; beforeEach(() => { keyService = mock(); @@ -39,7 +37,6 @@ describe("WebRegistrationFinishService", () => { policyApiService = mock(); logService = mock(); policyService = mock(); - configService = mock(); service = new WebRegistrationFinishService( keyService, @@ -48,7 +45,6 @@ describe("WebRegistrationFinishService", () => { policyApiService, logService, policyService, - configService, ); }); @@ -414,22 +410,4 @@ describe("WebRegistrationFinishService", () => { ); }); }); - - describe("determineLoginSuccessRoute", () => { - it("returns /setup-extension when the end user activation feature flag is enabled", async () => { - configService.getFeatureFlag.mockResolvedValue(true); - - const result = await service.determineLoginSuccessRoute(); - - expect(result).toBe("/setup-extension"); - }); - - it("returns /vault when the end user activation feature flag is disabled", async () => { - configService.getFeatureFlag.mockResolvedValue(false); - - const result = await service.determineLoginSuccessRoute(); - - expect(result).toBe("/vault"); - }); - }); }); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts index a3774e87db8..a9eba08be8c 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -14,12 +14,10 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptedString, EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { KeyService } from "@bitwarden/key-management"; @@ -34,7 +32,6 @@ export class WebRegistrationFinishService private policyApiService: PolicyApiServiceAbstraction, private logService: LogService, private policyService: PolicyService, - private configService: ConfigService, ) { super(keyService, accountApiService); } @@ -79,18 +76,6 @@ export class WebRegistrationFinishService return masterPasswordPolicyOpts; } - override async determineLoginSuccessRoute(): Promise { - const endUserActivationFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM19315EndUserActivationMvp, - ); - - if (endUserActivationFlagEnabled) { - return "/setup-extension"; - } else { - return super.determineLoginSuccessRoute(); - } - } - // Note: the org invite token and email verification are mutually exclusive. Only one will be present. override async buildRegisterRequest( email: string, diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index d98a2ee8cf2..7fe8ef4c79f 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -264,7 +264,6 @@ const safeProviders: SafeProvider[] = [ PolicyApiServiceAbstraction, LogService, PolicyService, - ConfigService, ], }), safeProvider({ diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8a2270113a9..1fb19757d60 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -83,6 +83,7 @@ import { SendComponent } from "./tools/send/send.component"; import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component"; import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component"; import { SetupExtensionComponent } from "./vault/components/setup-extension/setup-extension.component"; +import { setupExtensionRedirectGuard } from "./vault/guards/setup-extension-redirect.guard"; import { VaultModule } from "./vault/individual-vault/vault.module"; const routes: Routes = [ @@ -628,6 +629,7 @@ const routes: Routes = [ children: [ { path: "vault", + canActivate: [setupExtensionRedirectGuard], loadChildren: () => VaultModule, }, { diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html index df1786e227e..560bd5fd464 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.html @@ -18,7 +18,13 @@ > {{ "getTheExtension" | i18n }} - + {{ "skipToWebApp" | i18n }} diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts index d34dba737dd..a5d5ec4b939 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.spec.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { provideNoopAnimations } from "@angular/platform-browser/animations"; @@ -5,20 +6,26 @@ import { RouterModule } from "@angular/router"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DIALOG_DATA } from "@bitwarden/components"; import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component"; describe("AddExtensionLaterDialogComponent", () => { let fixture: ComponentFixture; const getDevice = jest.fn().mockReturnValue(null); + const onDismiss = jest.fn(); beforeEach(async () => { + onDismiss.mockClear(); + await TestBed.configureTestingModule({ imports: [AddExtensionLaterDialogComponent, RouterModule.forRoot([])], providers: [ provideNoopAnimations(), { provide: PlatformUtilsService, useValue: { getDevice } }, { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DialogRef, useValue: { close: jest.fn() } }, + { provide: DIALOG_DATA, useValue: { onDismiss } }, ], }).compileComponents(); @@ -39,4 +46,11 @@ describe("AddExtensionLaterDialogComponent", () => { expect(skipLink.attributes.href).toBe("/vault"); }); + + it('invokes `onDismiss` when "Skip to Web App" is clicked', () => { + const skipLink = fixture.debugElement.queryAll(By.css("a[bitButton]"))[1]; + skipLink.triggerEventHandler("click", {}); + + expect(onDismiss).toHaveBeenCalled(); + }); }); diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts index 3324cb8b1b0..5f4e3f586f5 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts @@ -4,7 +4,17 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; -import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/components"; +import { + ButtonComponent, + DIALOG_DATA, + DialogModule, + TypographyModule, +} from "@bitwarden/components"; + +export type AddExtensionLaterDialogData = { + /** Method invoked when the dialog is dismissed */ + onDismiss: () => void; +}; @Component({ selector: "vault-add-extension-later-dialog", @@ -13,6 +23,7 @@ import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/comp }) export class AddExtensionLaterDialogComponent implements OnInit { private platformUtilsService = inject(PlatformUtilsService); + private data: AddExtensionLaterDialogData = inject(DIALOG_DATA); /** Download Url for the extension based on the browser */ protected webStoreUrl: string = ""; @@ -20,4 +31,8 @@ export class AddExtensionLaterDialogComponent implements OnInit { ngOnInit(): void { this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); } + + async dismissExtensionPage() { + this.data.onDismiss(); + } } diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts index 752e2c8d4a6..e824cd92f37 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts @@ -3,12 +3,14 @@ import { By } from "@angular/platform-browser"; import { Router, RouterModule } from "@angular/router"; import { BehaviorSubject } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; @@ -21,11 +23,13 @@ describe("SetupExtensionComponent", () => { const getFeatureFlag = jest.fn().mockResolvedValue(false); const navigate = jest.fn().mockResolvedValue(true); const openExtension = jest.fn().mockResolvedValue(true); + const update = jest.fn().mockResolvedValue(true); const extensionInstalled$ = new BehaviorSubject(null); beforeEach(async () => { navigate.mockClear(); openExtension.mockClear(); + update.mockClear(); getFeatureFlag.mockClear().mockResolvedValue(true); window.matchMedia = jest.fn().mockReturnValue(false); @@ -36,6 +40,14 @@ describe("SetupExtensionComponent", () => { { provide: ConfigService, useValue: { getFeatureFlag } }, { provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } }, { provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } }, + { + provide: AccountService, + useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) }, + }, + { + provide: StateProvider, + useValue: { getUser: () => ({ update }) }, + }, ], }).compileComponents(); @@ -120,6 +132,10 @@ describe("SetupExtensionComponent", () => { expect(openExtension).toHaveBeenCalled(); }); + + it("dismisses the extension page", () => { + expect(update).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index 9ee8e189627..14770ca5d6c 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -2,13 +2,16 @@ import { DOCUMENT, NgIf } from "@angular/common"; import { Component, DestroyRef, inject, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router, RouterModule } from "@angular/router"; -import { pairwise, startWith } from "rxjs"; +import { firstValueFrom, pairwise, startWith } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; import { @@ -20,9 +23,13 @@ import { } from "@bitwarden/components"; import { VaultIcons } from "@bitwarden/vault"; +import { SETUP_EXTENSION_DISMISSED } from "../../guards/setup-extension-redirect.guard"; import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; -import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component"; +import { + AddExtensionLaterDialogComponent, + AddExtensionLaterDialogData, +} from "./add-extension-later-dialog.component"; import { AddExtensionVideosComponent } from "./add-extension-videos.component"; const SetupExtensionState = { @@ -53,6 +60,8 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { private destroyRef = inject(DestroyRef); private platformUtilsService = inject(PlatformUtilsService); private dialogService = inject(DialogService); + private stateProvider = inject(StateProvider); + private accountService = inject(AccountService); private document = inject(DOCUMENT); protected SetupExtensionState = SetupExtensionState; @@ -96,6 +105,7 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { // Extension was not installed and now it is, show success state if (previousState === false && currentState) { this.dialogRef?.close(); + void this.dismissExtensionPage(); this.state = SetupExtensionState.Success; } @@ -125,17 +135,31 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { const isMobile = Utils.isMobileBrowser; if (!isFeatureEnabled || isMobile) { + await this.dismissExtensionPage(); await this.router.navigate(["/vault"]); } } /** Opens the add extension later dialog */ addItLater() { - this.dialogRef = this.dialogService.open(AddExtensionLaterDialogComponent); + this.dialogRef = this.dialogService.open( + AddExtensionLaterDialogComponent, + { + data: { + onDismiss: this.dismissExtensionPage.bind(this), + }, + }, + ); } /** Opens the browser extension */ openExtension() { void this.webBrowserExtensionInteractionService.openExtension(); } + + /** Update local state to never show this page again. */ + private async dismissExtensionPage() { + const accountId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + void this.stateProvider.getUser(accountId, SETUP_EXTENSION_DISMISSED).update(() => true); + } } diff --git a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts new file mode 100644 index 00000000000..e6fc03fd844 --- /dev/null +++ b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.spec.ts @@ -0,0 +1,132 @@ +import { TestBed } from "@angular/core/testing"; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router"; +import { BehaviorSubject } from "rxjs"; + +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { WebBrowserInteractionService } from "../services/web-browser-interaction.service"; + +import { setupExtensionRedirectGuard } from "./setup-extension-redirect.guard"; + +describe("setupExtensionRedirectGuard", () => { + const _state = Object.freeze({}) as RouterStateSnapshot; + const emptyRoute = Object.freeze({ queryParams: {} }) as ActivatedRouteSnapshot; + const seventeenDaysAgo = new Date(); + seventeenDaysAgo.setDate(seventeenDaysAgo.getDate() - 17); + + const account = { + id: "account-id", + } as unknown as Account; + + const activeAccount$ = new BehaviorSubject(account); + const extensionInstalled$ = new BehaviorSubject(false); + const state$ = new BehaviorSubject(false); + const createUrlTree = jest.fn(); + const getFeatureFlag = jest.fn().mockImplementation((key) => { + if (key === FeatureFlag.PM19315EndUserActivationMvp) { + return Promise.resolve(true); + } + + return Promise.resolve(false); + }); + const getProfileCreationDate = jest.fn().mockResolvedValue(seventeenDaysAgo); + + beforeEach(() => { + Utils.isMobileBrowser = false; + + getFeatureFlag.mockClear(); + getProfileCreationDate.mockClear(); + createUrlTree.mockClear(); + + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: { createUrlTree } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: StateProvider, useValue: { getUser: () => ({ state$ }) } }, + { provide: WebBrowserInteractionService, useValue: { extensionInstalled$ } }, + { + provide: VaultProfileService, + useValue: { getProfileCreationDate }, + }, + ], + }); + }); + + function setupExtensionGuard(route?: ActivatedRouteSnapshot) { + // Run the guard within injection context so `inject` works as you'd expect + // Pass state object to make TypeScript happy + return TestBed.runInInjectionContext(async () => + setupExtensionRedirectGuard(route ?? emptyRoute, _state), + ); + } + + it("returns `true` when the profile was created more than 30 days ago", async () => { + const thirtyOneDaysAgo = new Date(); + thirtyOneDaysAgo.setDate(thirtyOneDaysAgo.getDate() - 31); + + getProfileCreationDate.mockResolvedValueOnce(thirtyOneDaysAgo); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the profile check fails", async () => { + getProfileCreationDate.mockRejectedValueOnce(new Error("Profile check failed")); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the user is on a mobile device", async () => { + Utils.isMobileBrowser = true; + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the user has dismissed the extension page", async () => { + state$.next(true); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it("returns `true` when the user has the extension installed", async () => { + state$.next(false); + extensionInstalled$.next(true); + + expect(await setupExtensionGuard()).toBe(true); + }); + + it('redirects the user to "/setup-extension" when all criteria do not pass', async () => { + state$.next(false); + extensionInstalled$.next(false); + + await setupExtensionGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/setup-extension"]); + }); + + describe("missing current account", () => { + afterAll(() => { + // reset `activeAccount$` observable + activeAccount$.next(account); + }); + + it("redirects to login when account is missing", async () => { + activeAccount$.next(null); + + await setupExtensionGuard(); + + expect(createUrlTree).toHaveBeenCalledWith(["/login"]); + }); + }); +}); diff --git a/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts new file mode 100644 index 00000000000..983fd8ed0aa --- /dev/null +++ b/apps/web/src/app/vault/guards/setup-extension-redirect.guard.ts @@ -0,0 +1,109 @@ +import { inject } from "@angular/core"; +import { CanActivateFn, Router } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; + +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + SETUP_EXTENSION_DISMISSED_DISK, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; + +import { WebBrowserInteractionService } from "../services/web-browser-interaction.service"; + +export const SETUP_EXTENSION_DISMISSED = new UserKeyDefinition( + SETUP_EXTENSION_DISMISSED_DISK, + "setupExtensionDismissed", + { + deserializer: (dismissed) => dismissed, + clearOn: [], + }, +); + +export const setupExtensionRedirectGuard: CanActivateFn = async () => { + const router = inject(Router); + const configService = inject(ConfigService); + const accountService = inject(AccountService); + const vaultProfileService = inject(VaultProfileService); + const stateProvider = inject(StateProvider); + const webBrowserInteractionService = inject(WebBrowserInteractionService); + + const isMobile = Utils.isMobileBrowser; + + const endUserFeatureEnabled = await configService.getFeatureFlag( + FeatureFlag.PM19315EndUserActivationMvp, + ); + + // The extension page isn't applicable for mobile users, do not redirect them. + // Include before any other checks to avoid unnecessary processing. + if (!endUserFeatureEnabled || isMobile) { + return true; + } + + const currentAcct = await firstValueFrom(accountService.activeAccount$); + + if (!currentAcct) { + return router.createUrlTree(["/login"]); + } + + const hasExtensionInstalledPromise = firstValueFrom( + webBrowserInteractionService.extensionInstalled$, + ); + + const dismissedExtensionPage = await firstValueFrom( + stateProvider + .getUser(currentAcct.id, SETUP_EXTENSION_DISMISSED) + .state$.pipe(map((dismissed) => dismissed ?? false)), + ); + + const isProfileOlderThan30Days = await profileIsOlderThan30Days( + vaultProfileService, + currentAcct.id, + ).catch( + () => + // If the call for the profile fails for any reason, do not block the user + true, + ); + + if (dismissedExtensionPage || isProfileOlderThan30Days) { + return true; + } + + // Checking for the extension is a more expensive operation, do it last to avoid unnecessary delays. + const hasExtensionInstalled = await hasExtensionInstalledPromise; + + if (hasExtensionInstalled) { + return true; + } + + return router.createUrlTree(["/setup-extension"]); +}; + +/** Returns true when the user's profile is older than 30 days */ +async function profileIsOlderThan30Days( + vaultProfileService: VaultProfileService, + userId: string, +): Promise { + const creationDate = await vaultProfileService.getProfileCreationDate(userId); + return isMoreThan30DaysAgo(creationDate); +} + +/** Returns the true when the date given is older than 30 days */ +function isMoreThan30DaysAgo(date?: string | Date): boolean { + if (!date) { + return false; + } + + const inputDate = new Date(date).getTime(); + const today = new Date().getTime(); + + const differenceInMS = today - inputDate; + const msInADay = 1000 * 60 * 60 * 24; + const differenceInDays = Math.round(differenceInMS / msInADay); + + return differenceInDays > 30; +} diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts index fef5d45e8c3..bfbfb0fb676 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts @@ -38,7 +38,7 @@ describe("WebBrowserInteractionService", () => { expect(installed).toBe(false); }); - tick(1500); + tick(150); })); it("returns true when the extension is installed", (done) => { @@ -58,13 +58,13 @@ describe("WebBrowserInteractionService", () => { }); // initial timeout, should emit false - tick(1500); + tick(26); expect(results[0]).toBe(false); tick(2500); // then emit `HasBwInstalled` dispatchEvent(VaultMessages.HasBwInstalled); - tick(); + tick(26); expect(results[1]).toBe(true); })); }); diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.ts index f1005ef6dc9..1f91942591b 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.ts @@ -21,10 +21,19 @@ import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; /** - * The amount of time in milliseconds to wait for a response from the browser extension. + * The amount of time in milliseconds to wait for a response from the browser extension. A longer duration is + * used to allow for the extension to open and then emit to the message. * NOTE: This value isn't computed by any means, it is just a reasonable timeout for the extension to respond. */ -const MESSAGE_RESPONSE_TIMEOUT_MS = 1500; +const OPEN_RESPONSE_TIMEOUT_MS = 1500; + +/** + * Timeout for checking if the extension is installed. + * + * A shorter timeout is used to avoid waiting for too long for the extension. The listener for + * checking the installation runs in the background scripts so the response should be relatively quick. + */ +const CHECK_FOR_EXTENSION_TIMEOUT_MS = 25; @Injectable({ providedIn: "root", @@ -63,7 +72,7 @@ export class WebBrowserInteractionService { filter((event) => event.data.command === VaultMessages.PopupOpened), map(() => true), ), - timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), + timer(OPEN_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), ) .pipe(take(1)) .subscribe((didOpen) => { @@ -85,7 +94,7 @@ export class WebBrowserInteractionService { filter((event) => event.data.command === VaultMessages.HasBwInstalled), map(() => true), ), - timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), + timer(CHECK_FOR_EXTENSION_TIMEOUT_MS).pipe(map(() => false)), ).pipe( tap({ subscribe: () => { diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index b51f45f1b27..2bef5670ac3 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -28,10 +28,6 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi return null; } - determineLoginSuccessRoute(): Promise { - return Promise.resolve("/vault"); - } - async finishRegistration( email: string, passwordInputResult: PasswordInputResult, diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 1d1a2d8f892..dac62f039ee 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -204,8 +204,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { await this.loginSuccessHandlerService.run(authenticationResult.userId); - const successRoute = await this.registrationFinishService.determineLoginSuccessRoute(); - await this.router.navigate([successRoute]); + await this.router.navigate(["/vault"]); } catch (e) { // If login errors, redirect to login page per product. Don't show error this.logService.error("Error logging in after registration: ", e.message); diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index 523a4c79c54..5f3c04e5155 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -16,11 +16,6 @@ export abstract class RegistrationFinishService { */ abstract getMasterPasswordPolicyOptsFromOrgInvite(): Promise; - /** - * Returns the route the user is redirected to after a successful login. - */ - abstract determineLoginSuccessRoute(): Promise; - /** * Finishes the registration process by creating a new user account. * diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 93c489a343e..a1c3ee35c5c 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -202,6 +202,13 @@ export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk"); export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk"); export const NUDGES_DISK = new StateDefinition("nudges", "disk", { web: "disk-local" }); +export const SETUP_EXTENSION_DISMISSED_DISK = new StateDefinition( + "setupExtensionDismissed", + "disk", + { + web: "disk-local", + }, +); export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition( "vaultBrowserIntroCarousel", "disk", From e8629e5e1b276973ab9820e4ddc4a7a215bc69be Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:00:07 +0100 Subject: [PATCH 026/179] Resolve the dropdown display error (#15704) --- .../src/app/billing/settings/sponsored-families.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.html b/apps/web/src/app/billing/settings/sponsored-families.component.html index 7708f63365e..7d240cb0665 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.html +++ b/apps/web/src/app/billing/settings/sponsored-families.component.html @@ -28,7 +28,7 @@ > Date: Wed, 23 Jul 2025 09:51:02 -0400 Subject: [PATCH 027/179] Removing the notifications feature flag and logic (#15551) --- .../critical-applications.component.html | 1 - .../access-intelligence/critical-applications.component.ts | 6 ------ libs/common/src/enums/feature-flag.enum.ts | 6 ------ 3 files changed, 13 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html index 4e2b4e5c404..ffef3f3b0b9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html @@ -29,7 +29,6 @@

{{ "criticalApplications" | i18n }}

+ diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index ef12e7eead6..c6c5f2757dd 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, ElementRef, ViewChild, input, model } from "@angular/core"; +import { NgIf, NgClass } from "@angular/common"; +import { Component, ElementRef, ViewChild, input, model, signal, computed } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR, @@ -16,6 +17,9 @@ import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; +/** + * Do not nest Search components inside another `
`, as they already contain their own standalone `` element for searching. + */ @Component({ selector: "bit-search", templateUrl: "./search.component.html", @@ -30,7 +34,7 @@ let nextId = 0; useExisting: SearchComponent, }, ], - imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe], + imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass], }) export class SearchComponent implements ControlValueAccessor, FocusableElement { private notifyOnChange: (v: string) => void; @@ -43,6 +47,11 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { // Use `type="text"` for Safari to improve rendering performance protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const); + protected isInputFocused = signal(false); + protected isFormHovered = signal(false); + + protected showResetButton = computed(() => this.isInputFocused() || this.isFormHovered()); + readonly disabled = model(); readonly placeholder = input(); readonly autocomplete = input(); @@ -52,11 +61,20 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { } onChange(searchText: string) { + this.searchText = searchText; // update the model when the input changes (so we can use it with *ngIf in the template) if (this.notifyOnChange != undefined) { this.notifyOnChange(searchText); } } + // Handle the reset button click + clearSearch() { + this.searchText = ""; + if (this.notifyOnChange) { + this.notifyOnChange(""); + } + } + onTouch() { if (this.notifyOnTouch != undefined) { this.notifyOnTouch(); diff --git a/libs/components/src/search/search.mdx b/libs/components/src/search/search.mdx index 7775225b8c2..98e91162c94 100644 --- a/libs/components/src/search/search.mdx +++ b/libs/components/src/search/search.mdx @@ -1,4 +1,4 @@ -import { Meta, Canvas, Source, Primary, Controls, Title } from "@storybook/addon-docs"; +import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs"; import * as stories from "./search.stories"; @@ -9,6 +9,7 @@ import { SearchModule } from "@bitwarden/components"; ``` Search field + From aee23f72062cb7eee6065e7ce891294500e25766 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 23 Jul 2025 12:29:40 -0400 Subject: [PATCH 031/179] [PM-23722] remove previous change for the account security badge (#15739) --- .../tools/popup/settings/settings-v2.component.html | 11 +---------- .../src/tools/popup/settings/settings-v2.component.ts | 6 ------ libs/angular/src/vault/services/nudges.service.ts | 1 - 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 3f8bdb1cf2f..0b2e84712a4 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -10,16 +10,7 @@ -
-

{{ "accountSecurity" | i18n }}

- 1 -
+ {{ "accountSecurity" | i18n }}
diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts index 7d3b9c776fc..a0383b99390 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts @@ -50,12 +50,6 @@ export class SettingsV2Component implements OnInit { shareReplay({ bufferSize: 1, refCount: true }), ); - protected showAcctSecurityNudge$: Observable = this.authenticatedAccount$.pipe( - switchMap((account) => - this.nudgesService.showNudgeBadge$(NudgeType.AccountSecurity, account.id), - ), - ); - showDownloadBitwardenNudge$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id), diff --git a/libs/angular/src/vault/services/nudges.service.ts b/libs/angular/src/vault/services/nudges.service.ts index 584aacd9837..6cb7ae4abf1 100644 --- a/libs/angular/src/vault/services/nudges.service.ts +++ b/libs/angular/src/vault/services/nudges.service.ts @@ -160,7 +160,6 @@ export class NudgesService { hasActiveBadges$(userId: UserId): Observable { // Add more nudge types here if they have the settings badge feature const nudgeTypes = [ - NudgeType.AccountSecurity, NudgeType.EmptyVaultNudge, NudgeType.DownloadBitwarden, NudgeType.AutofillNudge, From 417c4cd13b33a667c5bc9c9bba2eac5b070fcb11 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:33:29 -0700 Subject: [PATCH 032/179] [PM-23479] - Can see card filter in AC if you belong to multiple orgs (#15661) * hide card filter if user does not have a cipher with the allowing org * fix restricted item type filter visibility * do not include deleted ciphers --- .../vault-filter/vault-filter.component.ts | 5 +- .../components/vault-filter.component.ts | 48 ++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 49bf43d60bf..bf0df14c8c6 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -10,6 +10,7 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -53,6 +54,7 @@ export class VaultFilterComponent protected configService: ConfigService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, + protected cipherService: CipherService, ) { super( vaultFilterService, @@ -65,6 +67,7 @@ export class VaultFilterComponent configService, accountService, restrictedItemTypesService, + cipherService, ); } @@ -131,7 +134,7 @@ export class VaultFilterComponent async buildAllFilters(): Promise { const builderFilter = {} as VaultFilterList; - builderFilter.typeFilter = await this.addTypeFilter(["favorites"]); + builderFilter.typeFilter = await this.addTypeFilter(["favorites"], this._organization?.id); builderFilter.collectionFilter = await this.addCollectionFilter(); builderFilter.trashFilter = await this.addTrashFilter(); return builderFilter; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 61dd3e9ca80..4525d702153 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { + combineLatest, distinctUntilChanged, firstValueFrom, map, @@ -20,6 +21,7 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; @@ -155,6 +157,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { protected configService: ConfigService, protected accountService: AccountService, protected restrictedItemTypesService: RestrictedItemTypesService, + protected cipherService: CipherService, ) {} async ngOnInit(): Promise { @@ -292,16 +295,47 @@ export class VaultFilterComponent implements OnInit, OnDestroy { return orgFilterSection; } - protected async addTypeFilter(excludeTypes: CipherStatus[] = []): Promise { + protected async addTypeFilter( + excludeTypes: CipherStatus[] = [], + organizationId?: string, + ): Promise { const allFilter: CipherTypeFilter = { id: "AllItems", name: "allItems", type: "all", icon: "" }; - const data$ = this.restrictedItemTypesService.restricted$.pipe( - map((restricted) => { - // List of types restricted by all orgs - const restrictedByAll = restricted - .filter((r) => r.allowViewOrgIds.length === 0) + const userId = await firstValueFrom(this.activeUserId$); + + const data$ = combineLatest([ + this.restrictedItemTypesService.restricted$, + this.cipherService.cipherViews$(userId), + ]).pipe( + map(([restrictedTypes, ciphers]) => { + const restrictedForUser = restrictedTypes + .filter((r) => { + // - All orgs restrict the type + if (r.allowViewOrgIds.length === 0) { + return true; + } + // - Admin console: user has no ciphers of that type in the selected org + // - Individual vault view: user has no ciphers of that type in any allowed org + return !ciphers?.some((c) => { + if (c.deletedDate || c.type !== r.cipherType) { + return false; + } + // If the cipher doesn't belong to an org it is automatically restricted + if (!c.organizationId) { + return false; + } + if (organizationId) { + return ( + c.organizationId === organizationId && + r.allowViewOrgIds.includes(c.organizationId) + ); + } + return r.allowViewOrgIds.includes(c.organizationId); + }); + }) .map((r) => r.cipherType); - const toExclude = [...excludeTypes, ...restrictedByAll]; + + const toExclude = [...excludeTypes, ...restrictedForUser]; return this.allTypeFilters.filter( (f) => typeof f.type === "string" || !toExclude.includes(f.type), ); From 2040be68e3dbc8a3113ebbd5b1700954bb89f039 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:33:45 -0700 Subject: [PATCH 033/179] [PM-23360] - Hide restricted cipher types in "File -> New Item" on desktop (#15743) * hide restricted cipher types in file menu on desktop * fix bitwarden menu * small fixes --- apps/desktop/src/app/app.component.ts | 7 ++++++ apps/desktop/src/main/menu/menu.file.ts | 25 +++++++++++++++++++++- apps/desktop/src/main/menu/menu.updater.ts | 6 ++++-- apps/desktop/src/main/menu/menubar.ts | 1 + 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b5c34cc95a3..10aa7ff9eeb 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -68,6 +68,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogRef, DialogService, ToastOptions, ToastService } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components"; import { KeyService, BiometricStateService } from "@bitwarden/key-management"; @@ -172,6 +173,7 @@ export class AppComponent implements OnInit, OnDestroy { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private readonly destroyRef: DestroyRef, private readonly documentLangSetter: DocumentLangSetter, + private restrictedItemTypesService: RestrictedItemTypesService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -523,10 +525,12 @@ export class AppComponent implements OnInit, OnDestroy { private async updateAppMenu() { let updateRequest: MenuUpdateRequest; const stateAccounts = await firstValueFrom(this.accountService.accounts$); + if (stateAccounts == null || Object.keys(stateAccounts).length < 1) { updateRequest = { accounts: null, activeUserId: null, + restrictedCipherTypes: null, }; } else { const accounts: { [userId: string]: MenuAccount } = {}; @@ -557,6 +561,9 @@ export class AppComponent implements OnInit, OnDestroy { activeUserId: await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ), + restrictedCipherTypes: ( + await firstValueFrom(this.restrictedItemTypesService.restricted$) + ).map((restrictedItems) => restrictedItems.cipherType), }; } diff --git a/apps/desktop/src/main/menu/menu.file.ts b/apps/desktop/src/main/menu/menu.file.ts index 19ba5e99792..a8cdb347a77 100644 --- a/apps/desktop/src/main/menu/menu.file.ts +++ b/apps/desktop/src/main/menu/menu.file.ts @@ -2,6 +2,7 @@ import { BrowserWindow, MenuItemConstructorOptions } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { CipherType } from "@bitwarden/sdk-internal"; import { isMac, isMacAppStore } from "../../utils"; import { UpdaterMain } from "../updater.main"; @@ -54,6 +55,7 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { accounts: { [userId: string]: MenuAccount }, isLocked: boolean, isLockable: boolean, + private restrictedCipherTypes: CipherType[], ) { super(i18nService, messagingService, updater, window, accounts, isLocked, isLockable); } @@ -77,6 +79,23 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { }; } + private mapMenuItemToCipherType(itemId: string): CipherType { + switch (itemId) { + case "typeLogin": + return CipherType.Login; + case "typeCard": + return CipherType.Card; + case "typeIdentity": + return CipherType.Identity; + case "typeSecureNote": + return CipherType.SecureNote; + case "typeSshKey": + return CipherType.SshKey; + default: + throw new Error(`Unknown menu item id: ${itemId}`); + } + } + private get addNewItemSubmenu(): MenuItemConstructorOptions[] { return [ { @@ -109,7 +128,11 @@ export class FileMenu extends FirstMenu implements IMenubarMenu { click: () => this.sendMessage("newSshKey"), accelerator: "CmdOrCtrl+Shift+K", }, - ]; + ].filter((item) => { + return !this.restrictedCipherTypes?.some( + (restrictedType) => restrictedType === this.mapMenuItemToCipherType(item.id), + ); + }); } private get addNewFolder(): MenuItemConstructorOptions { diff --git a/apps/desktop/src/main/menu/menu.updater.ts b/apps/desktop/src/main/menu/menu.updater.ts index 6f82a78384f..8b658049de7 100644 --- a/apps/desktop/src/main/menu/menu.updater.ts +++ b/apps/desktop/src/main/menu/menu.updater.ts @@ -1,8 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CipherType } from "@bitwarden/common/vault/enums"; export class MenuUpdateRequest { - activeUserId: string; - accounts: { [userId: string]: MenuAccount }; + activeUserId: string | null; + accounts: { [userId: string]: MenuAccount } | null; + restrictedCipherTypes: CipherType[] | null; } export class MenuAccount { diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index 825afdaa1e8..8ac3a084d95 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -83,6 +83,7 @@ export class Menubar { updateRequest?.accounts, isLocked, isLockable, + updateRequest?.restrictedCipherTypes, ), new EditMenu(i18nService, messagingService, isLocked), new ViewMenu(i18nService, messagingService, isLocked), From e47e1f79d9b8d48358e6b57f665a5455c4dab156 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:58:44 -0400 Subject: [PATCH 034/179] fix(ChangePasswordComp): [Auth/PM-23913] Extension popout now closes after a password change (#15681) --- .../extension-change-password.service.spec.ts | 50 +++++++++++++++++++ .../extension-change-password.service.ts | 29 +++++++++++ .../src/popup/services/services.module.ts | 8 +++ .../change-password.component.ts | 3 ++ .../change-password.service.abstraction.ts | 6 +++ 5 files changed, 96 insertions(+) create mode 100644 apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts create mode 100644 apps/browser/src/auth/popup/change-password/extension-change-password.service.ts diff --git a/apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts b/apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts new file mode 100644 index 00000000000..a6a6b905218 --- /dev/null +++ b/apps/browser/src/auth/popup/change-password/extension-change-password.service.spec.ts @@ -0,0 +1,50 @@ +import { MockProxy, mock } from "jest-mock-extended"; + +import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeyService } from "@bitwarden/key-management"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; + +import { ExtensionChangePasswordService } from "./extension-change-password.service"; + +describe("ExtensionChangePasswordService", () => { + let keyService: MockProxy; + let masterPasswordApiService: MockProxy; + let masterPasswordService: MockProxy; + let window: MockProxy; + + let changePasswordService: ChangePasswordService; + + beforeEach(() => { + keyService = mock(); + masterPasswordApiService = mock(); + masterPasswordService = mock(); + window = mock(); + + changePasswordService = new ExtensionChangePasswordService( + keyService, + masterPasswordApiService, + masterPasswordService, + window, + ); + }); + + it("should instantiate the service", () => { + expect(changePasswordService).toBeDefined(); + }); + + it("should close the browser extension popout", () => { + const closePopupSpy = jest.spyOn(BrowserApi, "closePopup"); + const browserPopupUtilsInPopupSpy = jest + .spyOn(BrowserPopupUtils, "inPopout") + .mockReturnValue(true); + + changePasswordService.closeBrowserExtensionPopout?.(); + + expect(closePopupSpy).toHaveBeenCalledWith(window); + expect(browserPopupUtilsInPopupSpy).toHaveBeenCalledWith(window); + }); +}); diff --git a/apps/browser/src/auth/popup/change-password/extension-change-password.service.ts b/apps/browser/src/auth/popup/change-password/extension-change-password.service.ts new file mode 100644 index 00000000000..dd2ce48d27a --- /dev/null +++ b/apps/browser/src/auth/popup/change-password/extension-change-password.service.ts @@ -0,0 +1,29 @@ +import { + DefaultChangePasswordService, + ChangePasswordService, +} from "@bitwarden/angular/auth/password-management/change-password"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeyService } from "@bitwarden/key-management"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; + +export class ExtensionChangePasswordService + extends DefaultChangePasswordService + implements ChangePasswordService +{ + constructor( + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + private win: Window, + ) { + super(keyService, masterPasswordApiService, masterPasswordService); + } + closeBrowserExtensionPopout(): void { + if (BrowserPopupUtils.inPopout(this.win)) { + BrowserApi.closePopup(this.win); + } + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 3887c8c8b12..509f7554aef 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -5,6 +5,7 @@ import { merge, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction"; +import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; @@ -45,6 +46,7 @@ import { AccountService as AccountServiceAbstraction, } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { @@ -143,6 +145,7 @@ import { import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; +import { ExtensionChangePasswordService } from "../../auth/popup/change-password/extension-change-password.service"; import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service"; import { ExtensionLogoutService } from "../../auth/popup/logout/extension-logout.service"; @@ -664,6 +667,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSshImportPromptService, deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], }), + safeProvider({ + provide: ChangePasswordService, + useClass: ExtensionChangePasswordService, + deps: [KeyService, MasterPasswordApiService, InternalMasterPasswordServiceAbstraction, WINDOW], + }), safeProvider({ provide: NotificationsService, useClass: ForegroundNotificationsService, diff --git a/libs/angular/src/auth/password-management/change-password/change-password.component.ts b/libs/angular/src/auth/password-management/change-password/change-password.component.ts index 02738d33321..7bb9584e934 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.component.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.component.ts @@ -178,6 +178,9 @@ export class ChangePasswordComponent implements OnInit { // TODO: PM-23515 eventually use the logout service instead of messaging service once it is available without circular dependencies this.messagingService.send("logout"); + + // Close the popout if we are in a browser extension popout. + this.changePasswordService.closeBrowserExtensionPopout?.(); } } catch (error) { this.logService.error(error); diff --git a/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts b/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts index 2fd3bbae67a..1d6d789cdc5 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.service.abstraction.ts @@ -59,4 +59,10 @@ export abstract class ChangePasswordService { * - Currently only used on the web change password service. */ clearDeeplinkState?: () => Promise; + + /** + * Optional method that closes the browser extension popout if in a popout + * If not in a popout, does nothing. + */ + abstract closeBrowserExtensionPopout?(): void; } From d45cacc1aff9ce4ef66f9d163ba9091257e0bcd0 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Wed, 23 Jul 2025 13:26:35 -0400 Subject: [PATCH 035/179] [CL-801] Fix chromatic.yml externals formatting (#15736) --- .github/workflows/chromatic.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 4ee39305f84..d0b9cab4c45 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -102,6 +102,9 @@ jobs: storybookBuildDir: ./storybook-static exitOnceUploaded: true onlyChanged: true - externals: "[\"libs/components/**/*.scss\", \"libs/components/**/*.css\", \"libs/components/tailwind.config*.js\"]" + externals: | + libs/components/**/*.scss + libs/components/**/*.css + libs/components/tailwind.config*.js # Rather than use an `if` check on the whole publish step, we need to tell Chromatic to skip so that any Chromatic-spawned actions are properly skipped skip: ${{ steps.get-changed-files-for-chromatic.outputs.storyFiles == 'false' }} From fe1c04099355162b20a891ae73bee8f91cf4ab6a Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:37:40 -0700 Subject: [PATCH 036/179] [PM-23789][PM-237090][PM-23791][PM-23792] - [Web][Desktop][Browser] - Do not import cards if policy is enabled (#15740) * restrict item types in import * add comment * fix spec * fix dep * clean up logic --- apps/browser/src/background/main.background.ts | 1 + apps/cli/src/service-container/service-container.ts | 1 + libs/importer/src/components/import.component.ts | 1 + libs/importer/src/services/import.service.spec.ts | 4 ++++ libs/importer/src/services/import.service.ts | 13 +++++++++++++ 5 files changed, 20 insertions(+) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 1dfc947b284..2565f366870 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1070,6 +1070,7 @@ export default class MainBackground { this.pinService, this.accountService, this.sdkService, + this.restrictedItemTypesService, ); this.individualVaultExportService = new IndividualVaultExportService( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index aa507aec1d8..78f961973d9 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -821,6 +821,7 @@ export class ServiceContainer { this.pinService, this.accountService, this.sdkService, + this.restrictedItemTypesService, ); this.individualExportService = new IndividualVaultExportService( diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 4f2715fe9cf..7bac6b0e0a5 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -100,6 +100,7 @@ const safeProviders: SafeProvider[] = [ PinServiceAbstraction, AccountService, SdkService, + RestrictedItemTypesService, ], }), ]; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index f71c34bf209..a27b74c7ad5 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -13,6 +13,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; @@ -34,6 +35,7 @@ describe("ImportService", () => { let pinService: MockProxy; let accountService: MockProxy; let sdkService: MockSdkService; + let restrictedItemTypesService: MockProxy; beforeEach(() => { cipherService = mock(); @@ -45,6 +47,7 @@ describe("ImportService", () => { encryptService = mock(); pinService = mock(); sdkService = new MockSdkService(); + restrictedItemTypesService = mock(); importService = new ImportService( cipherService, @@ -57,6 +60,7 @@ describe("ImportService", () => { pinService, accountService, sdkService, + restrictedItemTypesService, ); }); diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index c9cb325d10b..c6bff607633 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -26,6 +26,7 @@ import { CipherRequest } from "@bitwarden/common/vault/models/request/cipher.req import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; import { @@ -119,6 +120,7 @@ export class ImportService implements ImportServiceAbstraction { private pinService: PinServiceAbstraction, private accountService: AccountService, private sdkService: SdkService, + private restrictedItemTypesService: RestrictedItemTypesService, ) {} getImportOptions(): ImportOption[] { @@ -166,6 +168,17 @@ export class ImportService implements ImportServiceAbstraction { } } + const restrictedItemTypes = await firstValueFrom( + this.restrictedItemTypesService.restricted$.pipe( + map((restrictedItemTypes) => restrictedItemTypes.map((r) => r.cipherType)), + ), + ); + + // Filter out restricted item types from the import result + importResult.ciphers = importResult.ciphers.filter( + (cipher) => !restrictedItemTypes.includes(cipher.type), + ); + if (organizationId && !selectedImportTarget && !canAccessImportExport) { const hasUnassignedCollections = importResult.collectionRelationships.length < importResult.ciphers.length; From 4458a7306be559d2d0cd5e673a4b84135cca4f36 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Wed, 23 Jul 2025 16:00:37 -0400 Subject: [PATCH 037/179] update xs text size (#15680) --- libs/components/tailwind.config.base.js | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 829c812a954..8b73ffc470c 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -155,6 +155,7 @@ module.exports = { "90vw": "90vw", }), fontSize: { + xs: [".8125rem", "1rem"], "3xl": ["1.75rem", "2rem"], }, }, From 7a24a538a4419fc822135d917edb73da09d97287 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 23 Jul 2025 22:29:44 +0200 Subject: [PATCH 038/179] [PM-23072] Remove legacy key support in auth code (#15350) * Remove legacy key support in auth code * Fix tests --- .../src/auth/guards/lock.guard.spec.ts | 30 ------------------- libs/angular/src/auth/guards/lock.guard.ts | 7 ----- .../common/login-strategies/login.strategy.ts | 2 +- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/libs/angular/src/auth/guards/lock.guard.spec.ts b/libs/angular/src/auth/guards/lock.guard.spec.ts index 2085e0f3486..53491bace00 100644 --- a/libs/angular/src/auth/guards/lock.guard.spec.ts +++ b/libs/angular/src/auth/guards/lock.guard.spec.ts @@ -26,7 +26,6 @@ import { lockGuard } from "./lock.guard"; interface SetupParams { authStatus: AuthenticationStatus; canLock?: boolean; - isLegacyUser?: boolean; clientType?: ClientType; everHadUserKey?: boolean; supportsDeviceTrust?: boolean; @@ -43,7 +42,6 @@ describe("lockGuard", () => { vaultTimeoutSettingsService.canLock.mockResolvedValue(setupParams.canLock); const keyService: MockProxy = mock(); - keyService.isLegacyUser.mockResolvedValue(setupParams.isLegacyUser); keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey)); const platformUtilService: MockProxy = mock(); @@ -155,37 +153,10 @@ describe("lockGuard", () => { expect(router.url).toBe("/"); }); - it("should log user out if they are a legacy user on a desktop client", async () => { - const { router, messagingService } = setup({ - authStatus: AuthenticationStatus.Locked, - canLock: true, - isLegacyUser: true, - clientType: ClientType.Desktop, - }); - - await router.navigate(["lock"]); - expect(router.url).toBe("/"); - expect(messagingService.send).toHaveBeenCalledWith("logout"); - }); - - it("should log user out if they are a legacy user on a browser extension client", async () => { - const { router, messagingService } = setup({ - authStatus: AuthenticationStatus.Locked, - canLock: true, - isLegacyUser: true, - clientType: ClientType.Browser, - }); - - await router.navigate(["lock"]); - expect(router.url).toBe("/"); - expect(messagingService.send).toHaveBeenCalledWith("logout"); - }); - it("should allow navigation to the lock route when device trust is supported, the user has a MP, and the user is coming from the login-initiated page", async () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, - isLegacyUser: false, clientType: ClientType.Web, everHadUserKey: false, supportsDeviceTrust: true, @@ -213,7 +184,6 @@ describe("lockGuard", () => { const { router } = setup({ authStatus: AuthenticationStatus.Locked, canLock: true, - isLegacyUser: false, clientType: ClientType.Web, everHadUserKey: false, supportsDeviceTrust: true, diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 4b09ddeee18..8acdadeb87c 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -13,7 +13,6 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { KeyService } from "@bitwarden/key-management"; /** @@ -31,7 +30,6 @@ export function lockGuard(): CanActivateFn { const authService = inject(AuthService); const keyService = inject(KeyService); const deviceTrustService = inject(DeviceTrustServiceAbstraction); - const messagingService = inject(MessagingService); const router = inject(Router); const userVerificationService = inject(UserVerificationService); const vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); @@ -56,11 +54,6 @@ export function lockGuard(): CanActivateFn { return false; } - if (await keyService.isLegacyUser()) { - messagingService.send("logout"); - return false; - } - // User is authN and in locked state. const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 463ea676163..b8d5f64bfcc 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -325,7 +325,7 @@ export abstract class LoginStrategy { protected async createKeyPairForOldAccount(userId: UserId) { try { - const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); + const userKey = await this.keyService.getUserKey(userId); const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); if (!privateKey.encryptedString) { throw new Error("Failed to create encrypted private key"); From d0d1359ff4fd4d40edaa5f1096373d642bafc40d Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 23 Jul 2025 19:05:15 -0400 Subject: [PATCH 039/179] [PM-12048] Wire up vNextCollectionService (#14871) * remove derived state, add cache in service. Fix ts strict errors * cleanup * promote vNextCollectionService * wip * replace callers in web WIP * refactor tests for web * update callers to use vNextCollectionServcie methods in CLI * WIP make decryptMany public again, fix callers, imports * wip cli * wip desktop * update callers in browser, fix tests * remove in service cache * cleanup * fix test * clean up * address cr feedback * remove duplicate userId * clean up * remove unused import * fix vault-settings-import-nudge.service * fix caching issue * clean up * refactor decryption, cleanup, update callers * clean up * Use in-memory statedefinition * Ac/pm 12048 v next collection service pairing (#15239) * Draft from pairing with Gibson * Add todos * Add comment * wip * refactor upsert --------- Co-authored-by: Brandon * clean up * fix state definitions * fix linter error * cleanup * add test, fix shareReplay * fix item-more-options component * fix desktop build * refactor state to account for null as an initial value, remove caching * add proper cache, add unit test, update callers * clean up * fix routing when deleting collections * cleanup * use combineLatest * fix ts-strict errors, fix error handling * refactor Collection and CollectionView properties for ts-strict * Revert "refactor Collection and CollectionView properties for ts-strict" This reverts commit a5c63aab76ba5663f4136000fe31fbb9171b8d4b. --------- Co-authored-by: Thomas Rittson Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../background/notification.background.ts | 29 +- .../browser/src/background/main.background.ts | 1 - .../assign-collections.component.ts | 8 +- .../item-more-options.component.ts | 2 +- .../vault-popup-items.service.spec.ts | 2 +- .../services/vault-popup-items.service.ts | 9 +- .../vault-popup-list-filters.service.spec.ts | 28 +- .../vault-popup-list-filters.service.ts | 18 +- apps/cli/src/commands/get.command.ts | 10 +- apps/cli/src/commands/list.command.ts | 17 +- apps/cli/src/oss-serve-configurator.ts | 1 + .../service-container/service-container.ts | 1 - apps/cli/src/vault.program.ts | 1 + apps/desktop/src/app/app.component.ts | 1 - .../src/vault/app/vault/vault-v2.component.ts | 20 +- .../collections/vault.component.ts | 3 + .../organizations/manage/groups.component.ts | 26 +- .../members/members.component.ts | 27 +- .../collection-dialog.component.ts | 26 +- ...ree-org-collection-limit.validator.spec.ts | 10 +- .../free-org-collection-limit.validator.ts | 20 +- apps/web/src/app/app.component.ts | 1 - .../bulk-delete-dialog.component.ts | 9 +- .../services/vault-filter.service.spec.ts | 2 +- .../services/vault-filter.service.ts | 15 +- .../vault/individual-vault/vault.component.ts | 33 +- .../abstractions/collection-admin.service.ts | 19 +- .../abstractions/collection.service.ts | 40 +- .../abstractions/vnext-collection.service.ts | 43 -- .../collections/models/collection.data.ts | 5 +- .../common/collections/models/collection.ts | 26 +- .../collections/models/collection.view.ts | 2 +- .../collections/services/collection.state.ts | 28 ++ .../default-collection-admin.service.ts | 8 +- .../default-collection.service.spec.ts | 433 ++++++++++++++---- .../services/default-collection.service.ts | 338 +++++++------- .../default-vnext-collection.service.spec.ts | 345 -------------- .../default-vnext-collection.service.ts | 194 -------- .../services/vnext-collection.state.ts | 36 -- .../empty-vault-nudge.service.ts | 2 +- .../vault-settings-import-nudge.service.ts | 2 +- .../services/vault-filter.service.ts | 7 +- .../services/vault-timeout.service.ts | 4 - .../src/platform/misc/rxjs-operators.ts | 6 +- .../src/platform/models/domain/domain-base.ts | 2 +- .../src/platform/state/state-definitions.ts | 6 +- .../src/platform/sync/core-sync.service.ts | 6 +- .../cipher-authorization.service.spec.ts | 9 +- .../services/cipher-authorization.service.ts | 18 +- .../src/components/import.component.ts | 12 +- libs/importer/src/services/import.service.ts | 2 +- .../src/services/org-vault-export.service.ts | 35 +- .../src/components/export.component.ts | 40 +- .../default-cipher-form-config.service.ts | 5 +- .../src/cipher-view/cipher-view.component.ts | 13 +- .../assign-collections.component.ts | 12 +- 56 files changed, 906 insertions(+), 1112 deletions(-) delete mode 100644 libs/admin-console/src/common/collections/abstractions/vnext-collection.service.ts create mode 100644 libs/admin-console/src/common/collections/services/collection.state.ts delete mode 100644 libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts delete mode 100644 libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts delete mode 100644 libs/admin-console/src/common/collections/services/vnext-collection.state.ts diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 65c1ca0277f..15baccf3560 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1030,18 +1030,23 @@ export default class NotificationBackground { private async getCollectionData( message: NotificationBackgroundExtensionMessage, ): Promise { - const collections = (await this.collectionService.getAllDecrypted()).reduce( - (acc, collection) => { - if (collection.organizationId === message?.orgId) { - acc.push({ - id: collection.id, - name: collection.name, - organizationId: collection.organizationId, - }); - } - return acc; - }, - [], + const collections = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionService.decryptedCollections$(userId)), + map((collections) => + collections.reduce((acc, collection) => { + if (collection.organizationId === message?.orgId) { + acc.push({ + id: collection.id, + name: collection.name, + organizationId: collection.organizationId, + }); + } + return acc; + }, []), + ), + ), ); return collections; } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 2565f366870..3aaf24c07a4 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1620,7 +1620,6 @@ export default class MainBackground { this.keyService.clearKeys(userBeingLoggedOut), this.cipherService.clear(userBeingLoggedOut), this.folderService.clear(userBeingLoggedOut), - this.collectionService.clear(userBeingLoggedOut), this.vaultTimeoutSettingsService.clear(userBeingLoggedOut), this.vaultFilterService.clear(), this.biometricStateService.logout(userBeingLoggedOut), diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 8374cc254a9..0b7346c8613 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -10,6 +10,7 @@ import { Observable, combineLatest, filter, first, map, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -70,7 +71,12 @@ export class AssignCollections { ), ); - combineLatest([cipher$, this.collectionService.decryptedCollections$]) + const decryptedCollection$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionService.decryptedCollections$(userId)), + ); + + combineLatest([cipher$, decryptedCollection$]) .pipe(takeUntilDestroyed(), first()) .subscribe(([cipherView, collections]) => { let availableCollections = collections; diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index ce16ec2f3e0..6c3a7243309 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -93,7 +93,7 @@ export class ItemMoreOptionsComponent { switchMap((userId) => { return combineLatest([ this.organizationService.hasOrganizations(userId), - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(userId), ]).pipe( map(([hasOrgs, collections]) => { const canEditCollections = collections.some((c) => !c.readOnly); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 48788ea5ae9..32974da162d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -139,7 +139,7 @@ describe("VaultPopupItemsService", () => { ]; organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg])); - collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); + collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections)); activeUserLastSync$ = new BehaviorSubject(new Date()); syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index b2d4fd1b262..9d44eef2e47 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -72,6 +72,11 @@ export class VaultPopupItemsService { private organizations$ = this.activeUserId$.pipe( switchMap((userId) => this.organizationService.organizations$(userId)), ); + + private decryptedCollections$ = this.activeUserId$.pipe( + switchMap((userId) => this.collectionService.decryptedCollections$(userId)), + ); + /** * Observable that contains the list of other cipher types that should be shown * in the autofill section of the Vault tab. Depends on vault settings. @@ -130,7 +135,7 @@ export class VaultPopupItemsService { private _activeCipherList$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( + combineLatest([this.organizations$, this.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); @@ -291,7 +296,7 @@ export class VaultPopupItemsService { */ deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => - combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe( + combineLatest([this.organizations$, this.decryptedCollections$]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 9f1bd6e6e55..ebaeaeb6076 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -20,6 +20,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { @@ -58,7 +59,7 @@ describe("VaultPopupListFiltersService", () => { }; const collectionService = { - decryptedCollections$, + decryptedCollections$: () => decryptedCollections$, getAllNested: () => Promise.resolve([]), } as unknown as CollectionService; @@ -106,7 +107,7 @@ describe("VaultPopupListFiltersService", () => { signal: jest.fn(() => mockCachedSignal), }; - collectionService.getAllNested = () => Promise.resolve([]); + collectionService.getAllNested = () => []; TestBed.configureTestingModule({ providers: [ { @@ -382,14 +383,7 @@ describe("VaultPopupListFiltersService", () => { beforeEach(() => { decryptedCollections$.next(testCollections); - collectionService.getAllNested = () => - Promise.resolve( - testCollections.map((c) => ({ - children: [], - node: c, - parent: null, - })), - ); + collectionService.getAllNested = () => testCollections.map((c) => new TreeNode(c, null)); }); it("returns all collections", (done) => { @@ -755,15 +749,13 @@ function createSeededVaultPopupListFiltersService( } as any; const collectionServiceMock = { - decryptedCollections$: seededCollections$, + decryptedCollections$: () => seededCollections$, getAllNested: () => - Promise.resolve( - seededCollections$.value.map((c) => ({ - children: [], - node: c, - parent: null, - })), - ), + seededCollections$.value.map((c) => ({ + children: [], + node: c, + parent: null, + })), } as any; const folderServiceMock = { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 7af6fb5f212..adc0589e7e8 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -6,7 +6,6 @@ import { debounceTime, distinctUntilChanged, filter, - from, map, Observable, shareReplay, @@ -446,7 +445,7 @@ export class VaultPopupListFiltersService { this.filters$.pipe( distinctUntilChanged((prev, curr) => prev.organization?.id === curr.organization?.id), ), - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(userId), this.organizationService.memberOrganizations$(userId), this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation), ]), @@ -463,16 +462,11 @@ export class VaultPopupListFiltersService { } return sortDefaultCollections(filtered, orgs, this.i18nService.collator); }), - switchMap((collections) => { - return from(this.collectionService.getAllNested(collections)).pipe( - map( - (nested) => - new DynamicTreeNode({ - fullList: collections, - nestedList: nested, - }), - ), - ); + map((fullList) => { + return new DynamicTreeNode({ + fullList, + nestedList: this.collectionService.getAllNested(fullList), + }); }), map((tree) => tree.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")), diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index b20052fbb53..756316cba43 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -24,6 +24,7 @@ import { LoginUriExport } from "@bitwarden/common/models/export/login-uri.export import { LoginExport } from "@bitwarden/common/models/export/login.export"; import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -442,8 +443,11 @@ export class GetCommand extends DownloadCommand { private async getCollection(id: string) { let decCollection: CollectionView = null; + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (Utils.isGuid(id)) { - const collection = await this.collectionService.get(id); + const collection = await firstValueFrom( + this.collectionService.encryptedCollections$(activeUserId).pipe(getById(id)), + ); if (collection != null) { const orgKeys = await firstValueFrom(this.keyService.activeUserOrgKeys$); decCollection = await collection.decrypt( @@ -451,7 +455,9 @@ export class GetCommand extends DownloadCommand { ); } } else if (id.trim() !== "") { - let collections = await this.collectionService.getAllDecrypted(); + let collections = await firstValueFrom( + this.collectionService.decryptedCollections$(activeUserId), + ); collections = CliUtils.searchCollections(collections, id); if (collections.length > 1) { return Response.multipleResults(collections.map((c) => c.id)); diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 517050728c0..94abd97d6eb 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -20,6 +20,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { KeyService } from "@bitwarden/key-management"; import { CollectionResponse } from "../admin-console/models/response/collection.response"; import { OrganizationUserResponse } from "../admin-console/models/response/organization-user.response"; @@ -42,6 +43,7 @@ export class ListCommand { private apiService: ApiService, private eventCollectionService: EventCollectionService, private accountService: AccountService, + private keyService: KeyService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService, ) {} @@ -158,7 +160,10 @@ export class ListCommand { } private async listCollections(options: Options) { - let collections = await this.collectionService.getAllDecrypted(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + let collections = await firstValueFrom( + this.collectionService.decryptedCollections$(activeUserId), + ); if (options.organizationId != null) { collections = collections.filter((c) => { @@ -178,13 +183,13 @@ export class ListCommand { } private async listOrganizationCollections(options: Options) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (options.organizationId == null || options.organizationId === "") { return Response.badRequest("`organizationid` option is required."); } if (!Utils.isGuid(options.organizationId)) { return Response.badRequest("`" + options.organizationId + "` is not a GUID."); } - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (!userId) { return Response.badRequest("No user found."); } @@ -207,7 +212,13 @@ export class ListCommand { const collections = response.data .filter((c) => c.organizationId === options.organizationId) .map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse))); - let decCollections = await this.collectionService.decryptMany(collections); + const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId)); + if (orgKeys == null) { + throw new Error("Organization keys not found."); + } + let decCollections = await firstValueFrom( + this.collectionService.decryptMany$(collections, orgKeys), + ); if (options.search != null && options.search.trim() !== "") { decCollections = CliUtils.searchCollections(decCollections, options.search); } diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 14e6ace3b34..848627b703f 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -79,6 +79,7 @@ export class OssServeConfigurator { this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, this.serviceContainer.accountService, + this.serviceContainer.keyService, this.serviceContainer.cliRestrictedItemTypesService, ); this.createCommand = new CreateCommand( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 78f961973d9..ddab6fb7cf1 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -901,7 +901,6 @@ export class ServiceContainer { this.keyService.clearKeys(userId), this.cipherService.clear(userId), this.folderService.clear(userId), - this.collectionService.clear(userId), ]); await this.stateEventRunnerService.handleEvent("logout", userId as UserId); diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index d5615d0bb1c..2b08bc67ec1 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -114,6 +114,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, this.serviceContainer.accountService, + this.serviceContainer.keyService, this.serviceContainer.cliRestrictedItemTypesService, ); const response = await command.run(object, cmd); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 10aa7ff9eeb..72bad5befe9 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -677,7 +677,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.keyService.clearKeys(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut); await this.folderService.clear(userBeingLoggedOut); - await this.collectionService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); await this.biometricStateService.logout(userBeingLoggedOut); diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 62ca41a3379..3c4a85f96b7 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -30,8 +30,9 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { getByIds } from "@bitwarden/common/platform/misc"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; @@ -360,7 +361,12 @@ export class VaultV2Component this.allOrganizations = orgs; }); - this.collectionService.decryptedCollections$ + if (!this.activeUserId) { + throw new Error("No user found."); + } + + this.collectionService + .decryptedCollections$(this.activeUserId) .pipe(takeUntil(this.componentIsDestroyed$)) .subscribe((collections) => { this.allCollections = collections; @@ -701,9 +707,17 @@ export class VaultV2Component this.cipherId = null; this.action = "view"; await this.vaultItemsComponent?.refresh().catch(() => {}); + + if (!this.activeUserId) { + throw new Error("No userId provided."); + } + this.collections = await firstValueFrom( - this.collectionService.decryptedCollectionViews$(cipher.collectionIds as CollectionId[]), + this.collectionService + .decryptedCollections$(this.activeUserId) + .pipe(getByIds(cipher.collectionIds)), ); + this.cipherId = cipher.id; this.cipher = cipher; if (this.activeUserId) { diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 47ad93d81e2..5dc217b7a50 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -29,6 +29,7 @@ import { import { CollectionAdminService, CollectionAdminView, + CollectionService, CollectionView, Unassigned, } from "@bitwarden/admin-console/common"; @@ -264,6 +265,7 @@ export class VaultComponent implements OnInit, OnDestroy { private accountService: AccountService, private billingNotificationService: BillingNotificationService, private organizationWarningsService: OrganizationWarningsService, + private collectionService: CollectionService, ) {} async ngOnInit() { @@ -1133,6 +1135,7 @@ export class VaultComponent implements OnInit, OnDestroy { } try { await this.apiService.deleteCollection(this.organization?.id, collection.id); + await this.collectionService.delete([collection.id as CollectionId], this.userId); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 6459cd1f857..1c40ec462d3 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -11,6 +11,7 @@ import { from, lastValueFrom, map, + Observable, switchMap, tap, } from "rxjs"; @@ -25,10 +26,13 @@ import { CollectionView, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; import { GroupDetailsView, InternalGroupApiService as GroupService } from "../core"; @@ -100,6 +104,8 @@ export class GroupsComponent { private logService: LogService, private collectionService: CollectionService, private toastService: ToastService, + private keyService: KeyService, + private accountService: AccountService, ) { this.route.params .pipe( @@ -244,16 +250,22 @@ export class GroupsComponent { this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow); } - private async toCollectionMap(response: ListResponse) { + private toCollectionMap( + response: ListResponse, + ): Observable> { const collections = response.data.map( (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), ); - const decryptedCollections = await this.collectionService.decryptMany(collections); - // Convert to an object using collection Ids as keys for faster name lookups - const collectionMap: Record = {}; - decryptedCollections.forEach((c) => (collectionMap[c.id] = c)); - - return collectionMap; + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + switchMap((orgKeys) => this.collectionService.decryptMany$(collections, orgKeys)), + map((collections) => { + const collectionMap: Record = {}; + collections.forEach((c) => (collectionMap[c.id] = c)); + return collectionMap; + }), + ); } } diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 0eec835e15f..77a0cecce8e 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -13,6 +13,7 @@ import { Observable, shareReplay, switchMap, + withLatestFrom, tap, } from "rxjs"; @@ -307,17 +308,27 @@ export class MembersComponent extends BaseMembersComponent * Retrieve a map of all collection IDs <-> names for the organization. */ async getCollectionNameMap() { - const collectionMap = new Map(); - const response = await this.apiService.getCollections(this.organization.id); - - const collections = response.data.map( - (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), + const response = from(this.apiService.getCollections(this.organization.id)).pipe( + map((res) => + res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))), + ), ); - const decryptedCollections = await this.collectionService.decryptMany(collections); - decryptedCollections.forEach((c) => collectionMap.set(c.id, c.name)); + const decryptedCollections$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + withLatestFrom(response), + switchMap(([orgKeys, collections]) => + this.collectionService.decryptMany$(collections, orgKeys), + ), + map((collections) => { + const collectionMap = new Map(); + collections.forEach((c) => collectionMap.set(c.id, c.name)); + return collectionMap; + }), + ); - return collectionMap; + return await firstValueFrom(decryptedCollections$); } removeUser(id: string): Promise { diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index 8763b75ffca..fabfb65fc6b 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -26,7 +26,6 @@ import { CollectionResponse, CollectionView, CollectionService, - Collection, } from "@bitwarden/admin-console/common"; import { getOrganizationById, @@ -38,6 +37,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DIALOG_DATA, @@ -141,7 +141,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { protected PermissionMode = PermissionMode; protected showDeleteButton = false; protected showAddAccessWarning = false; - protected collections: Collection[]; protected buttonDisplayName: ButtonType = ButtonType.Save; private orgExceedingCollectionLimit!: Organization; @@ -166,14 +165,12 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { async ngOnInit() { // Opened from the individual vault + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); if (this.params.showOrgSelector) { this.showOrgSelector = true; this.formGroup.controls.selectedOrg.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((id) => this.loadOrg(id)); - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); this.organizations$ = this.organizationService.organizations$(userId).pipe( first(), map((orgs) => @@ -195,9 +192,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { ); if (isBreadcrumbEventLogsEnabled) { - this.collections = await this.collectionService.getAll(); this.organizationSelected.setAsyncValidators( - freeOrgCollectionLimitValidator(this.organizations$, this.collections, this.i18nService), + freeOrgCollectionLimitValidator( + this.organizations$, + this.collectionService + .encryptedCollections$(userId) + .pipe(map((collections) => collections ?? [])), + this.i18nService, + ), ); this.formGroup.updateValueAndValidity(); } @@ -212,7 +214,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } }), filter(() => this.organizationSelected.errors?.cannotCreateCollections), - switchMap((value) => this.findOrganizationById(value)), + switchMap((organizationId) => this.organizations$.pipe(getById(organizationId))), takeUntil(this.destroy$), ) .subscribe((org) => { @@ -222,11 +224,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { }); } - async findOrganizationById(orgId: string): Promise { - const organizations = await firstValueFrom(this.organizations$); - return organizations.find((org) => org.id === orgId); - } - async loadOrg(orgId: string) { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const organization$ = this.organizationService @@ -413,7 +410,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { collectionView.name = this.formGroup.controls.name.value; } - const savedCollection = await this.collectionAdminService.save(collectionView); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const savedCollection = await this.collectionAdminService.save(collectionView, userId); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.spec.ts b/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.spec.ts index 120a58d6b1a..f9d389c979f 100644 --- a/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.spec.ts @@ -18,7 +18,7 @@ describe("freeOrgCollectionLimitValidator", () => { it("returns null if organization is not found", async () => { const orgs: Organization[] = []; - const validator = freeOrgCollectionLimitValidator(of(orgs), [], i18nService); + const validator = freeOrgCollectionLimitValidator(of(orgs), of([]), i18nService); const control = new FormControl("org-id"); const result: Observable = validator(control) as Observable; @@ -28,7 +28,7 @@ describe("freeOrgCollectionLimitValidator", () => { }); it("returns null if control is not an instance of FormControl", async () => { - const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService); + const validator = freeOrgCollectionLimitValidator(of([]), of([]), i18nService); const control = {} as AbstractControl; const result: Observable = validator( @@ -40,7 +40,7 @@ describe("freeOrgCollectionLimitValidator", () => { }); it("returns null if control is not provided", async () => { - const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService); + const validator = freeOrgCollectionLimitValidator(of([]), of([]), i18nService); const result: Observable = validator( undefined as any, @@ -53,7 +53,7 @@ describe("freeOrgCollectionLimitValidator", () => { it("returns null if organization has not reached collection limit (Observable)", async () => { const org = { id: "org-id", maxCollections: 2 } as Organization; const collections = [{ organizationId: "org-id" } as Collection]; - const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService); + const validator = freeOrgCollectionLimitValidator(of([org]), of(collections), i18nService); const control = new FormControl("org-id"); const result$ = validator(control) as Observable; @@ -65,7 +65,7 @@ describe("freeOrgCollectionLimitValidator", () => { it("returns error if organization has reached collection limit (Observable)", async () => { const org = { id: "org-id", maxCollections: 1 } as Organization; const collections = [{ organizationId: "org-id" } as Collection]; - const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService); + const validator = freeOrgCollectionLimitValidator(of([org]), of(collections), i18nService); const control = new FormControl("org-id"); const result$ = validator(control) as Observable; diff --git a/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts b/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts index 75919d31c1a..7132428c375 100644 --- a/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts +++ b/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts @@ -1,13 +1,14 @@ import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms"; -import { map, Observable, of } from "rxjs"; +import { combineLatest, map, Observable, of } from "rxjs"; import { Collection } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { getById } from "@bitwarden/common/platform/misc"; export function freeOrgCollectionLimitValidator( - orgs: Observable, - collections: Collection[], + organizations$: Observable, + collections$: Observable, i18nService: I18nService, ): AsyncValidatorFn { return (control: AbstractControl): Observable => { @@ -21,15 +22,16 @@ export function freeOrgCollectionLimitValidator( return of(null); } - return orgs.pipe( - map((organizations) => organizations.find((org) => org.id === orgId)), - map((org) => { - if (!org) { + return combineLatest([organizations$.pipe(getById(orgId)), collections$]).pipe( + map(([organization, collections]) => { + if (!organization) { return null; } - const orgCollections = collections.filter((c) => c.organizationId === org.id); - const hasReachedLimit = org.maxCollections === orgCollections.length; + const orgCollections = collections.filter( + (collection: Collection) => collection.organizationId === organization.id, + ); + const hasReachedLimit = organization.maxCollections === orgCollections.length; if (hasReachedLimit) { return { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index ada73dd0059..ceb2c788e75 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -285,7 +285,6 @@ export class AppComponent implements OnDestroy, OnInit { this.keyService.clearKeys(userId), this.cipherService.clear(userId), this.folderService.clear(userId), - this.collectionService.clear(userId), this.biometricStateService.logout(userId), ]); diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 128afdcccfc..78abad1ebf8 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -9,7 +9,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherBulkDeleteRequest } from "@bitwarden/common/vault/models/request/cipher-bulk-delete.request"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; @@ -68,7 +68,6 @@ export class BulkDeleteDialogComponent { @Inject(DIALOG_DATA) params: BulkDeleteDialogParams, private dialogRef: DialogRef, private cipherService: CipherService, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private apiService: ApiService, private collectionService: CollectionService, @@ -116,7 +115,11 @@ export class BulkDeleteDialogComponent { }); } if (this.collections.length) { - await this.collectionService.delete(this.collections.map((c) => c.id)); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.collectionService.delete( + this.collections.map((c) => c.id as CollectionId), + userId, + ); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 93189f2bf1c..b7a19bf2e76 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -78,7 +78,7 @@ describe("vault filter service", () => { configService.getFeatureFlag$.mockReturnValue(of(true)); organizationService.memberOrganizations$.mockReturnValue(organizations); folderService.folderViews$.mockReturnValue(folderViews); - collectionService.decryptedCollections$ = collectionViews; + collectionService.decryptedCollections$.mockReturnValue(collectionViews); policyService.policyAppliesToUser$ .calledWith(PolicyType.OrganizationDataOwnership, mockUserId) .mockReturnValue(organizationDataOwnershipPolicy); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 1fe618c6c4e..266676e418b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -4,7 +4,6 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, combineLatest, - combineLatestWith, filter, firstValueFrom, map, @@ -100,13 +99,13 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { map((folders) => this.buildFolderTree(folders)), ); - filteredCollections$: Observable = - this.collectionService.decryptedCollections$.pipe( - combineLatestWith(this._organizationFilter), - switchMap(([collections, org]) => { - return this.filterCollections(collections, org); - }), - ); + filteredCollections$: Observable = combineLatest([ + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionService.decryptedCollections$(userId)), + ), + this._organizationFilter, + ]).pipe(switchMap(([collections, org]) => this.filterCollections(collections, org))); collectionTree$: Observable> = combineLatest([ this.filteredCollections$, diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index c8c2f681bb4..09284cec9c9 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -334,7 +334,8 @@ export class VaultComponent implements OnInit, OnDestr }); const filter$ = this.routedVaultFilterService.filter$; - const allCollections$ = this.collectionService.decryptedCollections$; + + const allCollections$ = this.collectionService.decryptedCollections$(activeUserId); const nestedCollections$ = allCollections$.pipe( map((collections) => getNestedCollectionTree(collections)), ); @@ -861,7 +862,10 @@ export class VaultComponent implements OnInit, OnDestr if (result.collection) { // Update CollectionService with the new collection const c = new CollectionData(result.collection as CollectionDetailsResponse); - await this.collectionService.upsert(c); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + await this.collectionService.upsert(c, activeUserId); } this.refresh(); } @@ -878,20 +882,23 @@ export class VaultComponent implements OnInit, OnDestr }); const result = await lastValueFrom(dialog.closed); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (result.action === CollectionDialogAction.Saved) { if (result.collection) { // Update CollectionService with the new collection const c = new CollectionData(result.collection as CollectionDetailsResponse); - await this.collectionService.upsert(c); + await this.collectionService.upsert(c, activeUserId); } this.refresh(); } else if (result.action === CollectionDialogAction.Deleted) { - await this.collectionService.delete(result.collection?.id); - this.refresh(); + const parent = this.selectedCollection?.parent; // Navigate away if we deleted the collection we were viewing - if (this.selectedCollection?.node.id === c?.id) { + const navigateAway = this.selectedCollection && this.selectedCollection.node.id === c.id; + await this.collectionService.delete([result.collection?.id as CollectionId], activeUserId); + this.refresh(); + if (navigateAway) { await this.router.navigate([], { - queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParams: { collectionId: parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, }); @@ -916,18 +923,22 @@ export class VaultComponent implements OnInit, OnDestr return; } try { + const parent = this.selectedCollection?.parent; + // Navigate away if we deleted the collection we were viewing + const navigateAway = + this.selectedCollection && this.selectedCollection.node.id === collection.id; await this.apiService.deleteCollection(collection.organizationId, collection.id); - await this.collectionService.delete(collection.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.collectionService.delete([collection.id as CollectionId], activeUserId); this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("deletedCollectionId", collection.name), }); - // Navigate away if we deleted the collection we were viewing - if (this.selectedCollection?.node.id === collection.id) { + if (navigateAway) { await this.router.navigate([], { - queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParams: { collectionId: parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, }); diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts index 36222b16794..083539e9f6e 100644 --- a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -1,15 +1,20 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CollectionDetailsResponse } from "@bitwarden/admin-console/common"; +import { UserId } from "@bitwarden/common/types/guid"; import { CollectionAccessSelectionView, CollectionAdminView } from "../models"; export abstract class CollectionAdminService { - getAll: (organizationId: string) => Promise; - get: (organizationId: string, collectionId: string) => Promise; - save: (collection: CollectionAdminView) => Promise; - delete: (organizationId: string, collectionId: string) => Promise; - bulkAssignAccess: ( + abstract getAll: (organizationId: string) => Promise; + abstract get: ( + organizationId: string, + collectionId: string, + ) => Promise; + abstract save: ( + collection: CollectionAdminView, + userId: UserId, + ) => Promise; + abstract delete: (organizationId: string, collectionId: string) => Promise; + abstract bulkAssignAccess: ( organizationId: string, collectionIds: string[], users: CollectionAccessSelectionView[], diff --git a/libs/admin-console/src/common/collections/abstractions/collection.service.ts b/libs/admin-console/src/common/collections/abstractions/collection.service.ts index 61fc94b271c..e69f96232da 100644 --- a/libs/admin-console/src/common/collections/abstractions/collection.service.ts +++ b/libs/admin-console/src/common/collections/abstractions/collection.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; @@ -9,27 +7,25 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionData, Collection, CollectionView } from "../models"; export abstract class CollectionService { - encryptedCollections$: Observable; - decryptedCollections$: Observable; - - clearActiveUserCache: () => Promise; - encrypt: (model: CollectionView) => Promise; - decryptedCollectionViews$: (ids: CollectionId[]) => Observable; + abstract encryptedCollections$: (userId: UserId) => Observable; + abstract decryptedCollections$: (userId: UserId) => Observable; + abstract upsert: (collection: CollectionData, userId: UserId) => Promise; + abstract replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise; /** - * @deprecated This method will soon be made private - * See PM-12375 + * @deprecated This method will soon be made private, use `decryptedCollections$` instead. */ - decryptMany: ( + abstract decryptMany$: ( collections: Collection[], - orgKeys?: Record, - ) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getAllDecrypted: () => Promise; - getAllNested: (collections?: CollectionView[]) => Promise[]>; - getNested: (id: string) => Promise>; - upsert: (collection: CollectionData | CollectionData[]) => Promise; - replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise; - clear: (userId?: string) => Promise; - delete: (id: string | string[]) => Promise; + orgKeys: Record, + ) => Observable; + abstract delete: (ids: CollectionId[], userId: UserId) => Promise; + abstract encrypt: (model: CollectionView, userId: UserId) => Promise; + /** + * Transforms the input CollectionViews into TreeNodes + */ + abstract getAllNested: (collections: CollectionView[]) => TreeNode[]; + /* + * Transforms the input CollectionViews into TreeNodes and then returns the Treenode with the specified id + */ + abstract getNested: (collections: CollectionView[], id: string) => TreeNode; } diff --git a/libs/admin-console/src/common/collections/abstractions/vnext-collection.service.ts b/libs/admin-console/src/common/collections/abstractions/vnext-collection.service.ts deleted file mode 100644 index e1b2a5759a1..00000000000 --- a/libs/admin-console/src/common/collections/abstractions/vnext-collection.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Observable } from "rxjs"; - -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; -import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; - -import { CollectionData, Collection, CollectionView } from "../models"; - -export abstract class vNextCollectionService { - encryptedCollections$: (userId: UserId) => Observable; - decryptedCollections$: (userId: UserId) => Observable; - upsert: (collection: CollectionData | CollectionData[], userId: UserId) => Promise; - replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise; - /** - * Clear decrypted state without affecting encrypted state. - * Used for locking the vault. - */ - clearDecryptedState: (userId: UserId) => Promise; - /** - * Clear decrypted and encrypted state. - * Used for logging out. - */ - clear: (userId: UserId) => Promise; - delete: (id: string | string[], userId: UserId) => Promise; - encrypt: (model: CollectionView) => Promise; - /** - * @deprecated This method will soon be made private, use `decryptedCollections$` instead. - */ - decryptMany: ( - collections: Collection[], - orgKeys?: Record | null, - ) => Promise; - /** - * Transforms the input CollectionViews into TreeNodes - */ - getAllNested: (collections: CollectionView[]) => TreeNode[]; - /** - * Transforms the input CollectionViews into TreeNodes and then returns the Treenode with the specified id - */ - getNested: (collections: CollectionView[], id: string) => TreeNode; -} diff --git a/libs/admin-console/src/common/collections/models/collection.data.ts b/libs/admin-console/src/common/collections/models/collection.data.ts index b28a066509c..27c5c0c0efa 100644 --- a/libs/admin-console/src/common/collections/models/collection.data.ts +++ b/libs/admin-console/src/common/collections/models/collection.data.ts @@ -26,7 +26,10 @@ export class CollectionData { this.type = response.type; } - static fromJSON(obj: Jsonify) { + static fromJSON(obj: Jsonify): CollectionData | null { + if (obj == null) { + return null; + } return Object.assign(new CollectionData(new CollectionDetailsResponse({})), obj); } } diff --git a/libs/admin-console/src/common/collections/models/collection.ts b/libs/admin-console/src/common/collections/models/collection.ts index 75d68222b38..7bbd018fa96 100644 --- a/libs/admin-console/src/common/collections/models/collection.ts +++ b/libs/admin-console/src/common/collections/models/collection.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import Domain from "@bitwarden/common/platform/models/domain/domain-base"; +import Domain, { EncryptableKeys } from "@bitwarden/common/platform/models/domain/domain-base"; import { OrgKey } from "@bitwarden/common/types/key"; import { CollectionData } from "./collection.data"; @@ -15,16 +13,16 @@ export const CollectionTypes = { export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes]; export class Collection extends Domain { - id: string; - organizationId: string; - name: EncString; - externalId: string; - readOnly: boolean; - hidePasswords: boolean; - manage: boolean; - type: CollectionType; + id: string | undefined; + organizationId: string | undefined; + name: EncString | undefined; + externalId: string | undefined; + readOnly: boolean = false; + hidePasswords: boolean = false; + manage: boolean = false; + type: CollectionType = CollectionTypes.SharedCollection; - constructor(obj?: CollectionData) { + constructor(obj?: CollectionData | null) { super(); if (obj == null) { return; @@ -51,8 +49,8 @@ export class Collection extends Domain { return this.decryptObj( this, new CollectionView(this), - ["name"], - this.organizationId, + ["name"] as EncryptableKeys[], + this.organizationId ?? null, orgKey, ); } diff --git a/libs/admin-console/src/common/collections/models/collection.view.ts b/libs/admin-console/src/common/collections/models/collection.view.ts index bce1d558f96..f75ff565100 100644 --- a/libs/admin-console/src/common/collections/models/collection.view.ts +++ b/libs/admin-console/src/common/collections/models/collection.view.ts @@ -12,7 +12,7 @@ export const NestingDelimiter = "/"; export class CollectionView implements View, ITreeNodeObject { id: string | undefined; organizationId: string | undefined; - name: string | undefined; + name: string = ""; externalId: string | undefined; // readOnly applies to the items within a collection readOnly: boolean = false; diff --git a/libs/admin-console/src/common/collections/services/collection.state.ts b/libs/admin-console/src/common/collections/services/collection.state.ts new file mode 100644 index 00000000000..ebb620c2354 --- /dev/null +++ b/libs/admin-console/src/common/collections/services/collection.state.ts @@ -0,0 +1,28 @@ +import { Jsonify } from "type-fest"; + +import { + COLLECTION_DISK, + COLLECTION_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; +import { CollectionId } from "@bitwarden/common/types/guid"; + +import { CollectionData, CollectionView } from "../models"; + +export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record< + CollectionData | null, + CollectionId +>(COLLECTION_DISK, "collections", { + deserializer: (jsonData: Jsonify) => CollectionData.fromJSON(jsonData), + clearOn: ["logout"], +}); + +export const DECRYPTED_COLLECTION_DATA_KEY = new UserKeyDefinition( + COLLECTION_MEMORY, + "decryptedCollections", + { + deserializer: (obj: Jsonify) => + obj?.map((f) => CollectionView.fromJSON(f)) ?? null, + clearOn: ["logout", "lock"], + }, +); diff --git a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index 325b17cbd56..a00be4e5174 100644 --- a/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -1,9 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore + import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; import { CollectionAdminService, CollectionService } from "../abstractions"; @@ -55,7 +57,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService { return view; } - async save(collection: CollectionAdminView): Promise { + async save(collection: CollectionAdminView, userId: UserId): Promise { const request = await this.encrypt(collection); let response: CollectionDetailsResponse; @@ -71,9 +73,9 @@ export class DefaultCollectionAdminService implements CollectionAdminService { } if (response.assigned) { - await this.collectionService.upsert(new CollectionData(response)); + await this.collectionService.upsert(new CollectionData(response), userId); } else { - await this.collectionService.delete(collection.id); + await this.collectionService.delete([collection.id as CollectionId], userId); } return response; diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts index 57bed5e4ca5..c2c0332a486 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.spec.ts @@ -1,10 +1,11 @@ -import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { mock, MockProxy } from "jest-mock-extended"; +import { combineLatest, first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { FakeStateProvider, @@ -16,124 +17,382 @@ import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/gu import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; -import { CollectionData } from "../models"; +import { CollectionData, CollectionView } from "../models"; -import { - DefaultCollectionService, - ENCRYPTED_COLLECTION_DATA_KEY, -} from "./default-collection.service"; +import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state"; +import { DefaultCollectionService } from "./default-collection.service"; describe("DefaultCollectionService", () => { + let keyService: MockProxy; + let encryptService: MockProxy; + let i18nService: MockProxy; + let stateProvider: FakeStateProvider; + + let userId: UserId; + + let cryptoKeys: ReplaySubject | null>; + + let collectionService: DefaultCollectionService; + + beforeEach(() => { + userId = Utils.newGuid() as UserId; + + keyService = mock(); + encryptService = mock(); + i18nService = mock(); + stateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); + + cryptoKeys = new ReplaySubject(1); + keyService.orgKeys$.mockReturnValue(cryptoKeys); + + // Set up mock decryption + encryptService.decryptString + .calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey)) + .mockImplementation((encString, key) => + Promise.resolve(encString.data.replace("ENC_", "DEC_")), + ); + + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); + + // Arrange i18nService so that sorting algorithm doesn't throw + i18nService.collator = null; + + collectionService = new DefaultCollectionService( + keyService, + encryptService, + i18nService, + stateProvider, + ); + }); + afterEach(() => { delete (window as any).bitwardenContainerService; }); describe("decryptedCollections$", () => { it("emits decrypted collections from state", async () => { - // Arrange test collections + // Arrange test data const org1 = Utils.newGuid() as OrganizationId; - const org2 = Utils.newGuid() as OrganizationId; - + const orgKey1 = makeSymmetricCryptoKey(64, 1); const collection1 = collectionDataFactory(org1); + + const org2 = Utils.newGuid() as OrganizationId; + const orgKey2 = makeSymmetricCryptoKey(64, 2); const collection2 = collectionDataFactory(org2); - // Arrange state provider - const fakeStateProvider = mockStateProvider(); - await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, { - [collection1.id]: collection1, - [collection2.id]: collection2, + // Arrange dependencies + await setEncryptedState([collection1, collection2]); + cryptoKeys.next({ + [org1]: orgKey1, + [org2]: orgKey2, }); - // Arrange cryptoService - orgKeys and mock decryption - const cryptoService = mockCryptoService(); - cryptoService.orgKeys$.mockReturnValue( - of({ - [org1]: makeSymmetricCryptoKey(), - [org2]: makeSymmetricCryptoKey(), - }), - ); + const result = await firstValueFrom(collectionService.decryptedCollections$(userId)); - const collectionService = new DefaultCollectionService( - cryptoService, - mock(), - mockI18nService(), - fakeStateProvider, - ); - - const result = await firstValueFrom(collectionService.decryptedCollections$); + // Assert emitted values expect(result.length).toBe(2); - expect(result[0]).toMatchObject({ - id: collection1.id, - name: "DECRYPTED_STRING", - }); - expect(result[1]).toMatchObject({ - id: collection2.id, - name: "DECRYPTED_STRING", - }); + expect(result).toContainPartialObjects([ + { + id: collection1.id, + name: "DEC_NAME_" + collection1.id, + }, + { + id: collection2.id, + name: "DEC_NAME_" + collection2.id, + }, + ]); + + // Assert that the correct org keys were used for each encrypted string + // This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged + expect(encryptService.decryptString).toHaveBeenCalledWith( + expect.objectContaining(new EncString(collection1.name)), + orgKey1, + ); + expect(encryptService.decryptString).toHaveBeenCalledWith( + expect.objectContaining(new EncString(collection2.name)), + orgKey2, + ); + }); + + it("emits decrypted collections from in-memory state when available", async () => { + // Arrange test data + const org1 = Utils.newGuid() as OrganizationId; + const collection1 = collectionViewDataFactory(org1); + + const org2 = Utils.newGuid() as OrganizationId; + const collection2 = collectionViewDataFactory(org2); + + await setDecryptedState([collection1, collection2]); + + const result = await firstValueFrom(collectionService.decryptedCollections$(userId)); + + // Assert emitted values + expect(result.length).toBe(2); + expect(result).toContainPartialObjects([ + { + id: collection1.id, + name: "DEC_NAME_" + collection1.id, + }, + { + id: collection2.id, + name: "DEC_NAME_" + collection2.id, + }, + ]); + + // Ensure that the returned data came from the in-memory state, rather than from decryption. + expect(encryptService.decryptString).not.toHaveBeenCalled(); }); it("handles null collection state", async () => { - // Arrange test collections + // Arrange dependencies + await setEncryptedState(null); + cryptoKeys.next({}); + + const encryptedCollections = await firstValueFrom( + collectionService.encryptedCollections$(userId), + ); + + expect(encryptedCollections).toBe(null); + }); + + it("handles undefined orgKeys", (done) => { + // Arrange test data const org1 = Utils.newGuid() as OrganizationId; + const collection1 = collectionDataFactory(org1); + const org2 = Utils.newGuid() as OrganizationId; + const collection2 = collectionDataFactory(org2); - // Arrange state provider - const fakeStateProvider = mockStateProvider(); - await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null); + // Emit a non-null value after the first undefined value has propagated + // This will cause the collections to emit, calling done() + cryptoKeys.pipe(first()).subscribe((val) => { + cryptoKeys.next({}); + }); - // Arrange cryptoService - orgKeys and mock decryption - const cryptoService = mockCryptoService(); - cryptoService.orgKeys$.mockReturnValue( - of({ - [org1]: makeSymmetricCryptoKey(), - [org2]: makeSymmetricCryptoKey(), - }), - ); + collectionService + .decryptedCollections$(userId) + .pipe(takeWhile((val) => val.length != 2)) + .subscribe({ complete: () => done() }); - const collectionService = new DefaultCollectionService( - cryptoService, - mock(), - mockI18nService(), - fakeStateProvider, - ); + // Arrange dependencies + void setEncryptedState([collection1, collection2]).then(() => { + // Act: emit undefined + cryptoKeys.next(undefined); + keyService.activeUserOrgKeys$ = of(undefined); + }); + }); - const decryptedCollections = await firstValueFrom(collectionService.decryptedCollections$); - expect(decryptedCollections.length).toBe(0); + it("Decrypts one time for multiple simultaneous callers", async () => { + const decryptedMock: CollectionView[] = [{ id: "col1" }] as CollectionView[]; + const decryptManySpy = jest + .spyOn(collectionService, "decryptMany$") + .mockReturnValue(of(decryptedMock)); - const encryptedCollections = await firstValueFrom(collectionService.encryptedCollections$); - expect(encryptedCollections.length).toBe(0); + jest + .spyOn(collectionService as any, "encryptedCollections$") + .mockReturnValue(of([{ id: "enc1" }])); + jest.spyOn(keyService, "orgKeys$").mockReturnValue(of({ key: "fake-key" })); + + // Simulate multiple subscribers + const obs1 = collectionService.decryptedCollections$(userId); + const obs2 = collectionService.decryptedCollections$(userId); + const obs3 = collectionService.decryptedCollections$(userId); + + await firstValueFrom(combineLatest([obs1, obs2, obs3])); + + // Expect decryptMany$ to be called only once + expect(decryptManySpy).toHaveBeenCalledTimes(1); }); }); + + describe("encryptedCollections$", () => { + it("emits encrypted collections from state", async () => { + // Arrange test data + const collection1 = collectionDataFactory(); + const collection2 = collectionDataFactory(); + + // Arrange dependencies + await setEncryptedState([collection1, collection2]); + + const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); + + expect(result!.length).toBe(2); + expect(result).toContainPartialObjects([ + { + id: collection1.id, + name: makeEncString("ENC_NAME_" + collection1.id), + }, + { + id: collection2.id, + name: makeEncString("ENC_NAME_" + collection2.id), + }, + ]); + }); + + it("handles null collection state", async () => { + await setEncryptedState(null); + + const decryptedCollections = await firstValueFrom( + collectionService.encryptedCollections$(userId), + ); + expect(decryptedCollections).toBe(null); + }); + }); + + describe("upsert", () => { + it("upserts to existing collections", async () => { + const org1 = Utils.newGuid() as OrganizationId; + const orgKey1 = makeSymmetricCryptoKey(64, 1); + const collection1 = collectionDataFactory(org1); + + await setEncryptedState([collection1]); + cryptoKeys.next({ + [collection1.organizationId]: orgKey1, + }); + + const updatedCollection1 = Object.assign(new CollectionData({} as any), collection1, { + name: makeEncString("UPDATED_ENC_NAME_" + collection1.id).encryptedString, + }); + + await collectionService.upsert(updatedCollection1, userId); + + const encryptedResult = await firstValueFrom(collectionService.encryptedCollections$(userId)); + + expect(encryptedResult!.length).toBe(1); + expect(encryptedResult).toContainPartialObjects([ + { + id: collection1.id, + name: makeEncString("UPDATED_ENC_NAME_" + collection1.id), + }, + ]); + + const decryptedResult = await firstValueFrom(collectionService.decryptedCollections$(userId)); + expect(decryptedResult.length).toBe(1); + expect(decryptedResult).toContainPartialObjects([ + { + id: collection1.id, + name: "UPDATED_DEC_NAME_" + collection1.id, + }, + ]); + }); + + it("upserts to a null state", async () => { + const org1 = Utils.newGuid() as OrganizationId; + const orgKey1 = makeSymmetricCryptoKey(64, 1); + const collection1 = collectionDataFactory(org1); + + cryptoKeys.next({ + [collection1.organizationId]: orgKey1, + }); + + await setEncryptedState(null); + + await collectionService.upsert(collection1, userId); + + const encryptedResult = await firstValueFrom(collectionService.encryptedCollections$(userId)); + expect(encryptedResult!.length).toBe(1); + expect(encryptedResult).toContainPartialObjects([ + { + id: collection1.id, + name: makeEncString("ENC_NAME_" + collection1.id), + }, + ]); + + const decryptedResult = await firstValueFrom(collectionService.decryptedCollections$(userId)); + expect(decryptedResult.length).toBe(1); + expect(decryptedResult).toContainPartialObjects([ + { + id: collection1.id, + name: "DEC_NAME_" + collection1.id, + }, + ]); + }); + }); + + describe("replace", () => { + it("replaces all collections", async () => { + await setEncryptedState([collectionDataFactory(), collectionDataFactory()]); + + const newCollection3 = collectionDataFactory(); + await collectionService.replace( + { + [newCollection3.id]: newCollection3, + }, + userId, + ); + + const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); + expect(result!.length).toBe(1); + expect(result).toContainPartialObjects([ + { + id: newCollection3.id, + name: makeEncString("ENC_NAME_" + newCollection3.id), + }, + ]); + }); + }); + + describe("delete", () => { + it("deletes a collection", async () => { + const collection1 = collectionDataFactory(); + const collection2 = collectionDataFactory(); + await setEncryptedState([collection1, collection2]); + + await collectionService.delete([collection1.id], userId); + + const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); + expect(result!.length).toEqual(1); + expect(result![0]).toMatchObject({ id: collection2.id }); + }); + + it("deletes several collections", async () => { + const collection1 = collectionDataFactory(); + const collection2 = collectionDataFactory(); + const collection3 = collectionDataFactory(); + await setEncryptedState([collection1, collection2, collection3]); + + await collectionService.delete([collection1.id, collection3.id], userId); + + const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); + expect(result!.length).toEqual(1); + expect(result![0]).toMatchObject({ id: collection2.id }); + }); + + it("handles null collections", async () => { + const collection1 = collectionDataFactory(); + await setEncryptedState(null); + + await collectionService.delete([collection1.id], userId); + + const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); + expect(result!.length).toEqual(0); + }); + }); + + const setEncryptedState = (collectionData: CollectionData[] | null) => + stateProvider.setUserState( + ENCRYPTED_COLLECTION_DATA_KEY, + collectionData == null ? null : Object.fromEntries(collectionData.map((c) => [c.id, c])), + userId, + ); + + const setDecryptedState = (collectionViews: CollectionView[] | null) => + stateProvider.setUserState(DECRYPTED_COLLECTION_DATA_KEY, collectionViews, userId); }); -const mockI18nService = () => { - const i18nService = mock(); - i18nService.collator = null; // this is a mock only, avoid use of this object - return i18nService; -}; - -const mockStateProvider = () => { - const userId = Utils.newGuid() as UserId; - return new FakeStateProvider(mockAccountServiceWith(userId)); -}; - -const mockCryptoService = () => { - const keyService = mock(); - const encryptService = mock(); - encryptService.decryptString - .calledWith(expect.any(EncString), expect.anything()) - .mockResolvedValue("DECRYPTED_STRING"); - - (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - - return keyService; -}; - -const collectionDataFactory = (orgId: OrganizationId) => { +const collectionDataFactory = (orgId?: OrganizationId) => { const collection = new CollectionData({} as any); collection.id = Utils.newGuid() as CollectionId; - collection.organizationId = orgId; - collection.name = makeEncString("ENC_STRING").encryptedString; + collection.organizationId = orgId ?? (Utils.newGuid() as OrganizationId); + collection.name = makeEncString("ENC_NAME_" + collection.id).encryptedString ?? ""; return collection; }; + +function collectionViewDataFactory(orgId?: OrganizationId): CollectionView { + const collectionView = new CollectionView(); + collectionView.id = Utils.newGuid() as CollectionId; + collectionView.organizationId = orgId ?? (Utils.newGuid() as OrganizationId); + collectionView.name = "DEC_NAME_" + collectionView.id; + return collectionView; +} diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts index a1dd0419e2c..4978b06df35 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts @@ -1,113 +1,193 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs"; -import { Jsonify } from "type-fest"; +import { + combineLatest, + delayWhen, + filter, + firstValueFrom, + from, + map, + NEVER, + Observable, + of, + shareReplay, + switchMap, +} from "rxjs"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { - ActiveUserState, - COLLECTION_DATA, - DeriveDefinition, - DerivedState, - StateProvider, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; +import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { KeyService } from "@bitwarden/key-management"; -import { CollectionService } from "../abstractions"; +import { CollectionService } from "../abstractions/collection.service"; import { Collection, CollectionData, CollectionView } from "../models"; -export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record( - COLLECTION_DATA, - "collections", - { - deserializer: (jsonData: Jsonify) => CollectionData.fromJSON(jsonData), - clearOn: ["logout"], - }, -); - -const DECRYPTED_COLLECTION_DATA_KEY = new DeriveDefinition< - [Record, Record], - CollectionView[], - { collectionService: DefaultCollectionService } ->(COLLECTION_DATA, "decryptedCollections", { - deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)), - derive: async ([collections, orgKeys], { collectionService }) => { - if (collections == null) { - return []; - } - - const data = Object.values(collections).map((c) => new Collection(c)); - return await collectionService.decryptMany(data, orgKeys); - }, -}); +import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state"; const NestingDelimiter = "/"; export class DefaultCollectionService implements CollectionService { - private encryptedCollectionDataState: ActiveUserState>; - encryptedCollections$: Observable; - private decryptedCollectionDataState: DerivedState; - decryptedCollections$: Observable; - - decryptedCollectionViews$(ids: CollectionId[]): Observable { - return this.decryptedCollections$.pipe( - map((collections) => collections.filter((c) => ids.includes(c.id as CollectionId))), - ); - } - constructor( private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, protected stateProvider: StateProvider, - ) { - this.encryptedCollectionDataState = this.stateProvider.getActive(ENCRYPTED_COLLECTION_DATA_KEY); + ) {} - this.encryptedCollections$ = this.encryptedCollectionDataState.state$.pipe( + private collectionViewCache = new Map>(); + + /** + * @returns a SingleUserState for encrypted collection data. + */ + private encryptedState( + userId: UserId, + ): SingleUserState> { + return this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY); + } + + /** + * @returns a SingleUserState for decrypted collection data. + */ + private decryptedState(userId: UserId): SingleUserState { + return this.stateProvider.getUser(userId, DECRYPTED_COLLECTION_DATA_KEY); + } + + encryptedCollections$(userId: UserId): Observable { + return this.encryptedState(userId).state$.pipe( map((collections) => { if (collections == null) { - return []; + return null; } return Object.values(collections).map((c) => new Collection(c)); }), ); + } - const encryptedCollectionsWithKeys = this.encryptedCollectionDataState.combinedState$.pipe( - switchMap(([userId, collectionData]) => - combineLatest([of(collectionData), this.keyService.orgKeys$(userId)]), + decryptedCollections$(userId: UserId): Observable { + const cachedResult = this.collectionViewCache.get(userId); + if (cachedResult) { + return cachedResult; + } + + const result$ = this.decryptedState(userId).state$.pipe( + switchMap((decryptedState) => { + // If decrypted state is already populated, return that + if (decryptedState !== null) { + return of(decryptedState ?? []); + } + + return this.initializeDecryptedState(userId).pipe(switchMap(() => NEVER)); + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + this.collectionViewCache.set(userId, result$); + return result$; + } + + private initializeDecryptedState(userId: UserId): Observable { + return combineLatest([ + this.encryptedCollections$(userId), + this.keyService.orgKeys$(userId).pipe(filter((orgKeys) => !!orgKeys)), + ]).pipe( + switchMap(([collections, orgKeys]) => + this.decryptMany$(collections, orgKeys).pipe( + delayWhen((collections) => this.setDecryptedCollections(collections, userId)), + ), ), - shareReplay({ refCount: false, bufferSize: 1 }), ); - - this.decryptedCollectionDataState = this.stateProvider.getDerived( - encryptedCollectionsWithKeys, - DECRYPTED_COLLECTION_DATA_KEY, - { collectionService: this }, - ); - - this.decryptedCollections$ = this.decryptedCollectionDataState.state$; } - async clearActiveUserCache(): Promise { - await this.decryptedCollectionDataState.forceValue(null); + async upsert(toUpdate: CollectionData, userId: UserId): Promise { + if (toUpdate == null) { + return; + } + await this.encryptedState(userId).update((collections) => { + if (collections == null) { + collections = {}; + } + collections[toUpdate.id] = toUpdate; + + return collections; + }); + + const decryptedCollections = await firstValueFrom( + this.keyService.orgKeys$(userId).pipe( + switchMap((orgKeys) => { + if (!orgKeys) { + throw new Error("No key for this collection's organization."); + } + return this.decryptMany$([new Collection(toUpdate)], orgKeys); + }), + ), + ); + + await this.decryptedState(userId).update((collections) => { + if (collections == null) { + collections = []; + } + + if (!decryptedCollections?.length) { + return collections; + } + + const decryptedCollection = decryptedCollections[0]; + const existingIndex = collections.findIndex((collection) => collection.id == toUpdate.id); + if (existingIndex >= 0) { + collections[existingIndex] = decryptedCollection; + } else { + collections.push(decryptedCollection); + } + + return collections; + }); } - async encrypt(model: CollectionView): Promise { + async replace(collections: Record, userId: UserId): Promise { + await this.encryptedState(userId).update(() => collections); + await this.decryptedState(userId).update(() => null); + } + + async delete(ids: CollectionId[], userId: UserId): Promise { + await this.encryptedState(userId).update((collections) => { + if (collections == null) { + collections = {}; + } + ids.forEach((i) => { + delete collections[i]; + }); + return collections; + }); + + await this.decryptedState(userId).update((collections) => { + if (collections == null) { + collections = []; + } + ids.forEach((i) => { + if (collections?.length) { + collections = collections.filter((c) => c.id != i) ?? []; + } + }); + return collections; + }); + } + + async encrypt(model: CollectionView, userId: UserId): Promise { if (model.organizationId == null) { throw new Error("Collection has no organization id."); } - const key = await this.keyService.getOrgKey(model.organizationId); - if (key == null) { - throw new Error("No key for this collection's organization."); - } + + const key = await firstValueFrom( + this.keyService.orgKeys$(userId).pipe( + filter((orgKeys) => !!orgKeys), + map((k) => k[model.organizationId as OrganizationId]), + ), + ); + const collection = new Collection(); collection.id = model.id; collection.organizationId = model.organizationId; @@ -117,58 +197,37 @@ export class DefaultCollectionService implements CollectionService { return collection; } - // TODO: this should be private and orgKeys should be required. + // TODO: this should be private. // See https://bitwarden.atlassian.net/browse/PM-12375 - async decryptMany( - collections: Collection[], - orgKeys?: Record, - ): Promise { - if (collections == null || collections.length === 0) { - return []; + decryptMany$( + collections: Collection[] | null, + orgKeys: Record, + ): Observable { + if (collections === null || collections.length == 0 || orgKeys === null) { + return of([]); } - const decCollections: CollectionView[] = []; - orgKeys ??= await firstValueFrom(this.keyService.activeUserOrgKeys$); + const decCollections: Observable[] = []; - const promises: Promise[] = []; collections.forEach((collection) => { - promises.push( - collection - .decrypt(orgKeys[collection.organizationId as OrganizationId]) - .then((c) => decCollections.push(c)), + decCollections.push( + from(collection.decrypt(orgKeys[collection.organizationId as OrganizationId])), ); }); - await Promise.all(promises); - return decCollections.sort(Utils.getSortFunction(this.i18nService, "name")); - } - async get(id: string): Promise { - return ( - (await firstValueFrom( - this.encryptedCollections$.pipe(map((cs) => cs.find((c) => c.id === id))), - )) ?? null + return combineLatest(decCollections).pipe( + map((collections) => collections.sort(Utils.getSortFunction(this.i18nService, "name"))), ); } - async getAll(): Promise { - return await firstValueFrom(this.encryptedCollections$); - } - - async getAllDecrypted(): Promise { - return await firstValueFrom(this.decryptedCollections$); - } - - async getAllNested(collections: CollectionView[] = null): Promise[]> { - if (collections == null) { - collections = await this.getAllDecrypted(); - } + getAllNested(collections: CollectionView[]): TreeNode[] { const nodes: TreeNode[] = []; collections.forEach((c) => { const collectionCopy = new CollectionView(); collectionCopy.id = c.id; collectionCopy.organizationId = c.organizationId; const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; - ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); + ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter); }); return nodes; } @@ -177,58 +236,23 @@ export class DefaultCollectionService implements CollectionService { * @deprecated August 30 2022: Moved to new Vault Filter Service * Remove when Desktop and Browser are updated */ - async getNested(id: string): Promise> { - const collections = await this.getAllNested(); - return ServiceUtils.getTreeNodeObjectFromList(collections, id) as TreeNode; + getNested(collections: CollectionView[], id: string): TreeNode { + const nestedCollections = this.getAllNested(collections); + return ServiceUtils.getTreeNodeObjectFromList( + nestedCollections, + id, + ) as TreeNode; } - async upsert(toUpdate: CollectionData | CollectionData[]): Promise { - if (toUpdate == null) { - return; - } - await this.encryptedCollectionDataState.update((collections) => { - if (collections == null) { - collections = {}; - } - if (Array.isArray(toUpdate)) { - toUpdate.forEach((c) => { - collections[c.id] = c; - }); - } else { - collections[toUpdate.id] = toUpdate; - } - return collections; - }); - } - - async replace(collections: Record, userId: UserId): Promise { - await this.stateProvider - .getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY) - .update(() => collections); - } - - async clear(userId?: UserId): Promise { - if (userId == null) { - await this.encryptedCollectionDataState.update(() => null); - await this.decryptedCollectionDataState.forceValue(null); - } else { - await this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY).update(() => null); - } - } - - async delete(id: CollectionId | CollectionId[]): Promise { - await this.encryptedCollectionDataState.update((collections) => { - if (collections == null) { - collections = {}; - } - if (typeof id === "string") { - delete collections[id]; - } else { - (id as CollectionId[]).forEach((i) => { - delete collections[i]; - }); - } - return collections; - }); + /** + * Sets the decrypted collections state for a user. + * @param collections the decrypted collections + * @param userId the user id + */ + private async setDecryptedCollections( + collections: CollectionView[], + userId: UserId, + ): Promise { + await this.stateProvider.setUserState(DECRYPTED_COLLECTION_DATA_KEY, collections, userId); } } diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts deleted file mode 100644 index 256157a03c1..00000000000 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { mock, MockProxy } from "jest-mock-extended"; -import { first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs"; - -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { ContainerService } from "@bitwarden/common/platform/services/container.service"; -import { - FakeStateProvider, - makeEncString, - makeSymmetricCryptoKey, - mockAccountServiceWith, -} from "@bitwarden/common/spec"; -import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; -import { KeyService } from "@bitwarden/key-management"; - -import { CollectionData } from "../models"; - -import { DefaultvNextCollectionService } from "./default-vnext-collection.service"; -import { ENCRYPTED_COLLECTION_DATA_KEY } from "./vnext-collection.state"; - -describe("DefaultvNextCollectionService", () => { - let keyService: MockProxy; - let encryptService: MockProxy; - let i18nService: MockProxy; - let stateProvider: FakeStateProvider; - - let userId: UserId; - - let cryptoKeys: ReplaySubject | null>; - - let collectionService: DefaultvNextCollectionService; - - beforeEach(() => { - userId = Utils.newGuid() as UserId; - - keyService = mock(); - encryptService = mock(); - i18nService = mock(); - stateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); - - cryptoKeys = new ReplaySubject(1); - keyService.orgKeys$.mockReturnValue(cryptoKeys); - - // Set up mock decryption - encryptService.decryptString - .calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey)) - .mockImplementation((encString, key) => - Promise.resolve(encString.data.replace("ENC_", "DEC_")), - ); - - (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - - // Arrange i18nService so that sorting algorithm doesn't throw - i18nService.collator = null; - - collectionService = new DefaultvNextCollectionService( - keyService, - encryptService, - i18nService, - stateProvider, - ); - }); - - afterEach(() => { - delete (window as any).bitwardenContainerService; - }); - - describe("decryptedCollections$", () => { - it("emits decrypted collections from state", async () => { - // Arrange test data - const org1 = Utils.newGuid() as OrganizationId; - const orgKey1 = makeSymmetricCryptoKey(64, 1); - const collection1 = collectionDataFactory(org1); - - const org2 = Utils.newGuid() as OrganizationId; - const orgKey2 = makeSymmetricCryptoKey(64, 2); - const collection2 = collectionDataFactory(org2); - - // Arrange dependencies - await setEncryptedState([collection1, collection2]); - cryptoKeys.next({ - [org1]: orgKey1, - [org2]: orgKey2, - }); - - const result = await firstValueFrom(collectionService.decryptedCollections$(userId)); - - // Assert emitted values - expect(result.length).toBe(2); - expect(result).toContainPartialObjects([ - { - id: collection1.id, - name: "DEC_NAME_" + collection1.id, - }, - { - id: collection2.id, - name: "DEC_NAME_" + collection2.id, - }, - ]); - - // Assert that the correct org keys were used for each encrypted string - // This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged - expect(encryptService.decryptString).toHaveBeenCalledWith( - expect.objectContaining(new EncString(collection1.name)), - orgKey1, - ); - expect(encryptService.decryptString).toHaveBeenCalledWith( - expect.objectContaining(new EncString(collection2.name)), - orgKey2, - ); - }); - - it("handles null collection state", async () => { - // Arrange dependencies - await setEncryptedState(null); - cryptoKeys.next({}); - - const encryptedCollections = await firstValueFrom( - collectionService.encryptedCollections$(userId), - ); - - expect(encryptedCollections.length).toBe(0); - }); - - it("handles undefined orgKeys", (done) => { - // Arrange test data - const org1 = Utils.newGuid() as OrganizationId; - const collection1 = collectionDataFactory(org1); - - const org2 = Utils.newGuid() as OrganizationId; - const collection2 = collectionDataFactory(org2); - - // Emit a non-null value after the first undefined value has propagated - // This will cause the collections to emit, calling done() - cryptoKeys.pipe(first()).subscribe((val) => { - cryptoKeys.next({}); - }); - - collectionService - .decryptedCollections$(userId) - .pipe(takeWhile((val) => val.length != 2)) - .subscribe({ complete: () => done() }); - - // Arrange dependencies - void setEncryptedState([collection1, collection2]).then(() => { - // Act: emit undefined - cryptoKeys.next(undefined); - keyService.activeUserOrgKeys$ = of(undefined); - }); - }); - }); - - describe("encryptedCollections$", () => { - it("emits encrypted collections from state", async () => { - // Arrange test data - const collection1 = collectionDataFactory(); - const collection2 = collectionDataFactory(); - - // Arrange dependencies - await setEncryptedState([collection1, collection2]); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - - expect(result.length).toBe(2); - expect(result).toContainPartialObjects([ - { - id: collection1.id, - name: makeEncString("ENC_NAME_" + collection1.id), - }, - { - id: collection2.id, - name: makeEncString("ENC_NAME_" + collection2.id), - }, - ]); - }); - - it("handles null collection state", async () => { - await setEncryptedState(null); - - const decryptedCollections = await firstValueFrom( - collectionService.encryptedCollections$(userId), - ); - expect(decryptedCollections.length).toBe(0); - }); - }); - - describe("upsert", () => { - it("upserts to existing collections", async () => { - const collection1 = collectionDataFactory(); - const collection2 = collectionDataFactory(); - - await setEncryptedState([collection1, collection2]); - - const updatedCollection1 = Object.assign(new CollectionData({} as any), collection1, { - name: makeEncString("UPDATED_ENC_NAME_" + collection1.id).encryptedString, - }); - const newCollection3 = collectionDataFactory(); - - await collectionService.upsert([updatedCollection1, newCollection3], userId); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(result.length).toBe(3); - expect(result).toContainPartialObjects([ - { - id: collection1.id, - name: makeEncString("UPDATED_ENC_NAME_" + collection1.id), - }, - { - id: collection2.id, - name: makeEncString("ENC_NAME_" + collection2.id), - }, - { - id: newCollection3.id, - name: makeEncString("ENC_NAME_" + newCollection3.id), - }, - ]); - }); - - it("upserts to a null state", async () => { - const collection1 = collectionDataFactory(); - - await setEncryptedState(null); - - await collectionService.upsert(collection1, userId); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(result.length).toBe(1); - expect(result).toContainPartialObjects([ - { - id: collection1.id, - name: makeEncString("ENC_NAME_" + collection1.id), - }, - ]); - }); - }); - - describe("replace", () => { - it("replaces all collections", async () => { - await setEncryptedState([collectionDataFactory(), collectionDataFactory()]); - - const newCollection3 = collectionDataFactory(); - await collectionService.replace( - { - [newCollection3.id]: newCollection3, - }, - userId, - ); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(result.length).toBe(1); - expect(result).toContainPartialObjects([ - { - id: newCollection3.id, - name: makeEncString("ENC_NAME_" + newCollection3.id), - }, - ]); - }); - }); - - it("clearDecryptedState", async () => { - await setEncryptedState([collectionDataFactory(), collectionDataFactory()]); - - await collectionService.clearDecryptedState(userId); - - // Encrypted state remains - const encryptedState = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(encryptedState.length).toEqual(2); - - // Decrypted state is cleared - const decryptedState = await firstValueFrom(collectionService.decryptedCollections$(userId)); - expect(decryptedState.length).toEqual(0); - }); - - it("clear", async () => { - await setEncryptedState([collectionDataFactory(), collectionDataFactory()]); - cryptoKeys.next({}); - - await collectionService.clear(userId); - - // Encrypted state is cleared - const encryptedState = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(encryptedState.length).toEqual(0); - - // Decrypted state is cleared - const decryptedState = await firstValueFrom(collectionService.decryptedCollections$(userId)); - expect(decryptedState.length).toEqual(0); - }); - - describe("delete", () => { - it("deletes a collection", async () => { - const collection1 = collectionDataFactory(); - const collection2 = collectionDataFactory(); - await setEncryptedState([collection1, collection2]); - - await collectionService.delete(collection1.id, userId); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(result.length).toEqual(1); - expect(result[0]).toMatchObject({ id: collection2.id }); - }); - - it("deletes several collections", async () => { - const collection1 = collectionDataFactory(); - const collection2 = collectionDataFactory(); - const collection3 = collectionDataFactory(); - await setEncryptedState([collection1, collection2, collection3]); - - await collectionService.delete([collection1.id, collection3.id], userId); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(result.length).toEqual(1); - expect(result[0]).toMatchObject({ id: collection2.id }); - }); - - it("handles null collections", async () => { - const collection1 = collectionDataFactory(); - await setEncryptedState(null); - - await collectionService.delete(collection1.id, userId); - - const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); - expect(result.length).toEqual(0); - }); - }); - - const setEncryptedState = (collectionData: CollectionData[] | null) => - stateProvider.setUserState( - ENCRYPTED_COLLECTION_DATA_KEY, - collectionData == null ? null : Object.fromEntries(collectionData.map((c) => [c.id, c])), - userId, - ); -}); - -const collectionDataFactory = (orgId?: OrganizationId) => { - const collection = new CollectionData({} as any); - collection.id = Utils.newGuid() as CollectionId; - collection.organizationId = orgId ?? (Utils.newGuid() as OrganizationId); - collection.name = makeEncString("ENC_NAME_" + collection.id).encryptedString; - - return collection; -}; diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts deleted file mode 100644 index 4dcda795afe..00000000000 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.ts +++ /dev/null @@ -1,194 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { combineLatest, filter, firstValueFrom, map } from "rxjs"; - -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { StateProvider, DerivedState } from "@bitwarden/common/platform/state"; -import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; -import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { KeyService } from "@bitwarden/key-management"; - -import { vNextCollectionService } from "../abstractions/vnext-collection.service"; -import { Collection, CollectionData, CollectionView } from "../models"; - -import { - DECRYPTED_COLLECTION_DATA_KEY, - ENCRYPTED_COLLECTION_DATA_KEY, -} from "./vnext-collection.state"; - -const NestingDelimiter = "/"; - -export class DefaultvNextCollectionService implements vNextCollectionService { - constructor( - private keyService: KeyService, - private encryptService: EncryptService, - private i18nService: I18nService, - protected stateProvider: StateProvider, - ) {} - - encryptedCollections$(userId: UserId) { - return this.encryptedState(userId).state$.pipe( - map((collections) => { - if (collections == null) { - return []; - } - - return Object.values(collections).map((c) => new Collection(c)); - }), - ); - } - - decryptedCollections$(userId: UserId) { - return this.decryptedState(userId).state$.pipe(map((collections) => collections ?? [])); - } - - async upsert(toUpdate: CollectionData | CollectionData[], userId: UserId): Promise { - if (toUpdate == null) { - return; - } - await this.encryptedState(userId).update((collections) => { - if (collections == null) { - collections = {}; - } - if (Array.isArray(toUpdate)) { - toUpdate.forEach((c) => { - collections[c.id] = c; - }); - } else { - collections[toUpdate.id] = toUpdate; - } - return collections; - }); - } - - async replace(collections: Record, userId: UserId): Promise { - await this.encryptedState(userId).update(() => collections); - } - - async clearDecryptedState(userId: UserId): Promise { - if (userId == null) { - throw new Error("User ID is required."); - } - - await this.decryptedState(userId).forceValue([]); - } - - async clear(userId: UserId): Promise { - await this.encryptedState(userId).update(() => null); - // This will propagate from the encrypted state update, but by doing it explicitly - // the promise doesn't resolve until the update is complete. - await this.decryptedState(userId).forceValue([]); - } - - async delete(id: CollectionId | CollectionId[], userId: UserId): Promise { - await this.encryptedState(userId).update((collections) => { - if (collections == null) { - collections = {}; - } - if (typeof id === "string") { - delete collections[id]; - } else { - (id as CollectionId[]).forEach((i) => { - delete collections[i]; - }); - } - return collections; - }); - } - - async encrypt(model: CollectionView): Promise { - if (model.organizationId == null) { - throw new Error("Collection has no organization id."); - } - const key = await this.keyService.getOrgKey(model.organizationId); - if (key == null) { - throw new Error("No key for this collection's organization."); - } - const collection = new Collection(); - collection.id = model.id; - collection.organizationId = model.organizationId; - collection.readOnly = model.readOnly; - collection.externalId = model.externalId; - collection.name = await this.encryptService.encryptString(model.name, key); - return collection; - } - - // TODO: this should be private and orgKeys should be required. - // See https://bitwarden.atlassian.net/browse/PM-12375 - async decryptMany( - collections: Collection[], - orgKeys?: Record | null, - ): Promise { - if (collections == null || collections.length === 0) { - return []; - } - const decCollections: CollectionView[] = []; - - orgKeys ??= await firstValueFrom(this.keyService.activeUserOrgKeys$); - - const promises: Promise[] = []; - collections.forEach((collection) => { - promises.push( - collection - .decrypt(orgKeys[collection.organizationId as OrganizationId]) - .then((c) => decCollections.push(c)), - ); - }); - await Promise.all(promises); - return decCollections.sort(Utils.getSortFunction(this.i18nService, "name")); - } - - getAllNested(collections: CollectionView[]): TreeNode[] { - const nodes: TreeNode[] = []; - collections.forEach((c) => { - const collectionCopy = new CollectionView(); - collectionCopy.id = c.id; - collectionCopy.organizationId = c.organizationId; - const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : []; - ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter); - }); - return nodes; - } - - /** - * @deprecated August 30 2022: Moved to new Vault Filter Service - * Remove when Desktop and Browser are updated - */ - getNested(collections: CollectionView[], id: string): TreeNode { - const nestedCollections = this.getAllNested(collections); - return ServiceUtils.getTreeNodeObjectFromList( - nestedCollections, - id, - ) as TreeNode; - } - - /** - * @returns a SingleUserState for encrypted collection data. - */ - private encryptedState(userId: UserId) { - return this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY); - } - - /** - * @returns a SingleUserState for decrypted collection data. - */ - private decryptedState(userId: UserId): DerivedState { - const encryptedCollectionsWithKeys$ = combineLatest([ - this.encryptedCollections$(userId), - // orgKeys$ can emit null during brief moments on unlock and lock/logout, we want to ignore those intermediate states - this.keyService.orgKeys$(userId).pipe(filter((orgKeys) => orgKeys != null)), - ]); - - return this.stateProvider.getDerived( - encryptedCollectionsWithKeys$, - DECRYPTED_COLLECTION_DATA_KEY, - { - collectionService: this, - }, - ); - } -} diff --git a/libs/admin-console/src/common/collections/services/vnext-collection.state.ts b/libs/admin-console/src/common/collections/services/vnext-collection.state.ts deleted file mode 100644 index 331c80436f7..00000000000 --- a/libs/admin-console/src/common/collections/services/vnext-collection.state.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { - COLLECTION_DATA, - DeriveDefinition, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; -import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; - -import { vNextCollectionService } from "../abstractions/vnext-collection.service"; -import { Collection, CollectionData, CollectionView } from "../models"; - -export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record( - COLLECTION_DATA, - "collections", - { - deserializer: (jsonData: Jsonify) => CollectionData.fromJSON(jsonData), - clearOn: ["logout"], - }, -); - -export const DECRYPTED_COLLECTION_DATA_KEY = new DeriveDefinition< - [Collection[], Record | null], - CollectionView[], - { collectionService: vNextCollectionService } ->(COLLECTION_DATA, "decryptedCollections", { - deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)), - derive: async ([collections, orgKeys], { collectionService }) => { - if (collections == null) { - return []; - } - - return await collectionService.decryptMany(collections, orgKeys); - }, -}); diff --git a/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts index d90ae06a75f..57df2d03398 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts @@ -27,7 +27,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService { this.getNudgeStatus$(nudgeType, userId), this.cipherService.cipherListViews$(userId), this.organizationService.organizations$(userId), - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(userId), ]).pipe( switchMap(([nudgeStatus, ciphers, orgs, collections]) => { const vaultHasContents = !(ciphers == null || ciphers.length === 0); diff --git a/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts index df0403ba4ab..2529fc40b73 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts @@ -27,7 +27,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService { this.getNudgeStatus$(nudgeType, userId), this.cipherService.cipherViews$(userId), this.organizationService.organizations$(userId), - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(userId), ]).pipe( switchMap(([nudgeStatus, ciphers, orgs, collections]) => { const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1; diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 0d633be868e..9bc10e5ffc5 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -109,7 +109,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async buildCollections(organizationId?: string): Promise> { - const storedCollections = await this.collectionService.getAllDecrypted(); + const storedCollections = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.collectionService.decryptedCollections$(userId)), + ), + ); const orgs = await this.buildOrganizations(); const defaulCollectionsFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.CreateDefaultLocation, diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts index b5ee6a1fc0f..6d71bad0b0a 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts @@ -143,10 +143,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { ), ); - if (userId == null || userId === currentUserId) { - await this.collectionService.clearActiveUserCache(); - } - await this.searchService.clearIndex(lockingUserId); await this.folderService.clearDecryptedFolderState(lockingUserId); diff --git a/libs/common/src/platform/misc/rxjs-operators.ts b/libs/common/src/platform/misc/rxjs-operators.ts index 689b928cd29..423bcbb790f 100644 --- a/libs/common/src/platform/misc/rxjs-operators.ts +++ b/libs/common/src/platform/misc/rxjs-operators.ts @@ -13,9 +13,9 @@ export const getById = (id: TId) => * @param id The IDs of the objects to return. * @returns An array containing objects with matching IDs, or an empty array if there are no matching objects. */ -export const getByIds = (ids: TId[]) => { - const idSet = new Set(ids); +export const getByIds = (ids: TId[]) => { + const idSet = new Set(ids.filter((id) => id != null)); return map((objects) => { - return objects.filter((o) => idSet.has(o.id)); + return objects.filter((o) => o.id && idSet.has(o.id)); }); }; diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index 08f5aca3a3c..58282a665af 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -14,7 +14,7 @@ export type DecryptedObject< > = Record & Omit; // extracts shared keys from the domain and view types -type EncryptableKeys = (keyof D & +export type EncryptableKeys = (keyof D & ConditionalKeys) & (keyof V & ConditionalKeys); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index a1c3ee35c5c..5fcb39e0356 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -164,9 +164,13 @@ export const SEND_ACCESS_AUTH_MEMORY = new StateDefinition("sendAccessAuth", "me // Vault -export const COLLECTION_DATA = new StateDefinition("collection", "disk", { +export const COLLECTION_DISK = new StateDefinition("collection", "disk", { web: "memory", }); +export const COLLECTION_MEMORY = new StateDefinition("decryptedCollections", "memory", { + browser: "memory-large-object", +}); + export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" }); export const FOLDER_MEMORY = new StateDefinition("decryptedFolders", "memory", { browser: "memory-large-object", diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 63f9ab17fb3..40419a343da 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -172,7 +172,11 @@ export abstract class CoreSyncService implements SyncService { notification.collectionIds != null && notification.collectionIds.length > 0 ) { - const collections = await this.collectionService.getAll(); + const collections = await firstValueFrom( + this.collectionService + .encryptedCollections$(userId) + .pipe(map((collections) => collections ?? [])), + ); if (collections != null) { for (let i = 0; i < collections.length; i++) { if (notification.collectionIds.indexOf(collections[i].id) > -1) { diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index 43e68bfc71f..78fe6f18913 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -119,7 +119,7 @@ describe("CipherAuthorizationService", () => { cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => { expect(result).toBe(false); - expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled(); + expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled(); done(); }); }); @@ -133,7 +133,7 @@ describe("CipherAuthorizationService", () => { cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => { expect(result).toBe(true); - expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled(); + expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled(); done(); }); }); @@ -198,6 +198,7 @@ describe("CipherAuthorizationService", () => { cipherAuthorizationService.canDeleteCipher$(cipher, false).subscribe((result) => { expect(result).toBe(false); + expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled(); done(); }); }); @@ -251,7 +252,7 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", true), createMockCollection("col2", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); @@ -270,7 +271,7 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", false), createMockCollection("col2", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 2933e94c302..06177629de5 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -1,11 +1,11 @@ -import { map, Observable, of, shareReplay, switchMap } from "rxjs"; +import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { getByIds } from "@bitwarden/common/platform/misc"; import { getUserId } from "../../auth/services/account.service"; import { CipherLike } from "../types/cipher-like"; @@ -125,8 +125,11 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.organization$(cipher).pipe( - switchMap((organization) => { + return combineLatest([ + this.organization$(cipher), + this.accountService.activeAccount$.pipe(getUserId), + ]).pipe( + switchMap(([organization, userId]) => { // Admins and custom users can always clone when in the Admin Console if ( isAdminConsoleAction && @@ -136,9 +139,10 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.collectionService - .decryptedCollectionViews$(cipher.collectionIds as CollectionId[]) - .pipe(map((allCollections) => allCollections.some((collection) => collection.manage))); + return this.collectionService.decryptedCollections$(userId).pipe( + getByIds(cipher.collectionIds), + map((allCollections) => allCollections.some((collection) => collection.manage)), + ); }), shareReplay({ bufferSize: 1, refCount: false }), ); diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 7bac6b0e0a5..63b35e979bd 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -300,7 +300,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { // Retrieve all organizations a user is a member of and has collections they can manage const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.organizations$ = this.organizationService.memberOrganizations$(userId).pipe( - combineLatestWith(this.collectionService.decryptedCollections$), + combineLatestWith(this.collectionService.decryptedCollections$(userId)), map(([organizations, collections]) => organizations .filter((org) => collections.some((c) => c.organizationId === org.id && c.manage)) @@ -318,15 +318,15 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { } if (value) { - this.collections$ = Utils.asyncToObservable(() => - this.collectionService - .getAllDecrypted() - .then((decryptedCollections) => + this.collections$ = this.collectionService + .decryptedCollections$(userId) + .pipe( + map((decryptedCollections) => decryptedCollections .filter((c2) => c2.organizationId === value && c2.manage) .sort(Utils.getSortFunction(this.i18nService, "name")), ), - ); + ); } }); this.formGroup.controls.vaultSelector.setValue("myVault"); diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index c6bff607633..5be597c0591 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -406,7 +406,7 @@ export class ImportService implements ImportServiceAbstraction { if (importResult.collections != null) { for (let i = 0; i < importResult.collections.length; i++) { importResult.collections[i].organizationId = organizationId; - const c = await this.collectionService.encrypt(importResult.collections[i]); + const c = await this.collectionService.encrypt(importResult.collections[i], activeUserId); request.collections.push(new CollectionWithIdRequest(c)); } } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 61fbcd261f4..8a518cb1304 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import * as papa from "papaparse"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { CollectionService, @@ -225,15 +225,8 @@ export class OrganizationVaultExportService ): Promise { let decCiphers: CipherView[] = []; let allDecCiphers: CipherView[] = []; - let decCollections: CollectionView[] = []; const promises = []; - promises.push( - this.collectionService.getAllDecrypted().then(async (collections) => { - decCollections = collections.filter((c) => c.organizationId == organizationId && c.manage); - }), - ); - promises.push( this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => { allDecCiphers = ciphers; @@ -241,6 +234,16 @@ export class OrganizationVaultExportService ); await Promise.all(promises); + const decCollections: CollectionView[] = await firstValueFrom( + this.collectionService + .decryptedCollections$(activeUserId) + .pipe( + map((collections) => + collections.filter((c) => c.organizationId == organizationId && c.manage), + ), + ), + ); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); decCiphers = allDecCiphers.filter( @@ -263,15 +266,8 @@ export class OrganizationVaultExportService ): Promise { let encCiphers: Cipher[] = []; let allCiphers: Cipher[] = []; - let encCollections: Collection[] = []; const promises = []; - promises.push( - this.collectionService.getAll().then((collections) => { - encCollections = collections.filter((c) => c.organizationId == organizationId && c.manage); - }), - ); - promises.push( this.cipherService.getAll(activeUserId).then((ciphers) => { allCiphers = ciphers; @@ -280,6 +276,15 @@ export class OrganizationVaultExportService await Promise.all(promises); + const encCollections: Collection[] = await firstValueFrom( + this.collectionService.encryptedCollections$(activeUserId).pipe( + map((collections) => collections ?? []), + map((collections) => + collections.filter((c) => c.organizationId == organizationId && c.manage), + ), + ), + ); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); encCiphers = allCiphers.filter( diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 6af6d5121fb..0b5d3d70834 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -272,25 +272,29 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { return; } - this.organizations$ = combineLatest({ - collections: this.collectionService.decryptedCollections$, - memberOrganizations: this.accountService.activeAccount$.pipe( + this.organizations$ = this.accountService.activeAccount$ + .pipe( getUserId, - switchMap((userId) => this.organizationService.memberOrganizations$(userId)), - ), - }).pipe( - map(({ collections, memberOrganizations }) => { - const managedCollectionsOrgIds = new Set( - collections.filter((c) => c.manage).map((c) => c.organizationId), - ); - // Filter organizations that exist in managedCollectionsOrgIds - const filteredOrgs = memberOrganizations.filter((org) => - managedCollectionsOrgIds.has(org.id), - ); - // Sort the filtered organizations based on the name - return filteredOrgs.sort(Utils.getSortFunction(this.i18nService, "name")); - }), - ); + switchMap((userId) => + combineLatest({ + collections: this.collectionService.decryptedCollections$(userId), + memberOrganizations: this.organizationService.memberOrganizations$(userId), + }), + ), + ) + .pipe( + map(({ collections, memberOrganizations }) => { + const managedCollectionsOrgIds = new Set( + collections.filter((c) => c.manage).map((c) => c.organizationId), + ); + // Filter organizations that exist in managedCollectionsOrgIds + const filteredOrgs = memberOrganizations.filter((org) => + managedCollectionsOrgIds.has(org.id), + ); + // Sort the filtered organizations based on the name + return filteredOrgs.sort(Utils.getSortFunction(this.i18nService, "name")); + }), + ); combineLatest([ this.disablePersonalVaultExportPolicy$, diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index c47e5842987..04a2bb957ec 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -48,9 +48,10 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { await firstValueFrom( combineLatest([ this.organizations$(activeUserId), - this.collectionService.encryptedCollections$.pipe( + this.collectionService.encryptedCollections$(activeUserId).pipe( + map((collections) => collections ?? []), switchMap((c) => - this.collectionService.decryptedCollections$.pipe( + this.collectionService.decryptedCollections$(activeUserId).pipe( filter((d) => d.length === c.length), // Ensure all collections have been decrypted ), ), diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 66910ad8ac7..4f54e5d393a 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -16,7 +16,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherId, CollectionId, EmergencyAccessId, UserId } from "@bitwarden/common/types/guid"; +import { getByIds } from "@bitwarden/common/platform/misc"; +import { CipherId, EmergencyAccessId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -143,6 +144,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy { return; } + const userId = await firstValueFrom(this.activeUserId$); + // Load collections if not provided and the cipher has collectionIds if ( this.cipher.collectionIds && @@ -150,14 +153,12 @@ export class CipherViewComponent implements OnChanges, OnDestroy { (!this.collections || this.collections.length === 0) ) { this.collections = await firstValueFrom( - this.collectionService.decryptedCollectionViews$( - this.cipher.collectionIds as CollectionId[], - ), + this.collectionService + .decryptedCollections$(userId) + .pipe(getByIds(this.cipher.collectionIds)), ); } - const userId = await firstValueFrom(this.activeUserId$); - if (this.cipher.organizationId) { this.organization$ = this.organizationService .organizations$(userId) diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 124dc783034..b2bd6e31ee5 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -435,12 +435,14 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI * @returns An observable of the collections for the organization. */ private getCollectionsForOrganization(orgId: OrganizationId): Observable { - return combineLatest([ - this.collectionService.decryptedCollections$, - this.accountService.activeAccount$.pipe( - switchMap((account) => this.organizationService.organizations$(account?.id)), + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + combineLatest([ + this.collectionService.decryptedCollections$(userId), + this.organizationService.organizations$(userId), + ]), ), - ]).pipe( map(([collections, organizations]) => { const org = organizations.find((o) => o.id === orgId); this.orgName = org.name; From df8e0ed094ca46c4644777b57b5d8762929410c9 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 24 Jul 2025 08:53:03 -0500 Subject: [PATCH 040/179] [PM-23825] setup crowdstrike card (#15728) --- .../integrations/integrations.component.ts | 34 ++++++++++++-- .../integration-card.component.html | 46 ++++++++++++++----- .../integration-card.component.spec.ts | 30 +++++++++++- .../integration-card.component.ts | 13 ++++++ .../integration-grid.component.html | 3 ++ .../shared/components/integrations/models.ts | 3 ++ .../integrations/logo-crowdstrike-black.svg | 22 +++++++++ apps/web/src/locales/en/messages.json | 12 +++++ libs/common/src/enums/feature-flag.enum.ts | 6 +++ 9 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/images/integrations/logo-crowdstrike-black.svg diff --git a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts index e6a62b1db73..c0a57c82954 100644 --- a/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts +++ b/apps/web/src/app/admin-console/organizations/integrations/integrations.component.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Observable, switchMap } from "rxjs"; +import { Observable, Subject, switchMap, takeUntil } from "rxjs"; import { getOrganizationById, @@ -11,6 +11,8 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { IntegrationType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared/shared.module"; @@ -30,10 +32,12 @@ import { Integration } from "../shared/components/integrations/models"; FilterIntegrationsPipe, ], }) -export class AdminConsoleIntegrationsComponent implements OnInit { +export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { integrationsList: Integration[] = []; tabIndex: number; organization$: Observable; + isEventBasedIntegrationsEnabled: boolean = false; + private destroy$ = new Subject(); ngOnInit(): void { this.organization$ = this.route.params.pipe( @@ -53,7 +57,15 @@ export class AdminConsoleIntegrationsComponent implements OnInit { private route: ActivatedRoute, private organizationService: OrganizationService, private accountService: AccountService, + private configService: ConfigService, ) { + this.configService + .getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations) + .pipe(takeUntil(this.destroy$)) + .subscribe((isEnabled) => { + this.isEventBasedIntegrationsEnabled = isEnabled; + }); + this.integrationsList = [ { name: "AD FS", @@ -229,6 +241,22 @@ export class AdminConsoleIntegrationsComponent implements OnInit { type: IntegrationType.DEVICE, }, ]; + + if (this.isEventBasedIntegrationsEnabled) { + this.integrationsList.push({ + name: "Crowdstrike", + linkURL: "", + image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg", + type: IntegrationType.EVENT, + description: "crowdstrikeEventIntegrationDesc", + isConnected: false, + canSetupConnection: true, + }); + } + } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } get IntegrationType(): typeof IntegrationType { diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html index e96fbef270c..2c0db1cf933 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.html @@ -17,16 +17,40 @@
-

{{ name }}

- - - - {{ "new" | i18n }} - +

+ {{ name }} + @if (showConnectedBadge()) { + + @if (isConnected) { + {{ "on" | i18n }} + } + @if (!isConnected) { + {{ "off" | i18n }} + } + + } +

+

{{ description }}

+ + @if (canSetupConnection) { + + } + + @if (linkURL) { + + + } + @if (showNewBadge()) { + + {{ "new" | i18n }} + + }
diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts index ec057f25176..16b7eb142e8 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.spec.ts @@ -16,6 +16,7 @@ import { IntegrationCardComponent } from "./integration-card.component"; describe("IntegrationCardComponent", () => { let component: IntegrationCardComponent; let fixture: ComponentFixture; + const mockI18nService = mock(); const systemTheme$ = new BehaviorSubject(ThemeType.Light); const usersPreferenceTheme$ = new BehaviorSubject(ThemeType.Light); @@ -41,7 +42,7 @@ describe("IntegrationCardComponent", () => { }, { provide: I18nService, - useValue: mock(), + useValue: mockI18nService, }, ], }).compileComponents(); @@ -55,6 +56,7 @@ describe("IntegrationCardComponent", () => { component.image = "test-image.png"; component.linkURL = "https://example.com/"; + mockI18nService.t.mockImplementation((key) => key); fixture.detectChanges(); }); @@ -67,7 +69,7 @@ describe("IntegrationCardComponent", () => { it("renders card body", () => { const name = fixture.nativeElement.querySelector("h3"); - expect(name.textContent).toBe("Integration Name"); + expect(name.textContent).toContain("Integration Name"); }); it("assigns external rel attribute", () => { @@ -182,4 +184,28 @@ describe("IntegrationCardComponent", () => { }); }); }); + + describe("connected badge", () => { + it("shows connected badge when isConnected is true", () => { + component.isConnected = true; + + expect(component.showConnectedBadge()).toBe(true); + }); + + it("does not show connected badge when isConnected is false", () => { + component.isConnected = false; + fixture.detectChanges(); + const name = fixture.nativeElement.querySelector("h3 > span > span > span"); + + expect(name.textContent).toContain("off"); + // when isConnected is true/false, the badge should be shown as on/off + // when isConnected is undefined, the badge should not be shown + expect(component.showConnectedBadge()).toBe(true); + }); + + it("does not show connected badge when isConnected is undefined", () => { + component.isConnected = undefined; + expect(component.showConnectedBadge()).toBe(false); + }); + }); }); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts index 20e4028e9df..4188579bef9 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component.ts @@ -41,6 +41,9 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { * @example "2024-12-31" */ @Input() newBadgeExpiration?: string; + @Input() description?: string; + @Input() isConnected?: boolean; + @Input() canSetupConnection?: boolean; constructor( private themeStateService: ThemeStateService, @@ -93,4 +96,14 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { return expirationDate > new Date(); } + + showConnectedBadge(): boolean { + return this.isConnected !== undefined; + } + + setupConnection(app: string) { + // This method can be used to handle the connection logic for the integration + // For example, it could open a modal or redirect to a setup page + this.isConnected = !this.isConnected; // Toggle connection state for demonstration + } } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html index 4b4b3ac972b..b4eaff993f0 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component.html @@ -13,6 +13,9 @@ [imageDarkMode]="integration.imageDarkMode" [externalURL]="integration.type === IntegrationType.SDK" [newBadgeExpiration]="integration.newBadgeExpiration" + [description]="integration.description | i18n" + [isConnected]="integration.isConnected" + [canSetupConnection]="integration.canSetupConnection" > diff --git a/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts b/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts index 765b1d44a2e..a231523b578 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/integrations/models.ts @@ -17,4 +17,7 @@ export type Integration = { * @example "2024-12-31" */ newBadgeExpiration?: string; + description?: string; + isConnected?: boolean; + canSetupConnection?: boolean; }; diff --git a/apps/web/src/images/integrations/logo-crowdstrike-black.svg b/apps/web/src/images/integrations/logo-crowdstrike-black.svg new file mode 100644 index 00000000000..25875d705cb --- /dev/null +++ b/apps/web/src/images/integrations/logo-crowdstrike-black.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b143d4da56a..f34d6e41b37 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9478,6 +9478,9 @@ "deviceManagementDesc": { "message": "Configure device management for Bitwarden using the implementation guide for your platform." }, + "crowdstrikeEventIntegrationDesc": { + "message": "Send event data to your Logscale instance" + }, "deviceIdMissing": { "message": "Device ID is missing" }, @@ -9493,6 +9496,15 @@ "reopenLinkOnDesktop": { "message": "Reopen this link from your email on a desktop." }, + "connectIntegrationButtonDesc": { + "message": "Connect $INTEGRATION$", + "placeholders": { + "integration": { + "content": "$1", + "example": "Crowdstrike" + } + } + }, "integrationCardTooltip": { "message": "Launch $INTEGRATION$ implementation guide.", "placeholders": { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 114ace8bd8e..33ded4a22c8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -46,6 +46,9 @@ export enum FeatureFlag { /* Tools */ DesktopSendUIRefresh = "desktop-send-ui-refresh", + /* DIRT */ + EventBasedOrganizationIntegrations = "event-based-organization-integrations", + /* Vault */ PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", @@ -89,6 +92,9 @@ export const DefaultFeatureFlagValue = { /* Tools */ [FeatureFlag.DesktopSendUIRefresh]: FALSE, + /* DIRT */ + [FeatureFlag.EventBasedOrganizationIntegrations]: FALSE, + /* Vault */ [FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE, [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, From b3db1b79cea7c99ba610866b86fc7188a1cf1150 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:46:18 -0400 Subject: [PATCH 041/179] chore(feature flags): [PM-19034] Remove feature flags and old components for Set/Change Password * Removed flag and components. * More cleanup * Removed ChangePasswordComponent. * Removed old EmergencyAccessTakeover * Removed service initialization. * Fixed test failures. * Fixed tests. * Test changes. * Updated comments * Fixed tests. * Fixed tests. * Fixed merge conflict. * Removed style and routing references. * Better comments. * Removed ResetPasswordComponent --- .../auth/popup/set-password.component.html | 160 ---------- .../src/auth/popup/set-password.component.ts | 10 - .../popup/update-temp-password.component.html | 142 --------- .../popup/update-temp-password.component.ts | 30 -- apps/browser/src/popup/app-routing.module.ts | 50 +-- apps/browser/src/popup/app.module.ts | 4 - apps/desktop/src/app/app-routing.module.ts | 45 +-- apps/desktop/src/app/app.module.ts | 4 - .../desktop-set-password-jit.service.ts | 21 -- .../src/app/services/services.module.ts | 17 - .../src/auth/set-password.component.html | 169 ---------- .../src/auth/set-password.component.ts | 111 ------- .../auth/update-temp-password.component.html | 136 -------- .../auth/update-temp-password.component.ts | 10 - apps/desktop/src/scss/pages.scss | 94 ------ .../components/reset-password.component.html | 67 ---- .../components/reset-password.component.ts | 223 ------------- .../members/members.component.ts | 48 +-- .../organizations/members/members.module.ts | 2 - apps/web/src/app/auth/core/services/index.ts | 1 - .../login/web-login-component.service.spec.ts | 5 +- .../login/web-login-component.service.ts | 23 +- .../core/services/set-password-jit/index.ts | 1 - .../web-set-password-jit.service.ts | 27 -- .../src/app/auth/set-password.component.html | 130 -------- .../src/app/auth/set-password.component.ts | 30 -- .../settings/change-password.component.html | 129 -------- .../settings/change-password.component.ts | 258 --------------- .../emergency-access.component.ts | 65 +--- .../emergency-access-takeover.component.html | 54 ---- .../emergency-access-takeover.component.ts | 145 --------- .../security/security-routing.module.ts | 23 -- .../settings/security/security.component.ts | 16 +- .../src/app/auth/settings/settings.module.ts | 5 +- .../app/auth/update-password.component.html | 90 ------ .../src/app/auth/update-password.component.ts | 24 -- .../auth/update-temp-password.component.html | 96 ------ .../auth/update-temp-password.component.ts | 10 - apps/web/src/app/core/core.module.ts | 17 - apps/web/src/app/oss-routing.module.ts | 58 +--- .../src/app/shared/loose-components.module.ts | 12 - .../components/change-password.component.ts | 232 -------------- .../auth/components/set-password.component.ts | 300 ------------------ .../components/update-password.component.ts | 141 -------- .../update-temp-password.component.ts | 232 -------------- .../src/auth/guards/auth.guard.spec.ts | 161 +++------- libs/angular/src/auth/guards/auth.guard.ts | 46 +-- .../src/services/jslib-services.module.ts | 17 - libs/auth/src/angular/index.ts | 5 - .../auth/src/angular/login/login.component.ts | 47 +-- .../new-device-verification.component.ts | 27 +- .../default-set-password-jit.service.spec.ts | 241 -------------- .../default-set-password-jit.service.ts | 176 ---------- .../set-password-jit.component.html | 24 -- .../set-password-jit.component.ts | 135 -------- .../set-password-jit.service.abstraction.ts | 33 -- libs/auth/src/angular/sso/sso.component.ts | 9 +- .../two-factor-auth.component.spec.ts | 92 ++---- .../two-factor-auth.component.ts | 31 +- .../password-login.strategy.spec.ts | 35 +- .../password-login.strategy.ts | 40 +-- .../sso-login.strategy.spec.ts | 59 ++-- .../login-strategies/sso-login.strategy.ts | 82 ++--- .../services/policy/default-policy.service.ts | 3 +- libs/common/src/enums/feature-flag.enum.ts | 4 - 65 files changed, 247 insertions(+), 4487 deletions(-) delete mode 100644 apps/browser/src/auth/popup/set-password.component.html delete mode 100644 apps/browser/src/auth/popup/set-password.component.ts delete mode 100644 apps/browser/src/auth/popup/update-temp-password.component.html delete mode 100644 apps/browser/src/auth/popup/update-temp-password.component.ts delete mode 100644 apps/desktop/src/app/services/desktop-set-password-jit.service.ts delete mode 100644 apps/desktop/src/auth/set-password.component.html delete mode 100644 apps/desktop/src/auth/set-password.component.ts delete mode 100644 apps/desktop/src/auth/update-temp-password.component.html delete mode 100644 apps/desktop/src/auth/update-temp-password.component.ts delete mode 100644 apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html delete mode 100644 apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts delete mode 100644 apps/web/src/app/auth/core/services/set-password-jit/index.ts delete mode 100644 apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts delete mode 100644 apps/web/src/app/auth/set-password.component.html delete mode 100644 apps/web/src/app/auth/set-password.component.ts delete mode 100644 apps/web/src/app/auth/settings/change-password.component.html delete mode 100644 apps/web/src/app/auth/settings/change-password.component.ts delete mode 100644 apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.html delete mode 100644 apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts delete mode 100644 apps/web/src/app/auth/update-password.component.html delete mode 100644 apps/web/src/app/auth/update-password.component.ts delete mode 100644 apps/web/src/app/auth/update-temp-password.component.html delete mode 100644 apps/web/src/app/auth/update-temp-password.component.ts delete mode 100644 libs/angular/src/auth/components/change-password.component.ts delete mode 100644 libs/angular/src/auth/components/set-password.component.ts delete mode 100644 libs/angular/src/auth/components/update-password.component.ts delete mode 100644 libs/angular/src/auth/components/update-temp-password.component.ts delete mode 100644 libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts delete mode 100644 libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts delete mode 100644 libs/auth/src/angular/set-password-jit/set-password-jit.component.html delete mode 100644 libs/auth/src/angular/set-password-jit/set-password-jit.component.ts delete mode 100644 libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts diff --git a/apps/browser/src/auth/popup/set-password.component.html b/apps/browser/src/auth/popup/set-password.component.html deleted file mode 100644 index 71a2e3ac588..00000000000 --- a/apps/browser/src/auth/popup/set-password.component.html +++ /dev/null @@ -1,160 +0,0 @@ - -
-
- -
-

- {{ "setMasterPassword" | i18n }} -

-
- -
-
-
-
- -
-
-
-

- {{ "orgPermissionsUpdatedMustSetPassword" | i18n }} -

- - -

{{ "orgRequiresYouToSetPassword" | i18n }}

-
- - - {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} - - - -
-
-
-
-
-
- - -
-
- -
-
- - - -
-
- -
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
-
-
- - -
-
- -
-
-
- diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts deleted file mode 100644 index 2a796854531..00000000000 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from "@angular/core"; - -import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; - -@Component({ - selector: "app-set-password", - templateUrl: "set-password.component.html", - standalone: false, -}) -export class SetPasswordComponent extends BaseSetPasswordComponent {} diff --git a/apps/browser/src/auth/popup/update-temp-password.component.html b/apps/browser/src/auth/popup/update-temp-password.component.html deleted file mode 100644 index 0ce82aa20cf..00000000000 --- a/apps/browser/src/auth/popup/update-temp-password.component.html +++ /dev/null @@ -1,142 +0,0 @@ -
-
-
- -
-

- {{ "updateMasterPassword" | i18n }} -

-
- -
-
-
- - {{ masterPasswordWarningText }} - - - -
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
- - -
-
- -
-
- - -
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
-
- - -
-
- -
-
-
diff --git a/apps/browser/src/auth/popup/update-temp-password.component.ts b/apps/browser/src/auth/popup/update-temp-password.component.ts deleted file mode 100644 index e8cf64b7548..00000000000 --- a/apps/browser/src/auth/popup/update-temp-password.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component"; - -import { postLogoutMessageListener$ } from "./utils/post-logout-message-listener"; - -@Component({ - selector: "app-update-temp-password", - templateUrl: "update-temp-password.component.html", - standalone: false, -}) -export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { - onSuccessfulChangePassword: () => Promise = this.doOnSuccessfulChangePassword.bind(this); - - private async doOnSuccessfulChangePassword() { - // start listening for "switchAccountFinish" or "doneLoggingOut" - const messagePromise = firstValueFrom(postLogoutMessageListener$); - this.messagingService.send("logout"); - // wait for messages - const command = await messagePromise; - - // doneLoggingOut already has a message handler that will navigate us - if (command === "switchAccountFinish") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/"]); - } - } -} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 52a60d9c23d..f01809433e3 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -32,7 +32,6 @@ import { RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, RegistrationUserAddIcon, - SetPasswordJitComponent, SsoComponent, TwoFactorTimeoutIcon, TwoFactorAuthComponent, @@ -43,15 +42,13 @@ import { VaultIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; +import { AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard"; -import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { ExtensionDeviceManagementComponent } from "../auth/popup/settings/extension-device-management.component"; -import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component"; @@ -180,11 +177,6 @@ const routes: Routes = [ elevation: 1, } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, }, - { - path: "set-password", - component: SetPasswordComponent, - data: { elevation: 1 } satisfies RouteDataProperties, - }, { path: "remove-password", component: RemovePasswordComponent, @@ -337,20 +329,6 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - { - path: "update-temp-password", - component: UpdateTempPasswordComponent, - canActivate: [ - canAccessFeature( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - false, - `/change-password`, - false, - ), - authGuard, - ], - data: { elevation: 1 } satisfies RouteDataProperties, - }, { path: "", component: ExtensionAnonLayoutWrapperComponent, @@ -398,7 +376,7 @@ const routes: Routes = [ }, { path: "set-initial-password", - canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard], + canActivate: [authGuard], component: SetInitialPasswordComponent, data: { elevation: 1, @@ -586,29 +564,7 @@ const routes: Routes = [ component: ChangePasswordComponent, }, ], - canActivate: [ - canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor), - authGuard, - ], - }, - ], - }, - { - path: "", - component: AnonLayoutWrapperComponent, - children: [ - { - path: "set-password-jit", - component: SetPasswordJitComponent, - data: { - pageTitle: { - key: "joinOrganization", - }, - pageSubtitle: { - key: "finishJoiningThisOrganizationBySettingAMasterPassword", - }, - elevation: 1, - } satisfies RouteDataProperties & AnonLayoutWrapperData, + canActivate: [authGuard], }, ], }, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 77c87838ff7..687ae67d43c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -26,10 +26,8 @@ import { import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; -import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; -import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; @@ -96,9 +94,7 @@ import "../platform/popup/locales"; AppComponent, ColorPasswordPipe, ColorPasswordCountPipe, - SetPasswordComponent, TabsV2Component, - UpdateTempPasswordComponent, UserVerificationComponent, VaultTimeoutInputComponent, RemovePasswordComponent, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index db3e69f7d6f..30ae906c98d 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -15,8 +15,6 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; -import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { LoginComponent, LoginSecondaryContentComponent, @@ -28,7 +26,6 @@ import { RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, RegistrationUserAddIcon, - SetPasswordJitComponent, UserLockIcon, VaultIcon, LoginDecryptionOptionsComponent, @@ -40,13 +37,10 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; -import { SetPasswordComponent } from "../auth/set-password.component"; -import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; @@ -105,25 +99,11 @@ const routes: Routes = [ component: VaultV2Component, canActivate: [authGuard], }, - { path: "set-password", component: SetPasswordComponent }, { path: "send", component: SendComponent, canActivate: [authGuard], }, - { - path: "update-temp-password", - component: UpdateTempPasswordComponent, - canActivate: [ - canAccessFeature( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - false, - `/change-password`, - false, - ), - authGuard, - ], - }, { path: "remove-password", component: RemovePasswordComponent, @@ -308,26 +288,6 @@ const routes: Routes = [ }, ], }, - { - path: "set-password-jit", - component: SetPasswordJitComponent, - data: { - pageTitle: { - key: "joinOrganization", - }, - pageSubtitle: { - key: "finishJoiningThisOrganizationBySettingAMasterPassword", - }, - } satisfies AnonLayoutWrapperData, - }, - { - path: "set-initial-password", - canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard], - component: SetInitialPasswordComponent, - data: { - maxWidth: "lg", - } satisfies AnonLayoutWrapperData, - }, { path: "2fa", canActivate: [unauthGuardFn(), TwoFactorAuthGuard], @@ -346,10 +306,7 @@ const routes: Routes = [ { path: "change-password", component: ChangePasswordComponent, - canActivate: [ - canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor), - authGuard, - ], + canActivate: [authGuard], }, ], }, diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 112732d8f2c..79c1aae0c3b 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -13,8 +13,6 @@ import { AssignCollectionsComponent } from "@bitwarden/vault"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginModule } from "../auth/login/login.module"; -import { SetPasswordComponent } from "../auth/set-password.component"; -import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { SshAgentService } from "../autofill/services/ssh-agent.service"; import { PremiumComponent } from "../billing/app/accounts/premium.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; @@ -57,9 +55,7 @@ import { SharedModule } from "./shared/shared.module"; PremiumComponent, RemovePasswordComponent, SearchComponent, - SetPasswordComponent, SettingsComponent, - UpdateTempPasswordComponent, VaultTimeoutInputComponent, ], providers: [SshAgentService], diff --git a/apps/desktop/src/app/services/desktop-set-password-jit.service.ts b/apps/desktop/src/app/services/desktop-set-password-jit.service.ts deleted file mode 100644 index f6ea3d0ce84..00000000000 --- a/apps/desktop/src/app/services/desktop-set-password-jit.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { inject } from "@angular/core"; - -import { - DefaultSetPasswordJitService, - SetPasswordCredentials, - SetPasswordJitService, -} from "@bitwarden/auth/angular"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export class DesktopSetPasswordJitService - extends DefaultSetPasswordJitService - implements SetPasswordJitService -{ - messagingService = inject(MessagingService); - - override async setPassword(credentials: SetPasswordCredentials) { - await super.setPassword(credentials); - - this.messagingService.send("redrawMenu"); - } -} diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 04c989c4f36..c3b88077dbb 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -25,7 +25,6 @@ import { import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { LoginComponentService, - SetPasswordJitService, SsoComponentService, DefaultSsoComponentService, TwoFactorAuthDuoComponentService, @@ -139,7 +138,6 @@ import { NativeMessagingService } from "../../services/native-messaging.service" import { SearchBarService } from "../layout/search/search-bar.service"; import { DesktopFileDownloadService } from "./desktop-file-download.service"; -import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service"; import { InitService } from "./init.service"; import { NativeMessagingManifestService } from "./native-messaging-manifest.service"; import { DesktopSetInitialPasswordService } from "./set-initial-password/desktop-set-initial-password.service"; @@ -379,21 +377,6 @@ const safeProviders: SafeProvider[] = [ provide: CLIENT_TYPE, useValue: ClientType.Desktop, }), - safeProvider({ - provide: SetPasswordJitService, - useClass: DesktopSetPasswordJitService, - deps: [ - EncryptService, - I18nServiceAbstraction, - KdfConfigService, - KeyService, - MasterPasswordApiService, - InternalMasterPasswordServiceAbstraction, - OrganizationApiServiceAbstraction, - OrganizationUserApiService, - InternalUserDecryptionOptionsServiceAbstraction, - ], - }), safeProvider({ provide: SetInitialPasswordService, useClass: DesktopSetInitialPasswordService, diff --git a/apps/desktop/src/auth/set-password.component.html b/apps/desktop/src/auth/set-password.component.html deleted file mode 100644 index 46d954327f8..00000000000 --- a/apps/desktop/src/auth/set-password.component.html +++ /dev/null @@ -1,169 +0,0 @@ -
-
- Bitwarden -

{{ "setMasterPassword" | i18n }}

-
- - {{ "loading" | i18n }} -
-
-
-

- {{ "orgPermissionsUpdatedMustSetPassword" | i18n }} -

- - -

{{ "orgRequiresYouToSetPassword" | i18n }}

-
- - - {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} - - - -
- -
-
-
-
-
- - -
-
- -
-
- - -
-
- -
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
-
-
- - -
-
- -
-
- - -
- -
-
- diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts deleted file mode 100644 index d45fc111a97..00000000000 --- a/apps/desktop/src/auth/set-password.component.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; - -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; -import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; - -const BroadcasterSubscriptionId = "SetPasswordComponent"; - -@Component({ - selector: "app-set-password", - templateUrl: "set-password.component.html", - standalone: false, -}) -export class SetPasswordComponent extends BaseSetPasswordComponent implements OnInit, OnDestroy { - constructor( - protected accountService: AccountService, - protected dialogService: DialogService, - protected encryptService: EncryptService, - protected i18nService: I18nService, - protected kdfConfigService: KdfConfigService, - protected keyService: KeyService, - protected masterPasswordApiService: MasterPasswordApiService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected messagingService: MessagingService, - protected organizationApiService: OrganizationApiServiceAbstraction, - protected organizationUserApiService: OrganizationUserApiService, - protected platformUtilsService: PlatformUtilsService, - protected policyApiService: PolicyApiServiceAbstraction, - protected policyService: PolicyService, - protected route: ActivatedRoute, - protected router: Router, - protected ssoLoginService: SsoLoginServiceAbstraction, - protected syncService: SyncService, - protected toastService: ToastService, - protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - ) { - super( - accountService, - dialogService, - encryptService, - i18nService, - kdfConfigService, - keyService, - masterPasswordApiService, - masterPasswordService, - messagingService, - organizationApiService, - organizationUserApiService, - platformUtilsService, - policyApiService, - policyService, - route, - router, - ssoLoginService, - syncService, - toastService, - userDecryptionOptionsService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message) => { - this.ngZone.run(() => { - switch (message.command) { - case "windowHidden": - this.onWindowHidden(); - break; - default: - } - }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - onWindowHidden() { - this.showPassword = false; - } - - protected async onSetPasswordSuccess( - masterKey: MasterKey, - userKey: [UserKey, EncString], - keyPair: [string, EncString], - ): Promise { - await super.onSetPasswordSuccess(masterKey, userKey, keyPair); - this.messagingService.send("redrawMenu"); - } -} diff --git a/apps/desktop/src/auth/update-temp-password.component.html b/apps/desktop/src/auth/update-temp-password.component.html deleted file mode 100644 index 11b8ad4361b..00000000000 --- a/apps/desktop/src/auth/update-temp-password.component.html +++ /dev/null @@ -1,136 +0,0 @@ -
-
- - {{ masterPasswordWarningText }} - - - -
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
- - -
-
- -
-
- - -
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
-
- - -
-
- -
-
- - -
-
-
diff --git a/apps/desktop/src/auth/update-temp-password.component.ts b/apps/desktop/src/auth/update-temp-password.component.ts deleted file mode 100644 index ead10660b92..00000000000 --- a/apps/desktop/src/auth/update-temp-password.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from "@angular/core"; - -import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component"; - -@Component({ - selector: "app-update-temp-password", - templateUrl: "update-temp-password.component.html", - standalone: false, -}) -export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {} diff --git a/apps/desktop/src/scss/pages.scss b/apps/desktop/src/scss/pages.scss index 4098ad860dd..73ef7554f6a 100644 --- a/apps/desktop/src/scss/pages.scss +++ b/apps/desktop/src/scss/pages.scss @@ -1,7 +1,5 @@ @import "variables.scss"; -#lock-page, -#set-password-page, #remove-password-page { display: flex; justify-content: center; @@ -23,9 +21,6 @@ } } -#register-page, -#hint-page, -#update-temp-password-page, #remove-password-page { padding-top: 20px; @@ -42,68 +37,6 @@ } } -#register-page, -#hint-page, -#lock-page, -#update-temp-password-page { - .content { - width: 325px; - transition: width 0.25s linear; - - p { - text-align: center; - } - - p.lead, - h1 { - font-size: $font-size-large; - text-align: center; - margin-bottom: 20px; - font-weight: normal; - } - - .box { - margin-bottom: 20px; - } - - .buttons { - &:not(.with-rows), - .buttons-row { - display: flex; - margin-bottom: 10px; - } - - &:not(.with-rows), - .buttons-row:last-child { - margin-bottom: 20px; - } - - button { - margin-right: 10px; - - &:last-child { - margin-right: 0; - } - } - } - - .sub-options { - text-align: center; - margin-bottom: 20px; - - a { - display: block; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - } - } - } -} - -#set-password-page, #remove-password-page { .content { width: 500px; @@ -155,35 +88,8 @@ } } -#register-page, -#update-temp-password-page { - .content { - width: 400px; - } -} - #remove-password-page { .content > p { margin-bottom: 20px; } } - -#login-approval-page { - .section-title { - padding: 20px; - } - .content { - padding: 16px; - .section { - margin-bottom: 30px; - code { - @include themify($themes) { - color: themed("codeColor"); - } - } - h4.label { - font-weight: bold; - } - } - } -} diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html deleted file mode 100644 index b5f50841e13..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html +++ /dev/null @@ -1,67 +0,0 @@ -
- - - {{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }} - - - - - - {{ "newPassword" | i18n }} - - - - - - - - - - - - - - -
diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts deleted file mode 100644 index 961d5482d8a..00000000000 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ /dev/null @@ -1,223 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, switchMap, takeUntil } from "rxjs"; - -import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { OrganizationId } from "@bitwarden/common/types/guid"; -import { - DIALOG_DATA, - DialogConfig, - DialogRef, - DialogService, - ToastService, -} from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; - -import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service"; - -/** - * Encapsulates a few key data inputs needed to initiate an account recovery - * process for the organization user in question. - */ -export type ResetPasswordDialogData = { - /** - * The organization user's full name - */ - name: string; - - /** - * The organization user's email address - */ - email: string; - - /** - * The `organizationUserId` for the user - */ - id: string; - - /** - * The organization's `organizationId` - */ - organizationId: OrganizationId; -}; - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum ResetPasswordDialogResult { - Ok = "ok", -} - -/** - * Used in a dialog for initiating the account recovery process against a - * given organization user. An admin will access this form when they want to - * reset a user's password and log them out of sessions. - * - * @deprecated Use the `AccountRecoveryDialogComponent` instead. - */ -@Component({ - selector: "app-reset-password", - templateUrl: "reset-password.component.html", - standalone: false, -}) -export class ResetPasswordComponent implements OnInit, OnDestroy { - formGroup = this.formBuilder.group({ - newPassword: ["", Validators.required], - }); - - @ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component; - - enforcedPolicyOptions: MasterPasswordPolicyOptions; - showPassword = false; - passwordStrengthScore: number; - - private destroy$ = new Subject(); - - constructor( - @Inject(DIALOG_DATA) protected data: ResetPasswordDialogData, - private resetPasswordService: OrganizationUserResetPasswordService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private passwordGenerationService: PasswordGenerationServiceAbstraction, - private policyService: PolicyService, - private logService: LogService, - private dialogService: DialogService, - private toastService: ToastService, - private formBuilder: FormBuilder, - private dialogRef: DialogRef, - private accountService: AccountService, - ) {} - - async ngOnInit() { - this.accountService.activeAccount$ - .pipe( - getUserId, - switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), - takeUntil(this.destroy$), - ) - .subscribe( - (enforcedPasswordPolicyOptions) => - (this.enforcedPolicyOptions = enforcedPasswordPolicyOptions), - ); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - get loggedOutWarningName() { - return this.data.name != null ? this.data.name : this.i18nService.t("thisUser"); - } - - async generatePassword() { - const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; - this.formGroup.patchValue({ - newPassword: await this.passwordGenerationService.generatePassword(options), - }); - this.passwordStrengthComponent.updatePasswordStrength(this.formGroup.value.newPassword); - } - - togglePassword() { - this.showPassword = !this.showPassword; - document.getElementById("newPassword").focus(); - } - - copy() { - const value = this.formGroup.value.newPassword; - if (value == null) { - return; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t("password")), - }); - } - - submit = async () => { - // Validation - if (this.formGroup.value.newPassword == null || this.formGroup.value.newPassword === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - return false; - } - - if (this.formGroup.value.newPassword.length < Utils.minimumPasswordLength) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength), - }); - return false; - } - - if ( - this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - this.passwordStrengthScore, - this.formGroup.value.newPassword, - this.enforcedPolicyOptions, - ) - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - }); - return; - } - - if (this.passwordStrengthScore < 3) { - const result = await this.dialogService.openSimpleDialog({ - title: { key: "weakMasterPassword" }, - content: { key: "weakMasterPasswordDesc" }, - type: "warning", - }); - - if (!result) { - return false; - } - } - - try { - await this.resetPasswordService.resetMasterPassword( - this.formGroup.value.newPassword, - this.data.email, - this.data.id, - this.data.organizationId, - ); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("resetPasswordSuccess"), - }); - } catch (e) { - this.logService.error(e); - } - - this.dialogRef.close(ResetPasswordDialogResult.Ok); - }; - - getStrengthScore(result: number) { - this.passwordStrengthScore = result; - } - - static open = (dialogService: DialogService, input: DialogConfig) => { - return dialogService.open(ResetPasswordComponent, input); - }; -} diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 77a0cecce8e..2a84efd3320 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -86,10 +86,6 @@ import { openUserAddEditDialog, } from "./components/member-dialog"; import { isFixedSeatPlan } from "./components/member-dialog/validators/org-seat-limit-reached.validator"; -import { - ResetPasswordComponent, - ResetPasswordDialogResult, -} from "./components/reset-password.component"; import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service"; import { OrganizationUserService } from "./services/organization-user/organization-user.service"; @@ -767,52 +763,32 @@ export class MembersComponent extends BaseMembersComponent } async resetPassword(user: OrganizationUserView) { - const changePasswordRefactorFlag = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); - - if (changePasswordRefactorFlag) { - if (!user || !user.email || !user.id) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("orgUserDetailsNotFound"), - }); - this.logService.error("Org user details not found when attempting account recovery"); - - return; - } - - const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - email: user.email, - organizationId: this.organization.id as OrganizationId, - organizationUserId: user.id, - }, + if (!user || !user.email || !user.id) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("orgUserDetailsNotFound"), }); - - const result = await lastValueFrom(dialogRef.closed); - if (result === AccountRecoveryDialogResultType.Ok) { - await this.load(); - } + this.logService.error("Org user details not found when attempting account recovery"); return; } - const dialogRef = ResetPasswordComponent.open(this.dialogService, { + const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, { data: { name: this.userNamePipe.transform(user), - email: user != null ? user.email : null, + email: user.email, organizationId: this.organization.id as OrganizationId, - id: user != null ? user.id : null, + organizationUserId: user.id, }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === ResetPasswordDialogResult.Ok) { + if (result === AccountRecoveryDialogResultType.Ok) { await this.load(); } + + return; } protected async removeUserConfirmationDialog(user: OrganizationUserView) { diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 5f626d44161..d9c5ae356a2 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -16,7 +16,6 @@ import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog. import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { UserDialogModule } from "./components/member-dialog"; -import { ResetPasswordComponent } from "./components/reset-password.component"; import { MembersRoutingModule } from "./members-routing.module"; import { MembersComponent } from "./members.component"; @@ -39,7 +38,6 @@ import { MembersComponent } from "./members.component"; BulkRestoreRevokeComponent, BulkStatusComponent, MembersComponent, - ResetPasswordComponent, BulkDeleteDialogComponent, ], }) diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index 8c556986225..02f13cd436b 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -3,7 +3,6 @@ export * from "./login"; export * from "./login-decryption-options"; export * from "./webauthn-login"; export * from "./password-management"; -export * from "./set-password-jit"; export * from "./registration"; export * from "./two-factor-auth"; export * from "./link-sso.service"; diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts index 4cc06baf32b..799e10bc15c 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts @@ -1,6 +1,5 @@ import { TestBed } from "@angular/core/testing"; import { MockProxy, mock } from "jest-mock-extended"; -import { of } from "rxjs"; import { DefaultLoginComponentService } from "@bitwarden/auth/angular"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; @@ -138,8 +137,8 @@ describe("WebLoginComponentService", () => { resetPasswordPolicyEnabled, ]); - internalPolicyService.masterPasswordPolicyOptions$.mockReturnValue( - of(masterPasswordPolicyOptions), + internalPolicyService.combinePoliciesIntoMasterPasswordPolicyOptions.mockReturnValue( + masterPasswordPolicyOptions, ); const result = await service.getOrgPoliciesFromOrgInvite(); diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index cf0adb91144..4ee84ecfde2 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, switchMap } from "rxjs"; import { DefaultLoginComponentService, @@ -11,13 +10,10 @@ import { } from "@bitwarden/auth/angular"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -99,23 +95,8 @@ export class WebLoginComponentService const isPolicyAndAutoEnrollEnabled = resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled; - let enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; - - if ( - await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) - ) { - enforcedPasswordPolicyOptions = - this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies); - } else { - enforcedPasswordPolicyOptions = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => - this.policyService.masterPasswordPolicyOptions$(userId, policies), - ), - ), - ); - } + const enforcedPasswordPolicyOptions = + this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies); return { policies, diff --git a/apps/web/src/app/auth/core/services/set-password-jit/index.ts b/apps/web/src/app/auth/core/services/set-password-jit/index.ts deleted file mode 100644 index fc119fd964f..00000000000 --- a/apps/web/src/app/auth/core/services/set-password-jit/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./web-set-password-jit.service"; diff --git a/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts deleted file mode 100644 index 3078b8e3b83..00000000000 --- a/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { inject } from "@angular/core"; - -import { - DefaultSetPasswordJitService, - SetPasswordCredentials, - SetPasswordJitService, -} from "@bitwarden/auth/angular"; -import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; - -import { RouterService } from "../../../../core/router.service"; - -export class WebSetPasswordJitService - extends DefaultSetPasswordJitService - implements SetPasswordJitService -{ - routerService = inject(RouterService); - organizationInviteService = inject(OrganizationInviteService); - - override async setPassword(credentials: SetPasswordCredentials) { - await super.setPassword(credentials); - - // SSO JIT accepts org invites when setting their MP, meaning - // we can clear the deep linked url for accepting it. - await this.routerService.getAndClearLoginRedirectUrl(); - await this.organizationInviteService.clearOrganizationInvitation(); - } -} diff --git a/apps/web/src/app/auth/set-password.component.html b/apps/web/src/app/auth/set-password.component.html deleted file mode 100644 index 252893d22cb..00000000000 --- a/apps/web/src/app/auth/set-password.component.html +++ /dev/null @@ -1,130 +0,0 @@ -
-
-
-

{{ "setMasterPassword" | i18n }}

-
-
- - {{ "loading" | i18n }} -
-
-

- {{ "orgPermissionsUpdatedMustSetPassword" | i18n }} -

- - -

{{ "orgRequiresYouToSetPassword" | i18n }}

-
- - - {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} - -
- - - -
-
- - - -
-
- - -
-
- {{ "masterPassDesc" | i18n }} -
-
- -
- - -
-
-
- - - {{ "masterPassHintDesc" | i18n }} -
-
-
- - -
-
-
-
-
-
diff --git a/apps/web/src/app/auth/set-password.component.ts b/apps/web/src/app/auth/set-password.component.ts deleted file mode 100644 index a2044e298a5..00000000000 --- a/apps/web/src/app/auth/set-password.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component, inject } from "@angular/core"; - -import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; -import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; - -import { RouterService } from "../core"; - -@Component({ - selector: "app-set-password", - templateUrl: "set-password.component.html", - standalone: false, -}) -export class SetPasswordComponent extends BaseSetPasswordComponent { - routerService = inject(RouterService); - organizationInviteService = inject(OrganizationInviteService); - - protected override async onSetPasswordSuccess( - masterKey: MasterKey, - userKey: [UserKey, EncString], - keyPair: [string, EncString], - ): Promise { - await super.onSetPasswordSuccess(masterKey, userKey, keyPair); - // SSO JIT accepts org invites when setting their MP, meaning - // we can clear the deep linked url for accepting it. - await this.routerService.getAndClearLoginRedirectUrl(); - await this.organizationInviteService.clearOrganizationInvitation(); - } -} diff --git a/apps/web/src/app/auth/settings/change-password.component.html b/apps/web/src/app/auth/settings/change-password.component.html deleted file mode 100644 index 34bb74ee473..00000000000 --- a/apps/web/src/app/auth/settings/change-password.component.html +++ /dev/null @@ -1,129 +0,0 @@ -
-

{{ "changeMasterPassword" | i18n }}

-
- -{{ "loggedOutWarning" | i18n }} - - - -
-
-
-
- - -
-
-
-
-
-
- - - - {{ "important" | i18n }} - {{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }} - - - -
-
-
-
- - -
-
-
-
-
- - -
-
-
-
- - - - - -
-
-
- - -
- -
- - diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts deleted file mode 100644 index ce10a0e5a34..00000000000 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ /dev/null @@ -1,258 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; - -import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service"; - -/** - * @deprecated use the auth `PasswordSettingsComponent` instead - */ -@Component({ - selector: "app-change-password", - templateUrl: "change-password.component.html", - standalone: false, -}) -export class ChangePasswordComponent - extends BaseChangePasswordComponent - implements OnInit, OnDestroy -{ - loading = false; - rotateUserKey = false; - currentMasterPassword: string; - masterPasswordHint: string; - checkForBreaches = true; - characterMinimumMessage = ""; - - constructor( - private auditService: AuditService, - private cipherService: CipherService, - private keyRotationService: UserKeyRotationService, - private masterPasswordApiService: MasterPasswordApiService, - private router: Router, - private syncService: SyncService, - private userVerificationService: UserVerificationService, - protected accountService: AccountService, - protected dialogService: DialogService, - protected i18nService: I18nService, - protected kdfConfigService: KdfConfigService, - protected keyService: KeyService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected messagingService: MessagingService, - protected platformUtilsService: PlatformUtilsService, - protected policyService: PolicyService, - protected toastService: ToastService, - ) { - super( - accountService, - dialogService, - i18nService, - kdfConfigService, - keyService, - masterPasswordService, - messagingService, - platformUtilsService, - policyService, - toastService, - ); - } - - async ngOnInit() { - if (!(await this.userVerificationService.hasMasterPassword())) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/settings/security/two-factor"]); - } - - await super.ngOnInit(); - - this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength); - } - - async rotateUserKeyClicked() { - if (this.rotateUserKey) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - const ciphers = await this.cipherService.getAllDecrypted(activeUserId); - let hasOldAttachments = false; - if (ciphers != null) { - for (let i = 0; i < ciphers.length; i++) { - if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) { - hasOldAttachments = true; - break; - } - } - } - - if (hasOldAttachments) { - const learnMore = await this.dialogService.openSimpleDialog({ - title: { key: "warning" }, - content: { key: "oldAttachmentsNeedFixDesc" }, - acceptButtonText: { key: "learnMore" }, - cancelButtonText: { key: "close" }, - type: "warning", - }); - - if (learnMore) { - this.platformUtilsService.launchUri( - "https://bitwarden.com/help/attachments/#add-storage-space", - ); - } - this.rotateUserKey = false; - return; - } - - const result = await this.dialogService.openSimpleDialog({ - title: { key: "rotateEncKeyTitle" }, - content: - this.i18nService.t("updateEncryptionKeyWarning") + - " " + - this.i18nService.t("updateEncryptionKeyAccountExportWarning") + - " " + - this.i18nService.t("rotateEncKeyConfirmation"), - type: "warning", - }); - - if (!result) { - this.rotateUserKey = false; - } - } - } - - async submit() { - this.loading = true; - if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - this.loading = false; - return; - } - - if ( - this.masterPasswordHint != null && - this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase() - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("hintEqualsPassword"), - }); - this.loading = false; - return; - } - - this.leakedPassword = false; - if (this.checkForBreaches) { - this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0; - } - - if (!(await this.strongPassword())) { - this.loading = false; - return; - } - - try { - if (this.rotateUserKey) { - await this.syncService.fullSync(true); - const user = await firstValueFrom(this.accountService.activeAccount$); - await this.keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData( - this.currentMasterPassword, - this.masterPassword, - user, - this.masterPasswordHint, - ); - } else { - await this.updatePassword(this.masterPassword); - } - } catch (e) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: e.message, - }); - } finally { - this.loading = false; - } - } - - // todo: move this to a service - // https://bitwarden.atlassian.net/browse/PM-17108 - private async updatePassword(newMasterPassword: string) { - const currentMasterPassword = this.currentMasterPassword; - const { userId, email } = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))), - ); - const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId)); - - const currentMasterKey = await this.keyService.makeMasterKey( - currentMasterPassword, - email, - kdfConfig, - ); - const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - currentMasterKey, - userId, - ); - if (decryptedUserKey == null) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("invalidMasterPassword"), - }); - return; - } - - const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig); - const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey( - newMasterKey, - decryptedUserKey, - ); - - const request = new PasswordRequest(); - request.masterPasswordHash = await this.keyService.hashMasterKey( - this.currentMasterPassword, - currentMasterKey, - ); - request.masterPasswordHint = this.masterPasswordHint; - request.newMasterPasswordHash = await this.keyService.hashMasterKey( - newMasterPassword, - newMasterKey, - ); - request.key = newMasterKeyEncryptedUserKey[1].encryptedString; - try { - await this.masterPasswordApiService.postPassword(request); - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("masterPasswordChanged"), - }); - this.messagingService.send("logout"); - } catch { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("errorOccurred"), - }); - } - } -} diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 1d78bb7dd17..bc4bfc5ef1d 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -10,8 +10,6 @@ import { OrganizationManagementPreferencesService } from "@bitwarden/common/admi import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -40,10 +38,6 @@ import { EmergencyAccessTakeoverDialogComponent, EmergencyAccessTakeoverDialogResultType, } from "./takeover/emergency-access-takeover-dialog.component"; -import { - EmergencyAccessTakeoverComponent, - EmergencyAccessTakeoverResultType, -} from "./takeover/emergency-access-takeover.component"; @Component({ selector: "emergency-access", @@ -75,7 +69,6 @@ export class EmergencyAccessComponent implements OnInit { private toastService: ToastService, private apiService: ApiService, private accountService: AccountService, - private configService: ConfigService, ) { this.canAccessPremium$ = this.accountService.activeAccount$.pipe( switchMap((account) => @@ -292,60 +285,36 @@ export class EmergencyAccessComponent implements OnInit { } takeover = async (details: GrantorEmergencyAccess) => { - const changePasswordRefactorFlag = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); - - if (changePasswordRefactorFlag) { - if (!details || !details.email || !details.id) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("grantorDetailsNotFound"), - }); - this.logService.error( - "Grantor details not found when attempting emergency access takeover", - ); - - return; - } - - const grantorName = this.userNamePipe.transform(details); - - const dialogRef = EmergencyAccessTakeoverDialogComponent.open(this.dialogService, { - data: { - grantorName, - grantorEmail: details.email, - emergencyAccessId: details.id, - }, + if (!details || !details.email || !details.id) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("grantorDetailsNotFound"), }); - const result = await lastValueFrom(dialogRef.closed); - if (result === EmergencyAccessTakeoverDialogResultType.Done) { - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("passwordResetFor", grantorName), - }); - } + this.logService.error("Grantor details not found when attempting emergency access takeover"); return; } - const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, { + const grantorName = this.userNamePipe.transform(details); + + const dialogRef = EmergencyAccessTakeoverDialogComponent.open(this.dialogService, { data: { - name: this.userNamePipe.transform(details), - email: details.email, - emergencyAccessId: details.id ?? null, + grantorName, + grantorEmail: details.email, + emergencyAccessId: details.id, }, }); const result = await lastValueFrom(dialogRef.closed); - if (result === EmergencyAccessTakeoverResultType.Done) { + if (result === EmergencyAccessTakeoverDialogResultType.Done) { this.toastService.showToast({ variant: "success", - title: null, - message: this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), + title: "", + message: this.i18nService.t("passwordResetFor", grantorName), }); } + + return; }; private removeGrantee(details: GranteeEmergencyAccess) { diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.html b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.html deleted file mode 100644 index 64b35344455..00000000000 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.html +++ /dev/null @@ -1,54 +0,0 @@ -
- - - {{ "takeover" | i18n }} - {{ params.name }} - -
- {{ "loggedOutWarning" | i18n }} - - -
-
- - {{ "newMasterPass" | i18n }} - - - - - -
-
- - {{ "confirmNewMasterPass" | i18n }} - - - -
-
-
- - - - -
-
diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts deleted file mode 100644 index ede60887725..00000000000 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ /dev/null @@ -1,145 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { switchMap, takeUntil } from "rxjs"; - -import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { - DialogConfig, - DialogRef, - DIALOG_DATA, - DialogService, - ToastService, -} from "@bitwarden/components"; -import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { EmergencyAccessService } from "../../../emergency-access"; - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum EmergencyAccessTakeoverResultType { - Done = "done", -} -type EmergencyAccessTakeoverDialogData = { - /** display name of the account requesting emergency access takeover */ - name: string; - /** email of the account requesting emergency access takeover */ - email: string; - /** traces a unique emergency request */ - emergencyAccessId: string; -}; -@Component({ - selector: "emergency-access-takeover", - templateUrl: "emergency-access-takeover.component.html", - standalone: false, -}) -export class EmergencyAccessTakeoverComponent - extends ChangePasswordComponent - implements OnInit, OnDestroy -{ - @Input() kdf: KdfType; - @Input() kdfIterations: number; - takeoverForm = this.formBuilder.group({ - masterPassword: ["", [Validators.required]], - masterPasswordRetype: ["", [Validators.required]], - }); - - constructor( - @Inject(DIALOG_DATA) protected params: EmergencyAccessTakeoverDialogData, - private formBuilder: FormBuilder, - i18nService: I18nService, - keyService: KeyService, - messagingService: MessagingService, - platformUtilsService: PlatformUtilsService, - policyService: PolicyService, - private emergencyAccessService: EmergencyAccessService, - private logService: LogService, - dialogService: DialogService, - private dialogRef: DialogRef, - kdfConfigService: KdfConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - protected toastService: ToastService, - ) { - super( - accountService, - dialogService, - i18nService, - kdfConfigService, - keyService, - masterPasswordService, - messagingService, - platformUtilsService, - policyService, - toastService, - ); - } - - async ngOnInit() { - const policies = await this.emergencyAccessService.getGrantorPolicies( - this.params.emergencyAccessId, - ); - this.accountService.activeAccount$ - .pipe( - getUserId, - switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)), - takeUntil(this.destroy$), - ) - .subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions)); - } - - ngOnDestroy(): void { - super.ngOnDestroy(); - } - - submit = async () => { - if (this.takeoverForm.invalid) { - this.takeoverForm.markAllAsTouched(); - return; - } - this.masterPassword = this.takeoverForm.get("masterPassword").value; - this.masterPasswordRetype = this.takeoverForm.get("masterPasswordRetype").value; - if (!(await this.strongPassword())) { - return; - } - - try { - await this.emergencyAccessService.takeover( - this.params.emergencyAccessId, - this.masterPassword, - this.params.email, - ); - } catch (e) { - this.logService.error(e); - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("unexpectedError"), - }); - } - this.dialogRef.close(EmergencyAccessTakeoverResultType.Done); - }; - /** - * Strongly typed helper to open a EmergencyAccessTakeoverComponent - * @param dialogService Instance of the dialog service that will be used to open the dialog - * @param config Configuration for the dialog - */ - static open = ( - dialogService: DialogService, - config: DialogConfig, - ) => { - return dialogService.open( - EmergencyAccessTakeoverComponent, - config, - ); - }; -} diff --git a/apps/web/src/app/auth/settings/security/security-routing.module.ts b/apps/web/src/app/auth/settings/security/security-routing.module.ts index 2ec1be5cb7f..f7586329380 100644 --- a/apps/web/src/app/auth/settings/security/security-routing.module.ts +++ b/apps/web/src/app/auth/settings/security/security-routing.module.ts @@ -2,11 +2,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ChangePasswordComponent } from "../change-password.component"; import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component"; import { DeviceManagementOldComponent } from "./device-management-old.component"; @@ -21,30 +19,9 @@ const routes: Routes = [ data: { titleId: "security" }, children: [ { path: "", pathMatch: "full", redirectTo: "password" }, - { - path: "change-password", - component: ChangePasswordComponent, - canActivate: [ - canAccessFeature( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - false, - "/settings/security/password", - false, - ), - ], - data: { titleId: "masterPassword" }, - }, { path: "password", component: PasswordSettingsComponent, - canActivate: [ - canAccessFeature( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - true, - "/settings/security/change-password", - false, - ), - ], data: { titleId: "masterPassword" }, }, { diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 2240371637d..2a237bf6d01 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -1,8 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; @@ -13,21 +11,11 @@ import { SharedModule } from "../../../shared"; }) export class SecurityComponent implements OnInit { showChangePassword = true; - changePasswordRoute = "change-password"; + changePasswordRoute = "password"; - constructor( - private userVerificationService: UserVerificationService, - private configService: ConfigService, - ) {} + constructor(private userVerificationService: UserVerificationService) {} async ngOnInit() { this.showChangePassword = await this.userVerificationService.hasMasterPassword(); - - const changePasswordRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); - if (changePasswordRefreshFlag) { - this.changePasswordRoute = "password"; - } } } diff --git a/apps/web/src/app/auth/settings/settings.module.ts b/apps/web/src/app/auth/settings/settings.module.ts index 437711f4aa6..fcab9450e1c 100644 --- a/apps/web/src/app/auth/settings/settings.module.ts +++ b/apps/web/src/app/auth/settings/settings.module.ts @@ -6,7 +6,6 @@ import { UserKeyRotationModule } from "../../key-management/key-rotation/user-ke import { SharedModule } from "../../shared"; import { EmergencyAccessModule } from "../emergency-access"; -import { ChangePasswordComponent } from "./change-password.component"; import { WebauthnLoginSettingsModule } from "./webauthn-login-settings"; @NgModule({ @@ -17,8 +16,8 @@ import { WebauthnLoginSettingsModule } from "./webauthn-login-settings"; PasswordCalloutComponent, UserKeyRotationModule, ], - declarations: [ChangePasswordComponent], + declarations: [], providers: [], - exports: [ChangePasswordComponent], + exports: [], }) export class AuthSettingsModule {} diff --git a/apps/web/src/app/auth/update-password.component.html b/apps/web/src/app/auth/update-password.component.html deleted file mode 100644 index 7fb3e1fa491..00000000000 --- a/apps/web/src/app/auth/update-password.component.html +++ /dev/null @@ -1,90 +0,0 @@ -
-
-
-

{{ "updateMasterPassword" | i18n }}

-
-
- {{ "masterPasswordInvalidWarning" | i18n }} - - - -
-
-
- - -
-
-
-
-
-
- - - -
-
-
-
- - -
-
-
- - - -
-
-
-
- diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts deleted file mode 100644 index bc53f824228..00000000000 --- a/apps/web/src/app/auth/update-password.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, inject } from "@angular/core"; - -import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; -import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; - -import { RouterService } from "../core"; - -@Component({ - selector: "app-update-password", - templateUrl: "update-password.component.html", - standalone: false, -}) -export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { - private routerService = inject(RouterService); - private organizationInviteService = inject(OrganizationInviteService); - - override async cancel() { - // clearing the login redirect url so that the user - // does not join the organization if they cancel - await this.routerService.getAndClearLoginRedirectUrl(); - await this.organizationInviteService.clearOrganizationInvitation(); - await super.cancel(); - } -} diff --git a/apps/web/src/app/auth/update-temp-password.component.html b/apps/web/src/app/auth/update-temp-password.component.html deleted file mode 100644 index 4fd0ea72b5f..00000000000 --- a/apps/web/src/app/auth/update-temp-password.component.html +++ /dev/null @@ -1,96 +0,0 @@ -
-
-
-

{{ "updateMasterPassword" | i18n }}

-
- {{ masterPasswordWarningText }} - - - - {{ "currentMasterPass" | i18n }} - - - -
- - {{ "newMasterPass" | i18n }} - - - - - -
- - {{ "confirmNewMasterPass" | i18n }} - - - - - {{ "masterPassHint" | i18n }} - - {{ "masterPassHintDesc" | i18n }} - -
-
- - -
-
-
-
-
diff --git a/apps/web/src/app/auth/update-temp-password.component.ts b/apps/web/src/app/auth/update-temp-password.component.ts deleted file mode 100644 index ead10660b92..00000000000 --- a/apps/web/src/app/auth/update-temp-password.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from "@angular/core"; - -import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component"; - -@Component({ - selector: "app-update-temp-password", - templateUrl: "update-temp-password.component.html", - standalone: false, -}) -export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 7fe8ef4c79f..2721f1f7add 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -33,7 +33,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { RegistrationFinishService as RegistrationFinishServiceAbstraction, LoginComponentService, - SetPasswordJitService, SsoComponentService, LoginDecryptionOptionsService, TwoFactorAuthDuoComponentService, @@ -117,7 +116,6 @@ import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; import { WebChangePasswordService, - WebSetPasswordJitService, WebRegistrationFinishService, WebLoginComponentService, WebLoginDecryptionOptionsService, @@ -277,21 +275,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebLockComponentService, deps: [], }), - safeProvider({ - provide: SetPasswordJitService, - useClass: WebSetPasswordJitService, - deps: [ - EncryptService, - I18nServiceAbstraction, - KdfConfigService, - KeyServiceAbstraction, - MasterPasswordApiService, - InternalMasterPasswordServiceAbstraction, - OrganizationApiServiceAbstraction, - OrganizationUserApiService, - InternalUserDecryptionOptionsServiceAbstraction, - ], - }), safeProvider({ provide: SetInitialPasswordService, useClass: WebSetInitialPasswordService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 1fb19757d60..4c1ed1a7472 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -12,14 +12,12 @@ import { } from "@bitwarden/angular/auth/guards"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, - SetPasswordJitComponent, RegistrationLinkExpiredComponent, LoginComponent, LoginSecondaryContentComponent, @@ -39,7 +37,6 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { VaultIcons } from "@bitwarden/vault"; @@ -55,13 +52,10 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component"; import { RecoverDeleteComponent } from "./auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component"; -import { SetPasswordComponent } from "./auth/set-password.component"; import { AccountComponent } from "./auth/settings/account/account.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component"; import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module"; -import { UpdatePasswordComponent } from "./auth/update-password.component"; -import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component"; import { SponsoredFamiliesComponent } from "./billing/settings/sponsored-families.component"; @@ -115,11 +109,6 @@ const routes: Routes = [ component: LoginViaWebAuthnComponent, data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties, }, - { - path: "set-password", - component: SetPasswordComponent, - data: { titleId: "setMasterPassword" } satisfies RouteDataProperties, - }, { path: "verify-email", component: VerifyEmailTokenComponent }, { path: "accept-organization", @@ -143,34 +132,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn()], data: { titleId: "deleteOrganization" }, }, - { - path: "update-temp-password", - component: UpdateTempPasswordComponent, - canActivate: [ - canAccessFeature( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - false, - "change-password", - false, - ), - authGuard, - ], - data: { titleId: "updateTempPassword" } satisfies RouteDataProperties, - }, - { - path: "update-password", - component: UpdatePasswordComponent, - canActivate: [ - canAccessFeature( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - false, - "change-password", - false, - ), - authGuard, - ], - data: { titleId: "updatePassword" } satisfies RouteDataProperties, - }, ], }, { @@ -329,24 +290,12 @@ const routes: Routes = [ }, { path: "set-initial-password", - canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard], + canActivate: [authGuard], component: SetInitialPasswordComponent, data: { maxWidth: "lg", } satisfies AnonLayoutWrapperData, }, - { - path: "set-password-jit", - component: SetPasswordJitComponent, - data: { - pageTitle: { - key: "joinOrganization", - }, - pageSubtitle: { - key: "finishJoiningThisOrganizationBySettingAMasterPassword", - }, - } satisfies AnonLayoutWrapperData, - }, { path: "signup-link-expired", canActivate: [unauthGuardFn()], @@ -601,10 +550,7 @@ const routes: Routes = [ { path: "change-password", component: ChangePasswordComponent, - canActivate: [ - canAccessFeature(FeatureFlag.PM16117_ChangeExistingPasswordRefactor), - authGuard, - ], + canActivate: [authGuard], }, { path: "setup-extension", diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 97c3fa0375c..4de217f331b 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -14,16 +14,12 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/ import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; -import { SetPasswordComponent } from "../auth/set-password.component"; import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component"; import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component"; import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component"; -import { EmergencyAccessTakeoverComponent } from "../auth/settings/emergency-access/takeover/emergency-access-takeover.component"; import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/view/emergency-access-view.component"; import { UserVerificationModule } from "../auth/shared/components/user-verification"; -import { UpdatePasswordComponent } from "../auth/update-password.component"; -import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component"; import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component"; @@ -73,7 +69,6 @@ import { SharedModule } from "./shared.module"; EmergencyAccessAddEditComponent, EmergencyAccessComponent, EmergencyAccessConfirmComponent, - EmergencyAccessTakeoverComponent, EmergencyAccessViewComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, @@ -85,12 +80,9 @@ import { SharedModule } from "./shared.module"; RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, - SetPasswordComponent, SponsoredFamiliesComponent, FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, - UpdatePasswordComponent, - UpdateTempPasswordComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, ], @@ -100,7 +92,6 @@ import { SharedModule } from "./shared.module"; EmergencyAccessAddEditComponent, EmergencyAccessComponent, EmergencyAccessConfirmComponent, - EmergencyAccessTakeoverComponent, EmergencyAccessViewComponent, OrganizationLayoutComponent, OrgEventsComponent, @@ -114,12 +105,9 @@ import { SharedModule } from "./shared.module"; RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, - SetPasswordComponent, SponsoredFamiliesComponent, FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, - UpdateTempPasswordComponent, - UpdatePasswordComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, HeaderModule, diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts deleted file mode 100644 index 8a564d68fd4..00000000000 --- a/libs/angular/src/auth/components/change-password.component.ts +++ /dev/null @@ -1,232 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, OnDestroy, OnInit } from "@angular/core"; -import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { PasswordColorText } from "../../tools/password-strength/password-strength.component"; - -@Directive() -export class ChangePasswordComponent implements OnInit, OnDestroy { - masterPassword: string; - masterPasswordRetype: string; - formPromise: Promise; - enforcedPolicyOptions: MasterPasswordPolicyOptions; - passwordStrengthResult: any; - color: string; - text: string; - leakedPassword: boolean; - minimumLength = Utils.minimumPasswordLength; - - protected email: string; - protected kdfConfig: KdfConfig; - - protected destroy$ = new Subject(); - - constructor( - protected accountService: AccountService, - protected dialogService: DialogService, - protected i18nService: I18nService, - protected kdfConfigService: KdfConfigService, - protected keyService: KeyService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected messagingService: MessagingService, - protected platformUtilsService: PlatformUtilsService, - protected policyService: PolicyService, - protected toastService: ToastService, - ) {} - - async ngOnInit() { - this.email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); - this.accountService.activeAccount$ - .pipe( - getUserId, - switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), - takeUntil(this.destroy$), - ) - .subscribe( - (enforcedPasswordPolicyOptions) => - (this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions), - ); - - if (this.enforcedPolicyOptions?.minLength) { - this.minimumLength = this.enforcedPolicyOptions.minLength; - } - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - async submit() { - if (!(await this.strongPassword())) { - return; - } - - if (!(await this.setupSubmitActions())) { - return; - } - - const [userId, email] = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), - ); - - if (this.kdfConfig == null) { - this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); - } - - // Create new master key - const newMasterKey = await this.keyService.makeMasterKey( - this.masterPassword, - email.trim().toLowerCase(), - this.kdfConfig, - ); - const newMasterKeyHash = await this.keyService.hashMasterKey(this.masterPassword, newMasterKey); - - let newProtectedUserKey: [UserKey, EncString] = null; - const userKey = await this.keyService.getUserKey(); - if (userKey == null) { - newProtectedUserKey = await this.keyService.makeUserKey(newMasterKey); - } else { - newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey); - } - - await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey); - } - - async setupSubmitActions(): Promise { - // Override in sub-class - // Can be used for additional validation and/or other processes the should occur before changing passwords - return true; - } - - async performSubmitActions( - newMasterKeyHash: string, - newMasterKey: MasterKey, - newUserKey: [UserKey, EncString], - ) { - // Override in sub-class - } - - async strongPassword(): Promise { - if (this.masterPassword == null || this.masterPassword === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - return false; - } - if (this.masterPassword.length < this.minimumLength) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength), - }); - return false; - } - if (this.masterPassword !== this.masterPasswordRetype) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPassDoesntMatch"), - }); - return false; - } - - const strengthResult = this.passwordStrengthResult; - - if ( - this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - strengthResult.score, - this.masterPassword, - this.enforcedPolicyOptions, - ) - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - }); - return false; - } - - const weakPassword = strengthResult != null && strengthResult.score < 3; - - if (weakPassword && this.leakedPassword) { - const result = await this.dialogService.openSimpleDialog({ - title: { key: "weakAndExposedMasterPassword" }, - content: { key: "weakAndBreachedMasterPasswordDesc" }, - type: "warning", - }); - - if (!result) { - return false; - } - } else { - if (weakPassword) { - const result = await this.dialogService.openSimpleDialog({ - title: { key: "weakMasterPassword" }, - content: { key: "weakMasterPasswordDesc" }, - type: "warning", - }); - - if (!result) { - return false; - } - } - if (this.leakedPassword) { - const result = await this.dialogService.openSimpleDialog({ - title: { key: "exposedMasterPassword" }, - content: { key: "exposedMasterPasswordDesc" }, - type: "warning", - }); - - if (!result) { - return false; - } - } - } - - return true; - } - - async logOut() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - acceptButtonText: { key: "logOut" }, - type: "warning", - }); - - if (confirmed) { - this.messagingService.send("logout"); - } - } - - getStrengthResult(result: any) { - this.passwordStrengthResult = result; - } - - getPasswordScoreText(event: PasswordColorText) { - this.color = event.color; - this.text = event.text; - } -} diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts deleted file mode 100644 index 03cbdbc625a..00000000000 --- a/libs/angular/src/auth/components/set-password.component.ts +++ /dev/null @@ -1,300 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, of } from "rxjs"; -import { filter, first, switchMap, tap } from "rxjs/operators"; - -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { - OrganizationUserApiService, - OrganizationUserResetPasswordEnrollmentRequest, -} from "@bitwarden/admin-console/common"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { HashPurpose } from "@bitwarden/common/platform/enums"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; - -@Directive() -export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit { - syncLoading = true; - showPassword = false; - hint = ""; - orgSsoIdentifier: string = null; - orgId: string; - resetPasswordAutoEnroll = false; - onSuccessfulChangePassword: () => Promise; - successRoute = "vault"; - activeUserId: UserId; - - forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; - ForceSetPasswordReason = ForceSetPasswordReason; - - constructor( - protected accountService: AccountService, - protected dialogService: DialogService, - protected encryptService: EncryptService, - protected i18nService: I18nService, - protected kdfConfigService: KdfConfigService, - protected keyService: KeyService, - protected masterPasswordApiService: MasterPasswordApiService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected messagingService: MessagingService, - protected organizationApiService: OrganizationApiServiceAbstraction, - protected organizationUserApiService: OrganizationUserApiService, - protected platformUtilsService: PlatformUtilsService, - protected policyApiService: PolicyApiServiceAbstraction, - protected policyService: PolicyService, - protected route: ActivatedRoute, - protected router: Router, - protected ssoLoginService: SsoLoginServiceAbstraction, - protected syncService: SyncService, - protected toastService: ToastService, - protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - ) { - super( - accountService, - dialogService, - i18nService, - kdfConfigService, - keyService, - masterPasswordService, - messagingService, - platformUtilsService, - policyService, - toastService, - ); - } - - async ngOnInit() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - super.ngOnInit(); - - await this.syncService.fullSync(true); - this.syncLoading = false; - - this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - - this.forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(this.activeUserId), - ); - - this.route.queryParams - .pipe( - first(), - switchMap((qParams) => { - if (qParams.identifier != null) { - return of(qParams.identifier); - } else { - // Try to get orgSsoId from state as fallback - // Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario. - return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId); - } - }), - filter((orgSsoId) => orgSsoId != null), - tap((orgSsoId: string) => { - this.orgSsoIdentifier = orgSsoId; - }), - switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)), - tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => { - this.orgId = orgAutoEnrollStatusResponse.id; - this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled; - }), - switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => - // Must get org id from response to get master password policy options - this.policyApiService.getMasterPasswordPolicyOptsForOrgUser( - orgAutoEnrollStatusResponse.id, - ), - ), - tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => { - this.enforcedPolicyOptions = masterPasswordPolicyOptions; - }), - ) - .subscribe({ - error: () => { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("errorOccurred"), - }); - }, - }); - } - - async setupSubmitActions() { - this.kdfConfig = DEFAULT_KDF_CONFIG; - return true; - } - - async performSubmitActions( - masterPasswordHash: string, - masterKey: MasterKey, - userKey: [UserKey, EncString], - ) { - let keysRequest: KeysRequest | null = null; - let newKeyPair: [string, EncString] | null = null; - - if ( - this.forceSetPasswordReason != - ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission - ) { - // Existing JIT provisioned user in a MP encryption org setting first password - // Users in this state will not already have a user asymmetric key pair so must create it for them - // We don't want to re-create the user key pair if the user already has one (TDE user case) - - // in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one - const existingUserPrivateKey = (await firstValueFrom( - this.keyService.userPrivateKey$(this.activeUserId), - )) as Uint8Array; - const existingUserPublicKey = await firstValueFrom( - this.keyService.userPublicKey$(this.activeUserId), - ); - if (existingUserPrivateKey != null && existingUserPublicKey != null) { - const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); - newKeyPair = [ - existingUserPublicKeyB64, - await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]), - ]; - } else { - newKeyPair = await this.keyService.makeKeyPair(userKey[0]); - } - keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString); - } - - const request = new SetPasswordRequest( - masterPasswordHash, - userKey[1].encryptedString, - this.hint, - this.orgSsoIdentifier, - keysRequest, - this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions - this.kdfConfig.iterations, - ); - try { - if (this.resetPasswordAutoEnroll) { - this.formPromise = this.masterPasswordApiService - .setPassword(request) - .then(async () => { - await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair); - return this.organizationApiService.getKeys(this.orgId); - }) - .then(async (response) => { - if (response == null) { - throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); - } - const publicKey = Utils.fromB64ToArray(response.publicKey); - - // RSA Encrypt user key with organization public key - const userKey = await this.keyService.getUserKey(); - const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( - userKey, - publicKey, - ); - - const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); - resetRequest.masterPasswordHash = masterPasswordHash; - resetRequest.resetPasswordKey = encryptedUserKey.encryptedString; - - return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( - this.orgId, - this.activeUserId, - resetRequest, - ); - }); - } else { - this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => { - await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair); - }); - } - - await this.formPromise; - - if (this.onSuccessfulChangePassword != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulChangePassword(); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.successRoute]); - } - } catch { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("errorOccurred"), - }); - } - } - - togglePassword(confirmField: boolean) { - this.showPassword = !this.showPassword; - document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); - } - - protected async onSetPasswordSuccess( - masterKey: MasterKey, - userKey: [UserKey, EncString], - keyPair: [string, EncString] | null, - ) { - // Clear force set password reason to allow navigation back to vault. - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.None, - this.activeUserId, - ); - - // User now has a password so update account decryption options in state - const userDecryptionOpts = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, - ); - userDecryptionOpts.hasMasterPassword = true; - await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig); - await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId); - await this.keyService.setUserKey(userKey[0], this.activeUserId); - - // Set private key only for new JIT provisioned users in MP encryption orgs - // Existing TDE users will have private key set on sync or on login - if ( - keyPair !== null && - this.forceSetPasswordReason != - ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission - ) { - await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId); - } - - const localMasterKeyHash = await this.keyService.hashMasterKey( - this.masterPassword, - masterKey, - HashPurpose.LocalAuthorization, - ); - await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId); - } -} diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts deleted file mode 100644 index fa3ab58db69..00000000000 --- a/libs/angular/src/auth/components/update-password.component.ts +++ /dev/null @@ -1,141 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { Verification } from "@bitwarden/common/auth/types/verification"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; - -@Directive() -export class UpdatePasswordComponent extends BaseChangePasswordComponent { - hint: string; - key: string; - enforcedPolicyOptions: MasterPasswordPolicyOptions; - showPassword = false; - currentMasterPassword: string; - - onSuccessfulChangePassword: () => Promise; - - constructor( - protected router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - policyService: PolicyService, - keyService: KeyService, - messagingService: MessagingService, - private masterPasswordApiService: MasterPasswordApiService, - private userVerificationService: UserVerificationService, - private logService: LogService, - dialogService: DialogService, - kdfConfigService: KdfConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - toastService: ToastService, - ) { - super( - accountService, - dialogService, - i18nService, - kdfConfigService, - keyService, - masterPasswordService, - messagingService, - platformUtilsService, - policyService, - toastService, - ); - } - - togglePassword(confirmField: boolean) { - this.showPassword = !this.showPassword; - document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); - } - - async cancel() { - await this.router.navigate(["/vault"]); - } - - async setupSubmitActions(): Promise { - if (this.currentMasterPassword == null || this.currentMasterPassword === "") { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("masterPasswordRequired"), - }); - return false; - } - - const secret: Verification = { - type: VerificationType.MasterPassword, - secret: this.currentMasterPassword, - }; - try { - await this.userVerificationService.verifyUser(secret); - } catch (e) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: e.message, - }); - return false; - } - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); - return true; - } - - async performSubmitActions( - newMasterKeyHash: string, - newMasterKey: MasterKey, - newUserKey: [UserKey, EncString], - ) { - try { - // Create Request - const request = new PasswordRequest(); - request.masterPasswordHash = await this.keyService.hashMasterKey( - this.currentMasterPassword, - await this.keyService.getOrDeriveMasterKey(this.currentMasterPassword), - ); - request.newMasterPasswordHash = newMasterKeyHash; - request.key = newUserKey[1].encryptedString; - - // Update user's password - await this.masterPasswordApiService.postPassword(request); - - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("masterPasswordChanged"), - message: this.i18nService.t("logBackIn"), - }); - - if (this.onSuccessfulChangePassword != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulChangePassword(); - } else { - this.messagingService.send("logout"); - } - } catch (e) { - this.logService.error(e); - } - } -} diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts deleted file mode 100644 index 681f69b083a..00000000000 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ /dev/null @@ -1,232 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; -import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request"; -import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request"; -import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component"; - -@Directive() -export class UpdateTempPasswordComponent extends BaseChangePasswordComponent implements OnInit { - hint: string; - key: string; - enforcedPolicyOptions: MasterPasswordPolicyOptions; - showPassword = false; - reason: ForceSetPasswordReason = ForceSetPasswordReason.None; - verification: MasterPasswordVerification = { - type: VerificationType.MasterPassword, - secret: "", - }; - - onSuccessfulChangePassword: () => Promise; - - get requireCurrentPassword(): boolean { - return this.reason === ForceSetPasswordReason.WeakMasterPassword; - } - - constructor( - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - policyService: PolicyService, - keyService: KeyService, - messagingService: MessagingService, - private masterPasswordApiService: MasterPasswordApiService, - private syncService: SyncService, - private logService: LogService, - private userVerificationService: UserVerificationService, - protected router: Router, - dialogService: DialogService, - kdfConfigService: KdfConfigService, - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - toastService: ToastService, - ) { - super( - accountService, - dialogService, - i18nService, - kdfConfigService, - keyService, - masterPasswordService, - messagingService, - platformUtilsService, - policyService, - toastService, - ); - } - - async ngOnInit() { - await this.syncService.fullSync(true); - - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId)); - - // If we somehow end up here without a reason, go back to the home page - if (this.reason == ForceSetPasswordReason.None) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/"]); - return; - } - - await super.ngOnInit(); - } - - get masterPasswordWarningText(): string { - if (this.reason == ForceSetPasswordReason.WeakMasterPassword) { - return this.i18nService.t("updateWeakMasterPasswordWarning"); - } else if (this.reason == ForceSetPasswordReason.TdeOffboarding) { - return this.i18nService.t("tdeDisabledMasterPasswordRequired"); - } else { - return this.i18nService.t("updateMasterPasswordWarning"); - } - } - - togglePassword(confirmField: boolean) { - this.showPassword = !this.showPassword; - document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus(); - } - - async setupSubmitActions(): Promise { - const [userId, email] = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), - ); - this.email = email; - this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId); - return true; - } - - async submit() { - // Validation - if (!(await this.strongPassword())) { - return; - } - - if (!(await this.setupSubmitActions())) { - return; - } - - try { - // Create new key and hash new password - const newMasterKey = await this.keyService.makeMasterKey( - this.masterPassword, - this.email.trim().toLowerCase(), - this.kdfConfig, - ); - const newPasswordHash = await this.keyService.hashMasterKey( - this.masterPassword, - newMasterKey, - ); - - // Grab user key - const userKey = await this.keyService.getUserKey(); - - // Encrypt user key with new master key - const newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey( - newMasterKey, - userKey, - ); - - await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey); - } catch (e) { - this.logService.error(e); - } - } - - async performSubmitActions( - masterPasswordHash: string, - masterKey: MasterKey, - userKey: [UserKey, EncString], - ) { - try { - switch (this.reason) { - case ForceSetPasswordReason.AdminForcePasswordReset: - this.formPromise = this.updateTempPassword(masterPasswordHash, userKey); - break; - case ForceSetPasswordReason.WeakMasterPassword: - this.formPromise = this.updatePassword(masterPasswordHash, userKey); - break; - case ForceSetPasswordReason.TdeOffboarding: - this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey); - break; - } - - await this.formPromise; - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("updatedMasterPassword"), - }); - - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.None, - userId, - ); - - if (this.onSuccessfulChangePassword != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.onSuccessfulChangePassword(); - } else { - this.messagingService.send("logout"); - } - } catch (e) { - this.logService.error(e); - } - } - private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) { - const request = new UpdateTempPasswordRequest(); - request.key = userKey[1].encryptedString; - request.newMasterPasswordHash = masterPasswordHash; - request.masterPasswordHint = this.hint; - - return this.masterPasswordApiService.putUpdateTempPassword(request); - } - - private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) { - const request = await this.userVerificationService.buildRequest( - this.verification, - PasswordRequest, - ); - request.masterPasswordHint = this.hint; - request.newMasterPasswordHash = newMasterPasswordHash; - request.key = userKey[1].encryptedString; - - return this.masterPasswordApiService.postPassword(request); - } - - private async updateTdeOffboardingPassword( - masterPasswordHash: string, - userKey: [UserKey, EncString], - ) { - const request = new UpdateTdeOffboardingPasswordRequest(); - request.key = userKey[1].encryptedString; - request.newMasterPasswordHash = masterPasswordHash; - request.masterPasswordHint = this.hint; - - return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request); - } -} diff --git a/libs/angular/src/auth/guards/auth.guard.spec.ts b/libs/angular/src/auth/guards/auth.guard.spec.ts index 1681fa2b4ea..fccfcd58874 100644 --- a/libs/angular/src/auth/guards/auth.guard.spec.ts +++ b/libs/angular/src/auth/guards/auth.guard.spec.ts @@ -68,10 +68,7 @@ describe("AuthGuard", () => { { path: "", component: EmptyComponent }, { path: "guarded-route", component: EmptyComponent, canActivate: [authGuard] }, { path: "lock", component: EmptyComponent }, - { path: "set-password", component: EmptyComponent }, - { path: "set-password-jit", component: EmptyComponent }, { path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] }, - { path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] }, { path: "change-password", component: EmptyComponent }, { path: "remove-password", component: EmptyComponent, canActivate: [authGuard] }, ]), @@ -125,109 +122,58 @@ describe("AuthGuard", () => { }); describe("given user is Locked", () => { - describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { - it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => { - const { router } = setup( - AuthenticationStatus.Locked, - ForceSetPasswordReason.TdeOffboardingUntrustedDevice, - false, - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); + it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => { + const { router } = setup( + AuthenticationStatus.Locked, + ForceSetPasswordReason.TdeOffboardingUntrustedDevice, + false, + ); - await router.navigate(["guarded-route"]); - expect(router.url).toBe("/set-initial-password"); - }); + await router.navigate(["guarded-route"]); + expect(router.url).toBe("/set-initial-password"); + }); - it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => { - const { router } = setup( - AuthenticationStatus.Unlocked, - ForceSetPasswordReason.TdeOffboardingUntrustedDevice, - false, - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); + it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => { + const { router } = setup( + AuthenticationStatus.Unlocked, + ForceSetPasswordReason.TdeOffboardingUntrustedDevice, + false, + ); - await router.navigate(["/set-initial-password"]); - expect(router.url).toContain("/set-initial-password"); - }); + await router.navigate(["/set-initial-password"]); + expect(router.url).toContain("/set-initial-password"); }); }); - describe("given user is Unlocked", () => { - describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { - const tests = [ - ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, - ForceSetPasswordReason.TdeOffboarding, - ]; + describe("given user is Unlocked and ForceSetPasswordReason requires setting an initial password", () => { + const tests = [ + ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + ForceSetPasswordReason.TdeOffboarding, + ]; - describe("given user attempts to navigate to an auth guarded route", () => { - tests.forEach((reason) => { - it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup( - AuthenticationStatus.Unlocked, - reason, - false, - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); + describe("given user attempts to navigate to an auth guarded route", () => { + tests.forEach((reason) => { + it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { + const { router } = setup(AuthenticationStatus.Unlocked, reason, false); - await router.navigate(["guarded-route"]); - expect(router.url).toContain("/set-initial-password"); - }); - }); - }); - - describe("given user attempts to navigate to /set-initial-password", () => { - tests.forEach((reason) => { - it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup( - AuthenticationStatus.Unlocked, - reason, - false, - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); - - await router.navigate(["/set-initial-password"]); - expect(router.url).toContain("/set-initial-password"); - }); + await router.navigate(["guarded-route"]); + expect(router.url).toContain("/set-initial-password"); }); }); }); - describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => { - const tests = [ - { - reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, - url: "/set-password", - }, - { - reason: ForceSetPasswordReason.TdeOffboarding, - url: "/update-temp-password", - }, - ]; + describe("given user attempts to navigate to /set-initial-password", () => { + tests.forEach((reason) => { + it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { + const { router } = setup(AuthenticationStatus.Unlocked, reason, false); - describe("given user attempts to navigate to an auth guarded route", () => { - tests.forEach(({ reason, url }) => { - it(`should redirect to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup(AuthenticationStatus.Unlocked, reason); - - await router.navigate(["/guarded-route"]); - expect(router.url).toContain(url); - }); - }); - }); - - describe("given user attempts to navigate to the set- or update- password route itself", () => { - tests.forEach(({ reason, url }) => { - it(`should allow navigation to continue to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup(AuthenticationStatus.Unlocked, reason); - - await router.navigate([url]); - expect(router.url).toContain(url); - }); + await router.navigate(["/set-initial-password"]); + expect(router.url).toContain("/set-initial-password"); }); }); }); - describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is ON", () => { + describe("given user is Unlocked and ForceSetPasswordReason requires changing an existing password", () => { const tests = [ ForceSetPasswordReason.AdminForcePasswordReset, ForceSetPasswordReason.WeakMasterPassword, @@ -236,12 +182,7 @@ describe("AuthGuard", () => { describe("given user attempts to navigate to an auth guarded route", () => { tests.forEach((reason) => { it(`should redirect to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup( - AuthenticationStatus.Unlocked, - reason, - false, - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); + const { router } = setup(AuthenticationStatus.Unlocked, reason, false); await router.navigate(["guarded-route"]); expect(router.url).toContain("/change-password"); @@ -256,7 +197,6 @@ describe("AuthGuard", () => { AuthenticationStatus.Unlocked, ForceSetPasswordReason.AdminForcePasswordReset, false, - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, ); await router.navigate(["/change-password"]); @@ -265,34 +205,5 @@ describe("AuthGuard", () => { }); }); }); - - describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is OFF", () => { - const tests = [ - ForceSetPasswordReason.AdminForcePasswordReset, - ForceSetPasswordReason.WeakMasterPassword, - ]; - - describe("given user attempts to navigate to an auth guarded route", () => { - tests.forEach((reason) => { - it(`should redirect to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup(AuthenticationStatus.Unlocked, reason); - - await router.navigate(["guarded-route"]); - expect(router.url).toContain("/update-temp-password"); - }); - }); - }); - - describe("given user attempts to navigate to /update-temp-password", () => { - tests.forEach((reason) => { - it(`should allow navigation to continue to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => { - const { router } = setup(AuthenticationStatus.Unlocked, reason); - - await router.navigate(["/update-temp-password"]); - expect(router.url).toContain("/update-temp-password"); - }); - }); - }); - }); }); }); diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 3722a7c802a..8e8e70a6d29 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -14,10 +14,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; export const authGuard: CanActivateFn = async ( @@ -30,7 +28,6 @@ export const authGuard: CanActivateFn = async ( const keyConnectorService = inject(KeyConnectorService); const accountService = inject(AccountService); const masterPasswordService = inject(MasterPasswordServiceAbstraction); - const configService = inject(ConfigService); const authStatus = await authService.getAuthStatus(); @@ -44,16 +41,11 @@ export const authGuard: CanActivateFn = async ( masterPasswordService.forceSetPasswordReason$(userId), ); - const isSetInitialPasswordFlagOn = await configService.getFeatureFlag( - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); - // User JIT provisioned into a master-password-encryption org if ( authStatus === AuthenticationStatus.Locked && forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser && - !routerState.url.includes("set-initial-password") && - isSetInitialPasswordFlagOn + !routerState.url.includes("set-initial-password") ) { return router.createUrlTree(["/set-initial-password"]); } @@ -62,8 +54,7 @@ export const authGuard: CanActivateFn = async ( if ( authStatus === AuthenticationStatus.Locked && forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice && - !routerState.url.includes("set-initial-password") && - isSetInitialPasswordFlagOn + !routerState.url.includes("set-initial-password") ) { return router.createUrlTree(["/set-initial-password"]); } @@ -90,39 +81,28 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree(["/remove-password"]); } - // TDE org user has "manage account recovery" permission + // Handle cases where a user needs to set a password when they don't already have one: + // - TDE org user has been given "manage account recovery" permission + // - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password if ( - forceSetPasswordReason === - ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission && - !routerState.url.includes("set-password") && + (forceSetPasswordReason === + ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission || + forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) && !routerState.url.includes("set-initial-password") ) { - const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password"; + const route = "/set-initial-password"; return router.createUrlTree([route]); } - // TDE Offboarding on trusted device - if ( - forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding && - !routerState.url.includes("update-temp-password") && - !routerState.url.includes("set-initial-password") - ) { - const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/update-temp-password"; - return router.createUrlTree([route]); - } - - const isChangePasswordFlagOn = await configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); - - // Post- Account Recovery or Weak Password on login + // Handle cases where a user has a password but needs to set a new one: + // - Account recovery + // - Weak Password on login if ( (forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset || forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) && - !routerState.url.includes("update-temp-password") && !routerState.url.includes("change-password") ) { - const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password"; + const route = "/change-password"; return router.createUrlTree([route]); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0ad7b57d9b3..aaabc405da0 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -22,13 +22,11 @@ import { DefaultLoginComponentService, DefaultLoginDecryptionOptionsService, DefaultRegistrationFinishService, - DefaultSetPasswordJitService, DefaultTwoFactorAuthComponentService, DefaultTwoFactorAuthWebAuthnComponentService, LoginComponentService, LoginDecryptionOptionsService, RegistrationFinishService as RegistrationFinishServiceAbstraction, - SetPasswordJitService, TwoFactorAuthComponentService, TwoFactorAuthWebAuthnComponentService, } from "@bitwarden/auth/angular"; @@ -1417,21 +1415,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationInviteService, deps: [], }), - safeProvider({ - provide: SetPasswordJitService, - useClass: DefaultSetPasswordJitService, - deps: [ - EncryptService, - I18nServiceAbstraction, - KdfConfigService, - KeyService, - MasterPasswordApiServiceAbstraction, - InternalMasterPasswordServiceAbstraction, - OrganizationApiServiceAbstraction, - OrganizationUserApiService, - InternalUserDecryptionOptionsServiceAbstraction, - ], - }), safeProvider({ provide: SetInitialPasswordService, useClass: DefaultSetInitialPasswordService, diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index aa0041c7ec3..3e8dc575c7b 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -41,11 +41,6 @@ export * from "./registration/registration-env-selector/registration-env-selecto export * from "./registration/registration-finish/registration-finish.service"; export * from "./registration/registration-finish/default-registration-finish.service"; -// set password (JIT user) -export * from "./set-password-jit/set-password-jit.component"; -export * from "./set-password-jit/set-password-jit.service.abstraction"; -export * from "./set-password-jit/default-set-password-jit.service"; - // user verification export * from "./user-verification/user-verification-dialog.component"; export * from "./user-verification/user-verification-dialog.types"; diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index b3509850ac0..2a2be148a86 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -18,7 +18,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -230,29 +229,21 @@ export class LoginComponent implements OnInit, OnDestroy { return; } - let credentials: PasswordLoginCredentials; + // Try to retrieve any org policies from an org invite now so we can send it to the + // login strategies. Since it is optional and we only want to be doing this on the + // web we will only send in content in the right context. + const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite + ? await this.loginComponentService.getOrgPoliciesFromOrgInvite() + : null; - if ( - await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) - ) { - // Try to retrieve any org policies from an org invite now so we can send it to the - // login strategies. Since it is optional and we only want to be doing this on the - // web we will only send in content in the right context. - const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite - ? await this.loginComponentService.getOrgPoliciesFromOrgInvite() - : null; + const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions; - const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions; - - credentials = new PasswordLoginCredentials( - email, - masterPassword, - undefined, - orgMasterPasswordPolicyOptions, - ); - } else { - credentials = new PasswordLoginCredentials(email, masterPassword); - } + const credentials = new PasswordLoginCredentials( + email, + masterPassword, + undefined, + orgMasterPasswordPolicyOptions, + ); try { const authResult = await this.loginStrategyService.logIn(credentials); @@ -332,7 +323,7 @@ export class LoginComponent implements OnInit, OnDestroy { await this.loginSuccessHandlerService.run(authResult.userId); // Determine where to send the user next - // The AuthGuard will handle routing to update-temp-password based on state + // The AuthGuard will handle routing to change-password based on state // TODO: PM-18269 - evaluate if we can combine this with the // password evaluation done in the password login strategy. @@ -344,7 +335,7 @@ export class LoginComponent implements OnInit, OnDestroy { if (orgPolicies) { // Since we have retrieved the policies, we can go ahead and set them into state for future use - // e.g., the update-password page currently only references state for policy data and + // e.g., the change-password page currently only references state for policy data and // doesn't fallback to pulling them from the server like it should if they are null. await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies); @@ -352,13 +343,7 @@ export class LoginComponent implements OnInit, OnDestroy { orgPolicies.enforcedPasswordPolicyOptions, ); if (isPasswordChangeRequired) { - const changePasswordFeatureFlagOn = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_ChangeExistingPasswordRefactor, - ); - - await this.router.navigate( - changePasswordFeatureFlagOn ? ["change-password"] : ["update-password"], - ); + await this.router.navigate(["change-password"]); return; } } diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts index 4325b4bcbc1..6362b901fc8 100644 --- a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -10,7 +10,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -151,25 +150,17 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { this.loginSuccessHandlerService.run(authResult.userId); // TODO: PM-22663 use the new service to handle routing. + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(activeUserId), + ); + if ( - await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) + forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword || + forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getUserId), - ); - - const forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(activeUserId), - ); - - if ( - forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword || - forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset - ) { - await this.router.navigate(["/change-password"]); - } else { - await this.router.navigate(["/vault"]); - } + await this.router.navigate(["/change-password"]); } else { await this.router.navigate(["/vault"]); } diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts deleted file mode 100644 index 6a9a37700cc..00000000000 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; - -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { - FakeUserDecryptionOptions as UserDecryptionOptions, - InternalUserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; - -import { PasswordInputResult } from "../input-password/password-input-result"; - -import { DefaultSetPasswordJitService } from "./default-set-password-jit.service"; -import { SetPasswordCredentials } from "./set-password-jit.service.abstraction"; - -describe("DefaultSetPasswordJitService", () => { - let sut: DefaultSetPasswordJitService; - - let masterPasswordApiService: MockProxy; - let keyService: MockProxy; - let encryptService: MockProxy; - let i18nService: MockProxy; - let kdfConfigService: MockProxy; - let masterPasswordService: MockProxy; - let organizationApiService: MockProxy; - let organizationUserApiService: MockProxy; - let userDecryptionOptionsService: MockProxy; - - beforeEach(() => { - masterPasswordApiService = mock(); - keyService = mock(); - encryptService = mock(); - i18nService = mock(); - kdfConfigService = mock(); - masterPasswordService = mock(); - organizationApiService = mock(); - organizationUserApiService = mock(); - userDecryptionOptionsService = mock(); - - sut = new DefaultSetPasswordJitService( - encryptService, - i18nService, - kdfConfigService, - keyService, - masterPasswordApiService, - masterPasswordService, - organizationApiService, - organizationUserApiService, - userDecryptionOptionsService, - ); - }); - - it("should instantiate the DefaultSetPasswordJitService", () => { - expect(sut).not.toBeFalsy(); - }); - - describe("setPassword", () => { - let masterKey: MasterKey; - let userKey: UserKey; - let userKeyEncString: EncString; - let protectedUserKey: [UserKey, EncString]; - let keyPair: [string, EncString]; - let keysRequest: KeysRequest; - let organizationKeys: OrganizationKeysResponse; - let orgPublicKey: Uint8Array; - - let orgSsoIdentifier: string; - let orgId: string; - let resetPasswordAutoEnroll: boolean; - let userId: UserId; - let passwordInputResult: PasswordInputResult; - let credentials: SetPasswordCredentials; - - let userDecryptionOptionsSubject: BehaviorSubject; - let setPasswordRequest: SetPasswordRequest; - - beforeEach(() => { - masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; - userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - userKeyEncString = new EncString("userKeyEncrypted"); - protectedUserKey = [userKey, userKeyEncString]; - keyPair = ["publicKey", new EncString("privateKey")]; - keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); - organizationKeys = { - privateKey: "orgPrivateKey", - publicKey: "orgPublicKey", - } as OrganizationKeysResponse; - orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey); - - orgSsoIdentifier = "orgSsoIdentifier"; - orgId = "orgId"; - resetPasswordAutoEnroll = false; - userId = "userId" as UserId; - - passwordInputResult = { - newMasterKey: masterKey, - newServerMasterKeyHash: "newServerMasterKeyHash", - newLocalMasterKeyHash: "newLocalMasterKeyHash", - newPasswordHint: "newPasswordHint", - kdfConfig: DEFAULT_KDF_CONFIG, - newPassword: "newPassword", - }; - - credentials = { - newMasterKey: passwordInputResult.newMasterKey, - newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, - newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash, - newPasswordHint: passwordInputResult.newPasswordHint, - kdfConfig: passwordInputResult.kdfConfig, - orgSsoIdentifier, - orgId, - resetPasswordAutoEnroll, - userId, - }; - - userDecryptionOptionsSubject = new BehaviorSubject(null); - userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; - - setPasswordRequest = new SetPasswordRequest( - passwordInputResult.newServerMasterKeyHash, - protectedUserKey[1].encryptedString, - passwordInputResult.newPasswordHint, - orgSsoIdentifier, - keysRequest, - passwordInputResult.kdfConfig.kdfType, - passwordInputResult.kdfConfig.iterations, - ); - }); - - function setupSetPasswordMocks(hasUserKey = true) { - if (!hasUserKey) { - keyService.userKey$.mockReturnValue(of(null)); - keyService.makeUserKey.mockResolvedValue(protectedUserKey); - } else { - keyService.userKey$.mockReturnValue(of(userKey)); - keyService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey); - } - - keyService.makeKeyPair.mockResolvedValue(keyPair); - - masterPasswordApiService.setPassword.mockResolvedValue(undefined); - masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined); - - userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); - userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined); - kdfConfigService.setKdfConfig.mockResolvedValue(undefined); - keyService.setUserKey.mockResolvedValue(undefined); - - keyService.setPrivateKey.mockResolvedValue(undefined); - - masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined); - } - - function setupResetPasswordAutoEnrollMocks(organizationKeysExist = true) { - if (organizationKeysExist) { - organizationApiService.getKeys.mockResolvedValue(organizationKeys); - } else { - organizationApiService.getKeys.mockResolvedValue(null); - return; - } - - keyService.userKey$.mockReturnValue(of(userKey)); - encryptService.encapsulateKeyUnsigned.mockResolvedValue(userKeyEncString); - - organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue( - undefined, - ); - } - - it("should set password successfully (given a user key)", async () => { - // Arrange - setupSetPasswordMocks(); - - // Act - await sut.setPassword(credentials); - - // Assert - expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); - }); - - it("should set password successfully (given no user key)", async () => { - // Arrange - setupSetPasswordMocks(false); - - // Act - await sut.setPassword(credentials); - - // Assert - expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); - }); - - it("should handle reset password auto enroll", async () => { - // Arrange - credentials.resetPasswordAutoEnroll = true; - - setupSetPasswordMocks(); - setupResetPasswordAutoEnrollMocks(); - - // Act - await sut.setPassword(credentials); - - // Assert - expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); - expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId); - expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(userKey, orgPublicKey); - expect( - organizationUserApiService.putOrganizationUserResetPasswordEnrollment, - ).toHaveBeenCalled(); - }); - - it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => { - // Arrange - credentials.resetPasswordAutoEnroll = true; - - setupSetPasswordMocks(); - setupResetPasswordAutoEnrollMocks(false); - - // Act and Assert - await expect(sut.setPassword(credentials)).rejects.toThrow(); - expect( - organizationUserApiService.putOrganizationUserResetPasswordEnrollment, - ).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts deleted file mode 100644 index 5fc3272b650..00000000000 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ /dev/null @@ -1,176 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom } from "rxjs"; - -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { - OrganizationUserApiService, - OrganizationUserResetPasswordEnrollmentRequest, -} from "@bitwarden/admin-console/common"; -import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; -import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management"; - -import { - SetPasswordCredentials, - SetPasswordJitService, -} from "./set-password-jit.service.abstraction"; - -export class DefaultSetPasswordJitService implements SetPasswordJitService { - constructor( - protected encryptService: EncryptService, - protected i18nService: I18nService, - protected kdfConfigService: KdfConfigService, - protected keyService: KeyService, - protected masterPasswordApiService: MasterPasswordApiService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected organizationApiService: OrganizationApiServiceAbstraction, - protected organizationUserApiService: OrganizationUserApiService, - protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - ) {} - - async setPassword(credentials: SetPasswordCredentials): Promise { - const { - newMasterKey, - newServerMasterKeyHash, - newLocalMasterKeyHash, - newPasswordHint, - kdfConfig, - orgSsoIdentifier, - orgId, - resetPasswordAutoEnroll, - userId, - } = credentials; - - for (const [key, value] of Object.entries(credentials)) { - if (value == null) { - throw new Error(`${key} not found. Could not set password.`); - } - } - - const protectedUserKey = await this.makeProtectedUserKey(newMasterKey, userId); - if (protectedUserKey == null) { - throw new Error("protectedUserKey not found. Could not set password."); - } - - // Since this is an existing JIT provisioned user in a MP encryption org setting first password, - // they will not already have a user asymmetric key pair so we must create it for them. - const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey); - - const request = new SetPasswordRequest( - newServerMasterKeyHash, - protectedUserKey[1].encryptedString, - newPasswordHint, - orgSsoIdentifier, - keysRequest, - kdfConfig.kdfType, - kdfConfig.iterations, - ); - - await this.masterPasswordApiService.setPassword(request); - - // Clear force set password reason to allow navigation back to vault. - await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); - - // User now has a password so update account decryption options in state - await this.updateAccountDecryptionProperties(newMasterKey, kdfConfig, protectedUserKey, userId); - - await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId); - - await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId); - - if (resetPasswordAutoEnroll) { - await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId); - } - } - - private async makeProtectedUserKey( - masterKey: MasterKey, - userId: UserId, - ): Promise<[UserKey, EncString]> { - let protectedUserKey: [UserKey, EncString] = null; - - const userKey = await firstValueFrom(this.keyService.userKey$(userId)); - - if (userKey == null) { - protectedUserKey = await this.keyService.makeUserKey(masterKey); - } else { - protectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey); - } - - return protectedUserKey; - } - - private async makeKeyPairAndRequest( - protectedUserKey: [UserKey, EncString], - ): Promise<[[string, EncString], KeysRequest]> { - const keyPair = await this.keyService.makeKeyPair(protectedUserKey[0]); - if (keyPair == null) { - throw new Error("keyPair not found. Could not set password."); - } - const keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); - - return [keyPair, keysRequest]; - } - - private async updateAccountDecryptionProperties( - masterKey: MasterKey, - kdfConfig: KdfConfig, - protectedUserKey: [UserKey, EncString], - userId: UserId, - ) { - const userDecryptionOpts = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, - ); - userDecryptionOpts.hasMasterPassword = true; - await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - await this.kdfConfigService.setKdfConfig(userId, kdfConfig); - await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.keyService.setUserKey(protectedUserKey[0], userId); - } - - private async handleResetPasswordAutoEnroll( - masterKeyHash: string, - orgId: string, - userId: UserId, - ) { - const organizationKeys = await this.organizationApiService.getKeys(orgId); - - if (organizationKeys == null) { - throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); - } - - const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey); - - // RSA Encrypt user key with organization public key - const userKey = await firstValueFrom(this.keyService.userKey$(userId)); - - if (userKey == null) { - throw new Error("userKey not found. Could not handle reset password auto enroll."); - } - - const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey); - - const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); - resetRequest.masterPasswordHash = masterKeyHash; - resetRequest.resetPasswordKey = encryptedUserKey.encryptedString; - - await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( - orgId, - userId, - resetRequest, - ); - } -} diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.html b/libs/auth/src/angular/set-password-jit/set-password-jit.component.html deleted file mode 100644 index 797f18732cb..00000000000 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.component.html +++ /dev/null @@ -1,24 +0,0 @@ - - - {{ "loading" | i18n }} - - - - - {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} - - - - diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts deleted file mode 100644 index 1a2674cd3d4..00000000000 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts +++ /dev/null @@ -1,135 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; - -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ToastService } from "../../../../components/src/toast"; -import { - InputPasswordComponent, - InputPasswordFlow, -} from "../input-password/input-password.component"; -import { PasswordInputResult } from "../input-password/password-input-result"; - -import { - SetPasswordCredentials, - SetPasswordJitService, -} from "./set-password-jit.service.abstraction"; - -@Component({ - selector: "auth-set-password-jit", - templateUrl: "set-password-jit.component.html", - imports: [CommonModule, InputPasswordComponent, JslibModule], -}) -export class SetPasswordJitComponent implements OnInit { - protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser; - protected email: string; - protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions; - protected orgId: string; - protected orgSsoIdentifier: string; - protected resetPasswordAutoEnroll: boolean; - protected submitting = false; - protected syncLoading = true; - protected userId: UserId; - - constructor( - private accountService: AccountService, - private activatedRoute: ActivatedRoute, - private i18nService: I18nService, - private organizationApiService: OrganizationApiServiceAbstraction, - private policyApiService: PolicyApiServiceAbstraction, - private router: Router, - private setPasswordJitService: SetPasswordJitService, - private syncService: SyncService, - private toastService: ToastService, - private validationService: ValidationService, - ) {} - - async ngOnInit() { - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - this.userId = activeAccount?.id; - this.email = activeAccount?.email; - - await this.syncService.fullSync(true); - this.syncLoading = false; - - await this.handleQueryParams(); - } - - private async handleQueryParams() { - const qParams = await firstValueFrom(this.activatedRoute.queryParams); - - if (qParams.identifier != null) { - try { - this.orgSsoIdentifier = qParams.identifier; - - const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus( - this.orgSsoIdentifier, - ); - this.orgId = autoEnrollStatus.id; - this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled; - this.masterPasswordPolicyOptions = - await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(autoEnrollStatus.id); - } catch { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("errorOccurred"), - }); - } - } - } - - protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { - this.submitting = true; - - const credentials: SetPasswordCredentials = { - newMasterKey: passwordInputResult.newMasterKey, - newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, - newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash, - newPasswordHint: passwordInputResult.newPasswordHint, - kdfConfig: passwordInputResult.kdfConfig, - orgSsoIdentifier: this.orgSsoIdentifier, - orgId: this.orgId, - resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, - userId: this.userId, - }; - - try { - await this.setPasswordJitService.setPassword(credentials); - } catch (e) { - this.validationService.showError(e); - this.submitting = false; - return; - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("accountSuccessfullyCreated"), - }); - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("inviteAccepted"), - }); - - this.submitting = false; - - await this.router.navigate(["vault"]); - } -} diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts deleted file mode 100644 index 92db88868a2..00000000000 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.service.abstraction.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { UserId } from "@bitwarden/common/types/guid"; -import { MasterKey } from "@bitwarden/common/types/key"; -import { KdfConfig } from "@bitwarden/key-management"; - -export interface SetPasswordCredentials { - newMasterKey: MasterKey; - newServerMasterKeyHash: string; - newLocalMasterKeyHash: string; - newPasswordHint: string; - kdfConfig: KdfConfig; - orgSsoIdentifier: string; - orgId: string; - resetPasswordAutoEnroll: boolean; - userId: UserId; -} - -/** - * This service handles setting a password for a "just-in-time" provisioned user. - * - * A "just-in-time" (JIT) provisioned user is a user who does not have a registered account at the - * time they first click "Login with SSO". Once they click "Login with SSO" we register the account on - * the fly ("just-in-time"). - */ -export abstract class SetPasswordJitService { - /** - * Sets the password for a JIT provisioned user. - * - * @param credentials An object of the credentials needed to set the password for a JIT provisioned user - * @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey - * or newKeyPair could not be created. - */ - abstract setPassword(credentials: SetPasswordCredentials): Promise; -} diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index 07b59ac661f..8acd6865b70 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -23,12 +23,10 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -118,7 +116,6 @@ export class SsoComponent implements OnInit { private toastService: ToastService, private ssoComponentService: SsoComponentService, private loginSuccessHandlerService: LoginSuccessHandlerService, - private configService: ConfigService, ) { environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; @@ -534,11 +531,7 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); - const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password-jit"; - + const route = "set-initial-password"; await this.router.navigate([route], { queryParams: { identifier: orgIdentifier, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index e7678102360..62271feee59 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -2,7 +2,7 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ActivatedRoute, Router, convertToParamMap } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { @@ -24,8 +24,10 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -66,7 +68,7 @@ describe("TwoFactorAuthComponent", () => { let mockLoginEmailService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; - let mockMasterPasswordService: FakeMasterPasswordService; + let mockMasterPasswordService: MockProxy; let mockAccountService: FakeAccountService; let mockDialogService: MockProxy; let mockToastService: MockProxy; @@ -107,7 +109,7 @@ describe("TwoFactorAuthComponent", () => { mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); mockAccountService = mockAccountServiceWith(userId); - mockMasterPasswordService = new FakeMasterPasswordService(); + mockMasterPasswordService = mock(); mockDialogService = mock(); mockToastService = mock(); mockTwoFactorAuthCompService = mock(); @@ -212,6 +214,7 @@ describe("TwoFactorAuthComponent", () => { }, { provide: AuthService, useValue: mockAuthService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: MasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, ], }); @@ -267,54 +270,16 @@ describe("TwoFactorAuthComponent", () => { selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword); }); - describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { - it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => { - // Arrange - mockConfigService.getFeatureFlag.mockResolvedValue(true); - - // Act - await component.submit("testToken"); - - // Assert - expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], { - queryParams: { - identifier: component.orgSsoIdentifier, - }, - }); - }); - }); - - describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => { - it("navigates to the /set-password route when user doesn't have a MP and key connector isn't enabled", async () => { - // Arrange - mockConfigService.getFeatureFlag.mockResolvedValue(false); - - // Act - await component.submit("testToken"); - - // Assert - expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], { - queryParams: { - identifier: component.orgSsoIdentifier, - }, - }); - }); - }); - }); - - describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { - it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => { + it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => { + // Arrange mockConfigService.getFeatureFlag.mockResolvedValue(true); - selectedUserDecryptionOptions.next( - mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, - ); + // Act + await component.submit("testToken"); - await component.submit(token, remember); - - expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], { + // Assert + expect(mockRouter.navigate).toHaveBeenCalledTimes(1); + expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], { queryParams: { identifier: component.orgSsoIdentifier, }, @@ -322,21 +287,19 @@ describe("TwoFactorAuthComponent", () => { }); }); - describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => { - it("does not navigate to the /set-password route when the user has key connector even if user has no master password", async () => { - mockConfigService.getFeatureFlag.mockResolvedValue(false); + it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); - selectedUserDecryptionOptions.next( - mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, - ); + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, + ); - await component.submit(token, remember); + await component.submit(token, remember); - expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], { - queryParams: { - identifier: component.orgSsoIdentifier, - }, - }); + expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, }); }); }); @@ -344,6 +307,9 @@ describe("TwoFactorAuthComponent", () => { it("navigates to the component's defined success route (vault is default) when the login is successful", async () => { mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked); + mockMasterPasswordService.forceSetPasswordReason$.mockReturnValue( + of(ForceSetPasswordReason.None), + ); // Act await component.submit("testToken"); @@ -409,7 +375,7 @@ describe("TwoFactorAuthComponent", () => { await component.submit(token, remember); // Assert - expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, userId, ); diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 50cc2d88d6a..07746cf6479 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -17,7 +17,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, - LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, TrustedDeviceUserDecryptionOption, UserDecryptionOptions, @@ -32,9 +31,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -156,7 +153,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private activatedRoute: ActivatedRoute, private logService: LogService, private twoFactorService: TwoFactorService, - private loginEmailService: LoginEmailServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction, private masterPasswordService: InternalMasterPasswordServiceAbstraction, @@ -171,7 +167,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private loginSuccessHandlerService: LoginSuccessHandlerService, private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService, private authService: AuthService, - private configService: ConfigService, ) {} async ngOnInit() { @@ -507,19 +502,15 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { } // TODO: PM-22663 use the new service to handle routing. - if ( - await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) - ) { - const forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(userId), - ); + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(userId), + ); - if ( - forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword || - forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset - ) { - return "change-password"; - } + if ( + forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword || + forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset + ) { + return "change-password"; } return "vault"; @@ -575,11 +566,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { } private async handleChangePasswordRequired(orgIdentifier: string | undefined) { - const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); - const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password"; - + const route = "set-initial-password"; await this.router.navigate([route], { queryParams: { identifier: orgIdentifier, diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 61a06f94b02..8e3867d1b36 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -12,7 +12,6 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { @@ -221,7 +220,10 @@ describe("PasswordLoginStrategy", () => { await passwordLoginStrategy.logIn(credentials); - expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); }); it("does not force the user to update their master password when it meets requirements", async () => { @@ -230,7 +232,10 @@ describe("PasswordLoginStrategy", () => { await passwordLoginStrategy.logIn(credentials); - expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); + expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); }); it("when given master password policies as part of the login credentials from an org invite, it combines them with the token response policies to evaluate the user's password as weak", async () => { @@ -242,12 +247,6 @@ describe("PasswordLoginStrategy", () => { policyService.evaluateMasterPassword.mockReturnValue(false); tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); - jest - .spyOn(configService, "getFeatureFlag") - .mockImplementation((flag: FeatureFlag) => - Promise.resolve(flag === FeatureFlag.PM16117_ChangeExistingPasswordRefactor), - ); - credentials.masterPasswordPoliciesFromOrgInvite = Object.assign( new MasterPasswordPolicyOptions(), { @@ -296,9 +295,16 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); - policyService.evaluateMasterPassword.mockReturnValue(false); tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); + const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), { + enforceOnLogin: true, + }); + policyService.combineMasterPasswordPolicyOptions.mockReturnValue( + combinedMasterPasswordPolicyOptions, + ); + policyService.evaluateMasterPassword.mockReturnValue(false); + await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); @@ -330,9 +336,16 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); - policyService.evaluateMasterPassword.mockReturnValue(false); tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); + const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), { + enforceOnLogin: true, + }); + policyService.combineMasterPasswordPolicyOptions.mockReturnValue( + combinedMasterPasswordPolicyOptions, + ); + policyService.evaluateMasterPassword.mockReturnValue(false); + const token2FAResponse = new IdentityTwoFactorResponse({ TwoFactorProviders: ["0"], TwoFactorProviders2: { 0: null }, diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index cd3d5df1d5e..3482e73d5d7 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -171,35 +170,22 @@ export class PasswordLoginStrategy extends LoginStrategy { return; } - // The identity result can contain master password policies for the user's organizations - let masterPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined; + // The identity result can contain master password policies for the user's organizations. + // Get the master password policy options from both the org invite and the identity response. + const masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions( + credentials.masterPasswordPoliciesFromOrgInvite, + this.getMasterPasswordPolicyOptionsFromResponse(identityResponse), + ); + // We deliberately do not check enforceOnLogin as existing users who are logging + // in after getting an org invite should always be forced to set a password that + // meets the org's policy. Org Invite -> Registration also works this way for + // new BW users as well. if ( - await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor) + !credentials.masterPasswordPoliciesFromOrgInvite && + !masterPasswordPolicyOptions?.enforceOnLogin ) { - // Get the master password policy options from both the org invite and the identity response. - masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions( - credentials.masterPasswordPoliciesFromOrgInvite, - this.getMasterPasswordPolicyOptionsFromResponse(identityResponse), - ); - - // We deliberately do not check enforceOnLogin as existing users who are logging - // in after getting an org invite should always be forced to set a password that - // meets the org's policy. Org Invite -> Registration also works this way for - // new BW users as well. - if ( - !credentials.masterPasswordPoliciesFromOrgInvite && - !masterPasswordPolicyOptions?.enforceOnLogin - ) { - return; - } - } else { - masterPasswordPolicyOptions = - this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); - - if (!masterPasswordPolicyOptions?.enforceOnLogin) { - return; - } + return; } // If there is a policy active, evaluate the supplied password before its no longer in memory diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 47a9d19f651..f057dc47c63 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -10,7 +10,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; @@ -83,6 +82,7 @@ describe("SsoLoginStrategy", () => { const ssoCodeVerifier = "SSO_CODE_VERIFIER"; const ssoRedirectUrl = "SSO_REDIRECT_URL"; const ssoOrgId = "SSO_ORG_ID"; + const privateKey = "userKeyEncryptedPrivateKey"; beforeEach(async () => { accountService = mockAccountServiceWith(userId); @@ -114,6 +114,9 @@ describe("SsoLoginStrategy", () => { tokenService.decodeAccessToken.mockResolvedValue({ sub: userId, }); + keyService.userEncryptedPrivateKey$ + .calledWith(userId) + .mockReturnValue(of(privateKey as EncryptedString)); const mockVaultTimeoutAction = VaultTimeoutAction.Lock; const mockVaultTimeoutActionBSub = new BehaviorSubject( @@ -163,6 +166,7 @@ describe("SsoLoginStrategy", () => { it("sends SSO information to server", async () => { apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); + keyService.hasUserKey.mockResolvedValue(true); await ssoLoginStrategy.logIn(credentials); @@ -185,6 +189,7 @@ describe("SsoLoginStrategy", () => { it("does not set keys for new SSO user flow", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.key = null; + tokenResponse.privateKey = null; apiService.postIdentityToken.mockResolvedValue(tokenResponse); await ssoLoginStrategy.logIn(credentials); @@ -210,42 +215,28 @@ describe("SsoLoginStrategy", () => { ); }); - describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { - beforeEach(() => { - configService.getFeatureFlag.mockImplementation(async (flag) => { - if (flag === FeatureFlag.PM16117_SetInitialPasswordRefactor) { - return true; - } - return false; - }); - }); + describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => { + it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => { + // Arrange + const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = { + HasMasterPassword: false, + TrustedDeviceOption: null, + KeyConnectorOption: null, + }; + const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions); + apiService.postIdentityToken.mockResolvedValue(tokenResponse); - describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => { - it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => { - // Arrange - const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = { - HasMasterPassword: false, - TrustedDeviceOption: null, - KeyConnectorOption: null, - }; - const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions); - apiService.postIdentityToken.mockResolvedValue(tokenResponse); + keyService.hasUserKey.mockResolvedValue(false); - keyService.userEncryptedPrivateKey$.mockReturnValue( - of("userKeyEncryptedPrivateKey" as EncryptedString), - ); - keyService.hasUserKey.mockResolvedValue(false); + // Act + await ssoLoginStrategy.logIn(credentials); - // Act - await ssoLoginStrategy.logIn(credentials); - - // Assert - expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1); - expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( - ForceSetPasswordReason.TdeOffboardingUntrustedDevice, - userId, - ); - }); + // Assert + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1); + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.TdeOffboardingUntrustedDevice, + userId, + ); }); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 8ab84f0968a..6f1231b3559 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -9,7 +9,6 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity- import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { HttpStatusCode } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -344,38 +343,18 @@ export class SsoLoginStrategy extends LoginStrategy { tokenResponse: IdentityTokenResponse, userId: UserId, ): Promise { - const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_SetInitialPasswordRefactor, - ); - - if (isSetInitialPasswordFlagOn) { - if (tokenResponse.hasMasterKeyEncryptedUserKey()) { - // User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey - // Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair - // and so we don't want them falling into the createKeyPairForOldAccount flow - await this.keyService.setPrivateKey( - tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), - userId, - ); - } else if (tokenResponse.privateKey) { - // User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey - // This is just existing TDE users or a TDE offboarder on an untrusted device - await this.keyService.setPrivateKey(tokenResponse.privateKey, userId); - } - // else { - // User could be new JIT provisioned SSO user in either a MP encryption org OR a TDE org. - // In either case, the user doesn't yet have a user asymmetric key pair, a user key, or a master key + master key encrypted user key. - // } - } else { - // A user that does not yet have a masterKeyEncryptedUserKey set is a new SSO user - const newSsoUser = tokenResponse.key == null; - - if (!newSsoUser) { - await this.keyService.setPrivateKey( - tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), - userId, - ); - } + if (tokenResponse.hasMasterKeyEncryptedUserKey()) { + // User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey + // Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair + // and so we don't want them falling into the createKeyPairForOldAccount flow + await this.keyService.setPrivateKey( + tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, + ); + } else if (tokenResponse.privateKey) { + // User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey + // This is just existing TDE users or a TDE offboarder on an untrusted device + await this.keyService.setPrivateKey(tokenResponse.privateKey, userId); } } @@ -431,30 +410,25 @@ export class SsoLoginStrategy extends LoginStrategy { // - UserDecryptionOptions.UsesKeyConnector is undefined. -- they aren't using key connector // - UserKey is not set after successful login -- because automatic decryption is not available // - userKeyEncryptedPrivateKey is set after successful login -- this is the key differentiator between a TDE org user logging into an untrusted device and MP encryption JIT provisioned user logging in for the first time. - const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag( - FeatureFlag.PM16117_SetInitialPasswordRefactor, + // Why is that the case? Because we set the userKeyEncryptedPrivateKey when we create the userKey, and this is serving as a proxy to tell us that the userKey has been created already (when enrolling in TDE). + const hasUserKeyEncryptedPrivateKey = await firstValueFrom( + this.keyService.userEncryptedPrivateKey$(userId), ); + const hasUserKey = await this.keyService.hasUserKey(userId); - if (isSetInitialPasswordFlagOn) { - const hasUserKeyEncryptedPrivateKey = await firstValueFrom( - this.keyService.userEncryptedPrivateKey$(userId), + // TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user. + if ( + !userDecryptionOptions.trustedDeviceOption && + !userDecryptionOptions.hasMasterPassword && + !userDecryptionOptions.keyConnectorOption?.keyConnectorUrl && + hasUserKeyEncryptedPrivateKey && + !hasUserKey + ) { + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.TdeOffboardingUntrustedDevice, + userId, ); - const hasUserKey = await this.keyService.hasUserKey(userId); - - // TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user. - if ( - !userDecryptionOptions.trustedDeviceOption && - !userDecryptionOptions.hasMasterPassword && - !userDecryptionOptions.keyConnectorOption?.keyConnectorUrl && - hasUserKeyEncryptedPrivateKey && - !hasUserKey - ) { - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.TdeOffboardingUntrustedDevice, - userId, - ); - return true; - } + return true; } // Check if user has permission to set password but hasn't yet diff --git a/libs/common/src/admin-console/services/policy/default-policy.service.ts b/libs/common/src/admin-console/services/policy/default-policy.service.ts index 798adf520f2..2d9518ee508 100644 --- a/libs/common/src/admin-console/services/policy/default-policy.service.ts +++ b/libs/common/src/admin-console/services/policy/default-policy.service.ts @@ -89,8 +89,7 @@ export class DefaultPolicyService implements PolicyService { const policies$ = policies ? of(policies) : this.policies$(userId); return policies$.pipe( map((obsPolicies) => { - // TODO: replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) once - // FeatureFlag.PM16117_ChangeExistingPasswordRefactor is removed. + // TODO ([PM-23777]): replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined; const filteredPolicies = obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? []; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 33ded4a22c8..ad61cb421cd 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -14,8 +14,6 @@ export enum FeatureFlag { CreateDefaultLocation = "pm-19467-create-default-location", /* Auth */ - PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor", - PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor", PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals", /* Autofill */ @@ -107,8 +105,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM22136_SdkCipherEncryption]: FALSE, /* Auth */ - [FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE, - [FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE, [FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE, /* Billing */ From cd33ea074713c3b45d414193d064a08e82acc3db Mon Sep 17 00:00:00 2001 From: Sunset Mikoto <26019675+SunsetMkt@users.noreply.github.com> Date: Fri, 25 Jul 2025 01:42:35 +0800 Subject: [PATCH 042/179] build(firefox): bump max file size limit to 5MB (#15477) https://github.com/mozilla/addons-linter/pull/5674 --- .github/workflows/build-browser.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index bd7d70e8543..be140b9a20e 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -269,19 +269,19 @@ jobs: # Declare variable as indexed array declare -a FILES - # Search for source files that are greater than 4M + # Search for source files that are greater than 5M TARGET_DIR='./browser-source/apps/browser' while IFS=' ' read -r RESULT; do FILES+=("$RESULT") - done < <(find $TARGET_DIR -size +4M) + done < <(find $TARGET_DIR -size +5M) # Validate results and provide messaging if [[ ${#FILES[@]} -ne 0 ]]; then - echo "File(s) exceeds size limit: 4MB" + echo "File(s) exceeds size limit: 5MB" for FILE in ${FILES[@]}; do echo "- $(du --si $FILE)" done - echo "ERROR Firefox rejects extension uploads that contain files larger than 4MB" + echo "ERROR Firefox rejects extension uploads that contain files larger than 5MB" # Invoke failure exit 1 fi From 7b85870e58d9e07ee78e00f35a417fa67a24e248 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:59:29 -0700 Subject: [PATCH 043/179] [PM-22377] - [Vault] [Clients] Update cipher form component to restrict editing old My Vault items (#15687) * disable cipher form for "My Items" ciphers * use correct property * prevent changing non org fields in cli for org owned vaults * update var name * fix tests * fix stories * revert changes to item details section. update comment in edit command * remove unused props * fix test * re-apply logic to enforce org ownership * re-apply logic to enforce org ownership * fix logic and test * add empty line to comment * remove unused var * delegate form enabling/disabling to cipherFormContainer * rename var and getter back to original. update comment --- apps/cli/src/commands/edit.command.ts | 15 ++++ apps/cli/src/oss-serve-configurator.ts | 1 + apps/cli/src/vault.program.ts | 1 + .../src/cipher-form/cipher-form-container.ts | 4 ++ .../components/cipher-form.component.ts | 8 +++ .../item-details-section.component.html | 2 +- .../item-details-section.component.spec.ts | 1 + .../item-details-section.component.ts | 68 ++++++++++++++----- 8 files changed, 81 insertions(+), 19 deletions(-) diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index ebf877011b7..6b8c5811056 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -4,6 +4,8 @@ import { firstValueFrom } from "rxjs"; import { CollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -36,6 +38,7 @@ export class EditCommand { private folderApiService: FolderApiServiceAbstraction, private accountService: AccountService, private cliRestrictedItemTypesService: CliRestrictedItemTypesService, + private policyService: PolicyService, ) {} async run( @@ -104,6 +107,18 @@ export class EditCommand { return Response.error("Editing this item type is restricted by organizational policy."); } + const isPersonalVaultItem = cipherView.organizationId == null; + + const organizationOwnershipPolicyApplies = await firstValueFrom( + this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, activeUserId), + ); + + if (isPersonalVaultItem && organizationOwnershipPolicyApplies) { + return Response.error( + "An organization policy restricts editing this cipher. Please use the share command first before modifying it.", + ); + } + const encCipher = await this.cipherService.encrypt(cipherView, activeUserId); try { const updatedCipher = await this.cipherService.updateWithServer(encCipher); diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 848627b703f..a460fa270a8 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -103,6 +103,7 @@ export class OssServeConfigurator { this.serviceContainer.folderApiService, this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, + this.serviceContainer.policyService, ); this.generateCommand = new GenerateCommand( this.serviceContainer.passwordGenerationService, diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 2b08bc67ec1..bdcc52393ca 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -285,6 +285,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.folderApiService, this.serviceContainer.accountService, this.serviceContainer.cliRestrictedItemTypesService, + this.serviceContainer.policyService, ); const response = await command.run(object, id, encodedJson, cmd); this.processResponse(response); diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index cef5e102afe..628b4c07f6c 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -70,4 +70,8 @@ export abstract class CipherFormContainer { /** Returns true when the `CipherFormContainer` was initialized with a cached cipher available. */ abstract initializedWithCachedCipher(): boolean; + + abstract disableFormFields(): void; + + abstract enableFormFields(): void; } diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index b8815235ee8..47ab9977f64 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -150,6 +150,14 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci } } + disableFormFields(): void { + this.cipherForm.disable({ emitEvent: false }); + } + + enableFormFields(): void { + this.cipherForm.enable({ emitEvent: false }); + } + /** * Registers a child form group with the parent form group. Used by child components to add their form groups to * the parent form for validation. diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index c61312c13eb..4d575634b1c 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -22,7 +22,7 @@ {{ "owner" | i18n }} diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index db8e2007c61..3c513a2f067 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -572,6 +572,7 @@ describe("ItemDetailsSectionComponent", () => { it("returns matching default when flag & policy match", async () => { const def = createMockCollection("def1", "Def", "orgA"); component.config.collections = [def] as CollectionView[]; + component.config.organizationDataOwnershipDisabled = false; component.config.initialValues = { collectionIds: [] } as OptionalInitialValues; mockConfigService.getFeatureFlag.mockResolvedValue(true); mockPolicyService.policiesByType$.mockReturnValue(of([{ organizationId: "orgA" } as Policy])); diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 1064980050f..4fd999ae601 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -19,7 +19,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CardComponent, @@ -80,6 +80,8 @@ export class ItemDetailsSectionComponent implements OnInit { protected organizations: Organization[] = []; + protected userId: UserId; + @Input({ required: true }) config: CipherFormConfig; @@ -96,7 +98,7 @@ export class ItemDetailsSectionComponent implements OnInit { return this.config.mode === "partial-edit"; } - get organizationDataOwnershipDisabled() { + get allowPersonalOwnership() { return this.config.organizationDataOwnershipDisabled; } @@ -109,16 +111,19 @@ export class ItemDetailsSectionComponent implements OnInit { } /** - * Show the organization data ownership option in the Owner dropdown when: - * - organization data ownership is disabled - * - The `organizationId` control is disabled. This avoids the scenario - * where a the dropdown is empty because the user personally owns the cipher - * but cannot edit the ownership. + * Show the personal ownership option in the Owner dropdown when any of the following: + * - personal ownership is allowed + * - `organizationId` control is disabled + * - personal ownership is not allowed AND the user is editing a cipher that is not + * currently owned by an organization */ - get showOrganizationDataOwnershipOption() { + get showPersonalOwnershipOption() { return ( - this.organizationDataOwnershipDisabled || - !this.itemDetailsForm.controls.organizationId.enabled + this.allowPersonalOwnership || + this.itemDetailsForm.controls.organizationId.disabled || + (!this.allowPersonalOwnership && + this.config.originalCipher && + this.itemDetailsForm.controls.organizationId.value === null) ); } @@ -170,7 +175,7 @@ export class ItemDetailsSectionComponent implements OnInit { } // If personal ownership is allowed and there is at least one organization, allow ownership change. - if (this.organizationDataOwnershipDisabled) { + if (this.allowPersonalOwnership) { return this.organizations.length > 0; } @@ -189,7 +194,7 @@ export class ItemDetailsSectionComponent implements OnInit { } get defaultOwner() { - return this.organizationDataOwnershipDisabled ? null : this.organizations[0].id; + return this.allowPersonalOwnership ? null : this.organizations[0].id; } async ngOnInit() { @@ -197,7 +202,9 @@ export class ItemDetailsSectionComponent implements OnInit { Utils.getSortFunction(this.i18nService, "name"), ); - if (!this.organizationDataOwnershipDisabled && this.organizations.length === 0) { + this.userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + if (!this.allowPersonalOwnership && this.organizations.length === 0) { throw new Error("No organizations available for ownership."); } @@ -216,43 +223,68 @@ export class ItemDetailsSectionComponent implements OnInit { }); await this.updateCollectionOptions(this.initialValues?.collectionIds); } + this.setFormState(); if (!this.allowOwnershipChange) { this.itemDetailsForm.controls.organizationId.disable(); } this.itemDetailsForm.controls.organizationId.valueChanges .pipe( takeUntilDestroyed(this.destroyRef), - concatMap(async () => await this.updateCollectionOptions()), + concatMap(async () => { + await this.updateCollectionOptions(); + this.setFormState(); + }), ) .subscribe(); } + /** + * When the cipher does not belong to an organization but the user's organization + * requires all ciphers to be owned by an organization, disable the entire form + * until the user selects an organization. + */ + private setFormState() { + if (this.config.originalCipher && !this.allowPersonalOwnership) { + if (this.itemDetailsForm.controls.organizationId.value === null) { + this.cipherFormContainer.disableFormFields(); + this.itemDetailsForm.controls.organizationId.enable(); + } else { + this.cipherFormContainer.enableFormFields(); + } + } + } + /** * Gets the default collection IDs for the selected organization. * Returns null if any of the following apply: * - the feature flag is disabled + * - the "no private data policy" doesn't apply to the user * - no org is currently selected * - the selected org doesn't have the "no private data policy" enabled */ private async getDefaultCollectionId(orgId?: OrganizationId) { - if (!orgId) { + if (!orgId || this.allowPersonalOwnership) { return; } + const isFeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.CreateDefaultLocation, ); + if (!isFeatureEnabled) { return; } - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const selectedOrgHasPolicyEnabled = ( await firstValueFrom( - this.policyService.policiesByType$(PolicyType.OrganizationDataOwnership, userId), + this.policyService.policiesByType$(PolicyType.OrganizationDataOwnership, this.userId), ) ).find((p) => p.organizationId); + if (!selectedOrgHasPolicyEnabled) { return; } + const defaultUserCollection = this.collections.find( (c) => c.organizationId === orgId && c.type === CollectionTypes.DefaultUserCollection, ); @@ -284,7 +316,7 @@ export class ItemDetailsSectionComponent implements OnInit { ); } - if (!this.organizationDataOwnershipDisabled && prefillCipher.organizationId == null) { + if (!this.allowPersonalOwnership && prefillCipher.organizationId == null) { this.itemDetailsForm.controls.organizationId.setValue(this.defaultOwner); } } From 4766efd938a9a59434e0492f75c7734fe23886f2 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Thu, 24 Jul 2025 15:05:40 -0400 Subject: [PATCH 044/179] [CL-803] Temporarily disable flaky popover test (#15761) --- .../src/stories/kitchen-sink/kitchen-sink.stories.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 9e7e6f5d3ba..671a8d9ad82 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -161,6 +161,11 @@ export const PopoverOpen: Story = { await userEvent.click(passwordLabelIcon); }, + parameters: { + chromatic: { + disableSnapshot: true, + }, + }, }; export const SimpleDialogOpen: Story = { From ebc6f9fea3fe063e2fcc9d874e8b35192582997e Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Thu, 24 Jul 2025 15:45:03 -0400 Subject: [PATCH 045/179] Refactor the Autotype Checkbox Name (#15753) * Refactor the autofill checkbox name * Refactor the autofill checkbox name one more time * Create transition key --- apps/desktop/src/app/accounts/settings.component.html | 2 +- apps/desktop/src/locales/en/messages.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 473cfa73f1d..4514a908bb9 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -495,7 +495,7 @@ formControlName="enableAutotype" (change)="saveEnableAutotype()" /> - {{ "enableAutotype" | i18n }} + {{ "enableAutotypeTransitionKey" | i18n }} Date: Thu, 24 Jul 2025 16:15:51 -0400 Subject: [PATCH 046/179] [CL-791] global text color change (#15723) * update variables to use same color as text-main * remove unused headers key from tailwind config --- apps/web/src/scss/variables.scss | 4 ++-- libs/components/src/variables.scss | 4 ++-- libs/components/tailwind.config.base.js | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/web/src/scss/variables.scss b/apps/web/src/scss/variables.scss index 66773999c54..6c5278b2f9a 100644 --- a/apps/web/src/scss/variables.scss +++ b/apps/web/src/scss/variables.scss @@ -18,7 +18,7 @@ $theme-colors: ( ); $body-bg: $white; -$body-color: #333333; +$body-color: #1b2029; $font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", @@ -201,7 +201,7 @@ $themes: ( textColor: $body-color, textDangerColor: $white, textInfoColor: $white, - textHeadingColor: #333333, + textHeadingColor: $body-color, textMuted: #6c757d, textSuccessColor: $white, textWarningColor: $white, diff --git a/libs/components/src/variables.scss b/libs/components/src/variables.scss index e3651f9c37d..724244d24be 100644 --- a/libs/components/src/variables.scss +++ b/libs/components/src/variables.scss @@ -18,7 +18,7 @@ $theme-colors: ( ); $body-bg: $white; -$body-color: #333333; +$body-color: #1b2029; $font-family-sans-serif: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", @@ -216,7 +216,7 @@ $themes: ( textColor: $body-color, textDangerColor: $white, textInfoColor: $white, - textHeadingColor: #333333, + textHeadingColor: $body-color, textMuted: #6c757d, textSuccessColor: $white, textWarningColor: $white, diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 8b73ffc470c..3c7437fb57f 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -74,7 +74,6 @@ module.exports = { contrast: rgba("--color-text-contrast"), alt2: rgba("--color-text-alt2"), code: rgba("--color-text-code"), - headers: rgba("--color-text-headers"), }, background: { DEFAULT: rgba("--color-background"), @@ -101,7 +100,6 @@ module.exports = { main: rgba("--color-text-main"), muted: rgba("--color-text-muted"), contrast: rgba("--color-text-contrast"), - headers: rgba("--color-text-headers"), alt2: rgba("--color-text-alt2"), code: rgba("--color-text-code"), black: colors.black, From 9826f0ab0abed77c40a9bc1f9ce027b2822fb95d Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Thu, 24 Jul 2025 16:20:57 -0400 Subject: [PATCH 047/179] Remove transition key (#15766) --- apps/desktop/src/app/accounts/settings.component.html | 2 +- apps/desktop/src/locales/en/messages.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 4514a908bb9..473cfa73f1d 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -495,7 +495,7 @@ formControlName="enableAutotype" (change)="saveEnableAutotype()" /> - {{ "enableAutotypeTransitionKey" | i18n }} + {{ "enableAutotype" | i18n }} Date: Fri, 25 Jul 2025 10:07:43 +0200 Subject: [PATCH 048/179] Autosync the updated translations (#15774) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 3 + apps/browser/src/_locales/az/messages.json | 3 + apps/browser/src/_locales/be/messages.json | 3 + apps/browser/src/_locales/bg/messages.json | 3 + apps/browser/src/_locales/bn/messages.json | 3 + apps/browser/src/_locales/bs/messages.json | 3 + apps/browser/src/_locales/ca/messages.json | 3 + apps/browser/src/_locales/cs/messages.json | 3 + apps/browser/src/_locales/cy/messages.json | 3 + apps/browser/src/_locales/da/messages.json | 3 + apps/browser/src/_locales/de/messages.json | 3 + apps/browser/src/_locales/el/messages.json | 3 + apps/browser/src/_locales/en_GB/messages.json | 3 + apps/browser/src/_locales/en_IN/messages.json | 3 + apps/browser/src/_locales/es/messages.json | 3 + apps/browser/src/_locales/et/messages.json | 3 + apps/browser/src/_locales/eu/messages.json | 3 + apps/browser/src/_locales/fa/messages.json | 3 + apps/browser/src/_locales/fi/messages.json | 193 ++++++++-------- apps/browser/src/_locales/fil/messages.json | 3 + apps/browser/src/_locales/fr/messages.json | 213 +++++++++--------- apps/browser/src/_locales/gl/messages.json | 3 + apps/browser/src/_locales/he/messages.json | 3 + apps/browser/src/_locales/hi/messages.json | 3 + apps/browser/src/_locales/hr/messages.json | 63 +++--- apps/browser/src/_locales/hu/messages.json | 3 + apps/browser/src/_locales/id/messages.json | 3 + apps/browser/src/_locales/it/messages.json | 3 + apps/browser/src/_locales/ja/messages.json | 3 + apps/browser/src/_locales/ka/messages.json | 3 + apps/browser/src/_locales/km/messages.json | 3 + apps/browser/src/_locales/kn/messages.json | 3 + apps/browser/src/_locales/ko/messages.json | 3 + apps/browser/src/_locales/lt/messages.json | 3 + apps/browser/src/_locales/lv/messages.json | 3 + apps/browser/src/_locales/ml/messages.json | 3 + apps/browser/src/_locales/mr/messages.json | 3 + apps/browser/src/_locales/my/messages.json | 3 + apps/browser/src/_locales/nb/messages.json | 3 + apps/browser/src/_locales/ne/messages.json | 3 + apps/browser/src/_locales/nl/messages.json | 3 + apps/browser/src/_locales/nn/messages.json | 3 + apps/browser/src/_locales/or/messages.json | 3 + apps/browser/src/_locales/pl/messages.json | 189 ++++++++-------- apps/browser/src/_locales/pt_BR/messages.json | 3 + apps/browser/src/_locales/pt_PT/messages.json | 3 + apps/browser/src/_locales/ro/messages.json | 3 + apps/browser/src/_locales/ru/messages.json | 3 + apps/browser/src/_locales/si/messages.json | 3 + apps/browser/src/_locales/sk/messages.json | 3 + apps/browser/src/_locales/sl/messages.json | 3 + apps/browser/src/_locales/sr/messages.json | 3 + apps/browser/src/_locales/sv/messages.json | 3 + apps/browser/src/_locales/te/messages.json | 3 + apps/browser/src/_locales/th/messages.json | 3 + apps/browser/src/_locales/tr/messages.json | 3 + apps/browser/src/_locales/uk/messages.json | 3 + apps/browser/src/_locales/vi/messages.json | 5 +- apps/browser/src/_locales/zh_CN/messages.json | 25 +- apps/browser/src/_locales/zh_TW/messages.json | 3 + 60 files changed, 515 insertions(+), 335 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index d7aef05ab92..65ee9cab458 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "البحث في الخزانة" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "تعديل" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 5e7bf056980..16b74ffe175 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Seyfdə axtar" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Düzəliş et" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index a49899eaee0..d7a1db3adc8 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Пошук у сховішчы" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Рэдагаваць" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 672e029a662..79e13cdb677 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Търсене в трезора" }, + "resetSearch": { + "message": "Нулиране на търсенето" + }, "edit": { "message": "Редактиране" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 4e30612b9a6..a3c029fb963 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "ভল্ট খুঁজুন" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "সম্পাদনা" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index be64d0bade5..8a94ba3e9e9 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index f6c40da1096..42fb9c24003 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Cerca en la caixa forta" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edita" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 4e0096a1520..3f8dd2e2b48 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Vyhledat v trezoru" }, + "resetSearch": { + "message": "Resetovat hledání" + }, "edit": { "message": "Upravit" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 1235b49dd2c..307373da9aa 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Chwilio'r gell" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Golygu" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index bc34810f97f..4b6da81a994 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Søg i boks" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Redigér" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 2e3d9369c41..9ef82a6d5ae 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Tresor durchsuchen" }, + "resetSearch": { + "message": "Suche zurücksetzen" + }, "edit": { "message": "Bearbeiten" }, diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 014d17b74c8..fa4f3ac0f3c 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Αναζήτηση στο vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Επεξεργασία" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index a17a48e95b8..a70fbd85123 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 9f383c2f0e3..39de06249fc 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 35a28528f49..3b681054abc 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Buscar en caja fuerte" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index daadcbf00e9..8b61aa70a60 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Otsi hoidlast" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Muuda" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index e5f836fcaae..73bd992dacb 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Bilatu kutxa gotorrean" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Editatu" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index e551e96f74a..18afcb775f9 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "جستجوی گاوصندوق" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "ویرایش" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 22f2046bae3..894b50b5273 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Etsi holvista" }, + "resetSearch": { + "message": "Nollaa haku" + }, "edit": { "message": "Muokkaa" }, @@ -887,7 +890,7 @@ "message": "Viimeistele kirjautuminen seuraamalla seuraavia vaiheita." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Seuraa alla olevia ohjeita, jotta pääset kirjautumaan suojausavaimellasi." }, "restartRegistration": { "message": "Aloita rekisteröityminen alusta" @@ -1063,7 +1066,7 @@ "message": "Tallenna" }, "notificationViewAria": { - "message": "View $ITEMNAME$, opens in new window", + "message": "Näytä $ITEMNAME$. Avautuu uudessa ikkunassa", "placeholders": { "itemName": { "content": "$1" @@ -1093,15 +1096,15 @@ } }, "notificationLoginSaveConfirmation": { - "message": "saved to Bitwarden.", + "message": "tallennettu Bitwardeniin.", "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { - "message": "updated in Bitwarden.", + "message": "päivitetty Bitwardeniin.", "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "Valitse $ITEMTYPE$, $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1121,7 +1124,7 @@ "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Avaa tallentaaksesi tämä kirjautumistieto", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { @@ -1174,10 +1177,10 @@ "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { - "message": "After changing your password, you will need to log in with your new password. Active sessions on other devices will be logged out within one hour." + "message": "Kun olet vaihtanut salasanaasi, sinun täytyy kirjautua sisään uudella salasanalla. Aktiiviset istunnot muilla laitteilla kirjataan ulos tunnin kuluessa." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Change your master password to complete account recovery." + "message": "Vaihda pääsalasanasi, jotta voit jatkaa tilin palautusta." }, "enableChangedPasswordNotification": { "message": "Kysy päivitetäänkö kirjautumistieto" @@ -1372,7 +1375,7 @@ "message": "Ominaisuus ei ole käytettävissä" }, "legacyEncryptionUnsupported": { - "message": "Legacy encryption is no longer supported. Please contact support to recover your account." + "message": "Vanhaa salausta ei enää tueta. Ota yhteyttä tukeen palauttaaksesi tilisi." }, "premiumMembership": { "message": "Premium-jäsenyys" @@ -1606,10 +1609,10 @@ "message": "Automaattitäytön ehdotukset" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "Löydä helposti automaattisen täytön ehdotukset" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "Poista käytöstä selaimesi oletuksena asetetut automaattisen täytön asetukset, joten ne eivät aiheuta ongelmia Bitwardenin kanssa." }, "turnOffBrowserAutofill": { "message": "Poista automaattitäyttö käytöstä selaimessa $BROWSER$", @@ -1830,7 +1833,7 @@ "message": "Turvakoodi (CVC/CVV)" }, "cardNumber": { - "message": "card number" + "message": "kortin numero" }, "ex": { "message": "esim." @@ -1932,7 +1935,7 @@ "message": "SSH-avain" }, "typeNote": { - "message": "Note" + "message": "Muistiinpano" }, "newItemHeader": { "message": "Uusi $TYPE$", @@ -2166,7 +2169,7 @@ "message": "Aseta PIN-koodi Bitwardenin avaukselle. PIN-asetukset tyhjentyvät, jos kirjaudut laajennuksesta kokonaan ulos." }, "setPinCode": { - "message": "You can use this PIN to unlock Bitwarden. Your PIN will be reset if you ever fully log out of the application." + "message": "Voit käyttää PIN-koodia avataksesi Bitwardenin. PIN-koodisi nollataan, mikäli kirjaudut täysin ulos sovelluksesta." }, "pinRequired": { "message": "PIN-koodi vaaditaan." @@ -2497,10 +2500,10 @@ "message": "Organisaatiokäytäntö estää kohteiden tuonnin yksityiseen holviisi." }, "restrictCardTypeImport": { - "message": "Cannot import card item types" + "message": "Ei voitu tuoda kortteja" }, "restrictCardTypeImportDesc": { - "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + "message": "Käytäntö, jonka on asettanut 1 tai useampi organisaatiosi estää sinua tuomasta korttitietoja holviisi." }, "domainsTitle": { "message": "Verkkotunnukset", @@ -2547,7 +2550,7 @@ } }, "atRiskPassword": { - "message": "At-risk password" + "message": "Riskialttiit salasanat" }, "atRiskPasswords": { "message": "Vaarantuneet salasanat" @@ -2584,7 +2587,7 @@ } }, "atRiskChangePrompt": { - "message": "Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.", + "message": "Salasanasi tälle sivustolle ei ole turvallinen. $ORGANIZATION$ on ilmoittanut, että se tulisi vaihtaa.", "placeholders": { "organization": { "content": "$1", @@ -2594,7 +2597,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.", + "message": "$ORGANIZATION$ ovat pyytäneet, että vaihdat tämän salasnaan, sillä se ei ole turvallinen. Mene tilin asetuksiin ja vaihda salasana.", "placeholders": { "organization": { "content": "$1", @@ -2723,7 +2726,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Max access count reached", + "message": "Käyttökertojen enimmäismäärä on saavutettu", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { @@ -2929,7 +2932,7 @@ "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi ominaisuutta. Voit vahvistaa osoitteesi verkkoholvissa." }, "masterPasswordSuccessfullySet": { - "message": "Master password successfully set" + "message": "Pääsalasana asetettu" }, "updatedMasterPassword": { "message": "Pääsalasanasi on vaihdettu" @@ -3070,13 +3073,13 @@ "message": "Yksilöllistä tunnistetta ei löytynyt." }, "removeMasterPasswordForOrganizationUserKeyConnector": { - "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + "message": "Pääsalasanaa ei enää tarvita tämän organisaation jäsenille. Ole hyvä ja vahvista alla oleva verkkotunnus organisaation ylläpitäjän kanssa." }, "organizationName": { "message": "Organisaation nimi" }, "keyConnectorDomain": { - "message": "Key Connector domain" + "message": "Key Connector URL" }, "leaveOrganization": { "message": "Poistu organisaatiosta" @@ -3464,7 +3467,7 @@ "message": "Pyyntö lähetetty" }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "Kirjautuminen hyväksytty tunnuksella $EMAIL$ laitteella $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3477,13 +3480,13 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "Kirjautumispyyntö on estetty toiselta laitteelta. Jos se olit sinä, yritä kirjautua uudelleen samalla laitteella." }, "device": { - "message": "Device" + "message": "Laite" }, "loginStatus": { - "message": "Login status" + "message": "Kirjautumisen tila" }, "masterPasswordChanged": { "message": "Pääsalasana tallennettiin" @@ -3582,53 +3585,53 @@ "message": "Muista tämä laite tehdäksesi tulevista kirjautumisista saumattomia" }, "manageDevices": { - "message": "Manage devices" + "message": "Hallinnoi laitteita" }, "currentSession": { - "message": "Current session" + "message": "Nykyinen istunto" }, "mobile": { - "message": "Mobile", + "message": "Mobiili", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "Laajennus", "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "Työpöytä", "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "Verkkoholvi" }, "webApp": { - "message": "Web app" + "message": "Verkkosovellus" }, "cli": { - "message": "CLI" + "message": "Komentorivi" }, "sdk": { "message": "SDK", "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "Pyyntö odottaa" }, "firstLogin": { - "message": "First login" + "message": "Ensimmäinen kirjautuminen" }, "trusted": { - "message": "Trusted" + "message": "Luotettu" }, "needsApproval": { - "message": "Needs approval" + "message": "Vaatii hyväksynnän" }, "devices": { - "message": "Devices" + "message": "Laitteet" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Kirjautumisyritys sähköpostilla $EMAIL$ ", "placeholders": { "email": { "content": "$1", @@ -3637,28 +3640,28 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Hyväksy pääsy" }, "denyAccess": { - "message": "Deny access" + "message": "Estä pääsy" }, "time": { - "message": "Time" + "message": "Aika" }, "deviceType": { - "message": "Device Type" + "message": "Laitteen tyyppi" }, "loginRequest": { - "message": "Login request" + "message": "Kirjautumispyyntö" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "Tämä pyyntö ei ole enää voimassa." }, "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" + "message": "Yritätkö kirjautua tilillesi?" }, "logInConfirmedForEmailOnDevice": { - "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "message": "Kirjautuminen vahvistettu tunnuksella $EMAIL$ laitteella $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3671,16 +3674,16 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + "message": "Estit toisen laitteen lähettämän kirjautumispyynnön. Jos kuitenkin tunnistit kirjautumisyrityksen, suorita kirjautuminen uudelleen." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "Kirjautumispyyntö on jo vanhentunut." }, "justNow": { - "message": "Just now" + "message": "juuri nyt" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "pyydetty $MINUTES$ minuuttia sitten", "placeholders": { "minutes": { "content": "$1", @@ -3710,10 +3713,10 @@ "message": "Pyydä hyväksyntää ylläpidolta" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "Kirjautuminen epäonnistui" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "You need to log in on a trusted device or ask your administrator to assign you a password." + "message": "Sinun on kirjauduttava luotettuun laitteeseen tai pyydettävä järjestelmänvalvojaasi antamaan sinulle salasana." }, "ssoIdentifierRequired": { "message": "Organisaation kertakirjautumistunniste tarvitaan." @@ -3789,23 +3792,23 @@ "message": "Organisaatio ei ole luotettu" }, "emergencyAccessTrustWarning": { - "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + "message": "Tilisi turvallisuuden varmistamiseksi, vahvista vain, jos olet antanut hätäpääsyn tälle käyttäjälle ja hänen sormenjälkensä vastaa sitä, mitä hänen tilillään näkyy" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "Tilisi turvallisuuden takaamiseksi jatka vain, jos olet tämän organisaation jäsen, tilin palautus on käytössä ja alla näkyvä sormenjälki vastaa organisaatiosi sormenjälkeä." }, "orgTrustWarning1": { - "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + "message": "Tällä organisaatiolla on yrityskäytäntö, joka tulee ilmi, kun yrität palauttaa tiliäsi. Ilmoittautuminen sallii organisaation ylläpitäjien vaihtaa salasanasi. Jatka vain, jos tunnistat tämän organisaation ja alla näkyvän sormenjäljen, joka vastaa organisaation sormenjälkeä." }, "trustUser": { "message": "Luota käyttäjään" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "Lähetä arkaluonteisia tietoja turvallisesti", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "Jaa tiedostoja ja dataa turvallisesti kenen tahansa kanssa millä tahansa alustalla. Tiedot pysyvät päästä päähän salattuina.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4395,23 +4398,23 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "URI:n Säännöllinen lauseke -asetus on se tapa, jolla Bitwarden tekee automaattisen täytön.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "message": "Säänöllinen lauseke -asetus on kehittynyt asetus, joka lisää kirjautumistietoja kaappausriskiä.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "message": "Alkaa sanoilla -asetus on kehittynyt asetus, joka lisää kirjautumistietoja kaappausriskiä.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "Lisätietoa vastaavuustunnistuksesta", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Advanced options", + "message": "Lisäasetukset", "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { @@ -4598,7 +4601,7 @@ } }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Kopioi $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4754,16 +4757,16 @@ "message": "Hanki mobiilisovellus" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "Pääse käsiksi salasanoihisi, jos et pääse tietokoneen ääreen Bitwarden-mobiilisovelluksella." }, "getTheDesktopApp": { "message": "Hanki työpöytäsovellus" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "Käytä holviasi ilman selainta ja aseta sitten lukitus biometriikan avulla nopeuttaaksesi lukituksen avaamista sekä työpöytäsovelluksessa että selaimessa." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Lataa bitwarden.comista nyt" }, "getItOnGooglePlay": { "message": "Hanki se Google Playstä" @@ -5233,16 +5236,16 @@ "message": "Biometrinen avaus ei ole tällä hetkellä käytettävissä tuntemattomasta syystä." }, "unlockVault": { - "message": "Unlock your vault in seconds" + "message": "Avaa holvisi lukitus sekunneissa" }, "unlockVaultDesc": { - "message": "You can customize your unlock and timeout settings to more quickly access your vault." + "message": "Voit muokata avaus- ja aikakatkaisuasetuksiasi päästäksesi holvisi nopeammin käsiksi." }, "unlockPinSet": { - "message": "Unlock PIN set" + "message": "PIN asetettu" }, "unlockWithBiometricSet": { - "message": "Unlock with biometrics set" + "message": "Biometrinen kirjautuminen otettu käyttöön" }, "authenticating": { "message": "Todennetaan" @@ -5464,7 +5467,7 @@ "message": "Holvin asetukset" }, "emptyVaultDescription": { - "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + "message": "Holvi suojaa muutakin kuin salasanojasi. Säilytä kirjautumsitietojen lisäksi kortteja, muistiinpanoja ja henkilötietoja turvallisesti." }, "introCarouselLabel": { "message": "Tervetuloa Bitwardeniin" @@ -5500,7 +5503,7 @@ "message": "Tuo olemassa olevat salasanat" }, "emptyVaultNudgeBody": { - "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + "message": "Käytä tuojaa siirtääksesi kirjautumisia nopeasti Bitwardeniin lisäämättä niitä manuaalisesti." }, "emptyVaultNudgeButton": { "message": "Tuo nyt" @@ -5509,19 +5512,19 @@ "message": "Tervetuloa holviisi!" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "Täytä nykyisen sivun kohteet automaattisesti" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "Suosikkikohteita helppoon käyttöön" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "Etsi holvistasi jotain muuta" }, "newLoginNudgeTitle": { "message": "Säästä aikaa automaattitäytöllä" }, "newLoginNudgeBodyOne": { - "message": "Include a", + "message": "Sisällytä a", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, @@ -5531,63 +5534,63 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyTwo": { - "message": "so this login appears as an autofill suggestion.", + "message": ", joten tämä kirjautuminen näkyy automaattisen täytön ehdotuksena.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newCardNudgeTitle": { - "message": "Seamless online checkout" + "message": "Saumaton verkkomaksaminen" }, "newCardNudgeBody": { - "message": "With cards, easily autofill payment forms securely and accurately." + "message": "Korteilla voit helposti täyttää automaattisesti maksulomakkeet turvallisesti ja oikein." }, "newIdentityNudgeTitle": { - "message": "Simplify creating accounts" + "message": "Yksinkertaista tilien luomista" }, "newIdentityNudgeBody": { - "message": "With identities, quickly autofill long registration or contact forms." + "message": "Identiteettien avulla täytä pitkät rekisteröinti- tai yhteydenottolomakkeet nopeasti automaattisesti." }, "newNoteNudgeTitle": { - "message": "Keep your sensitive data safe" + "message": "Pidä arkaluonteiset tietosi turvassa" }, "newNoteNudgeBody": { - "message": "With notes, securely store sensitive data like banking or insurance details." + "message": "Muistiinpanojen avulla tallennetaan turvallisesti arkaluonteiset tiedot, kuten pankkitiedot." }, "newSshNudgeTitle": { - "message": "Developer-friendly SSH access" + "message": "Kehittäjäystävällinen SSH-käyttö" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "Säilytä avaimet ja yhdistä SSH-agenttiin, niin saat nopean ja salatun todennuksen.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "Lue lisää SSH-agentista", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Luo salasanat nopeasti" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "Luo helposti vahvoja ja uniikkeja salasanoja klikkaamalla", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": "to help you keep your logins secure.", + "message": "auttaakssi sinua pitämään kirjautumisesi turvassa.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Luo helposti vahvoja ja uniikkeja salasanoja klikkaamalla Luo salasana -painiketta. Sen avuilla voit pitää kirjautumisesi turvallisina.", "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "Sinulla ei ole oikeuksia tähän sivuun. Yritä kirjautua sisään toisella tilillä." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "WebAssembly ei ole tuettu selaimessasi tai se ei ole käytössä. WebAssembly vaaditaan, jotta voi käyttää Bitwardenia.", "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 88610d6874c..dfc8d65cfd9 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Hanapin ang vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "I-edit" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 680a19f33cc..c35f4ca4bb9 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Rechercher dans le coffre" }, + "resetSearch": { + "message": "Réinitialiser la recherche" + }, "edit": { "message": "Modifier" }, @@ -1063,7 +1066,7 @@ "message": "Enregistrer" }, "notificationViewAria": { - "message": "View $ITEMNAME$, opens in new window", + "message": "Afficher $ITEMNAME$, s'ouvre dans une nouvelle fenêtre", "placeholders": { "itemName": { "content": "$1" @@ -1076,14 +1079,14 @@ "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { - "message": "Edit before saving", + "message": "Modifier avant d'enregistrer", "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { "message": "Nouvelle notification" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: Nouvelle notification", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1101,7 +1104,7 @@ "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "Sélectionner $ITEMTYPE$, $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1141,7 +1144,7 @@ "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "Bon travail ! Vous avez pris les mesures pour rendre vous et $ORGANIZATION$ plus sécurisés.", "placeholders": { "organization": { "content": "$1" @@ -1150,7 +1153,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "Merci d'avoir rendu $ORGANIZATION$ plus sécurisé. Il vous reste $TASK_COUNT$ mots de passe à mettre à jour.", "placeholders": { "organization": { "content": "$1" @@ -1174,10 +1177,10 @@ "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { - "message": "After changing your password, you will need to log in with your new password. Active sessions on other devices will be logged out within one hour." + "message": "Après avoir changé votre mot de passe, vous devrez vous connecter avec votre nouveau mot de passe. Les sessions actives sur d'autres appareils seront déconnectées dans l'heure qui suit." }, "accountRecoveryUpdateMasterPasswordSubtitle": { - "message": "Change your master password to complete account recovery." + "message": "Changez votre mot de passe principal pour finaliser la récupération de compte." }, "enableChangedPasswordNotification": { "message": "Demander de mettre à jour un identifiant existant" @@ -1372,7 +1375,7 @@ "message": "Fonctionnalité indisponible" }, "legacyEncryptionUnsupported": { - "message": "Legacy encryption is no longer supported. Please contact support to recover your account." + "message": "Le chiffrement hérité n'est plus pris en charge. Veuillez contacter le support pour récupérer votre compte." }, "premiumMembership": { "message": "Adhésion Premium" @@ -1606,10 +1609,10 @@ "message": "Suggestions de saisie automatique" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "Trouver facilement des suggestions de remplissage automatique" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "Désactivez les paramètres de remplissage automatique de votre navigateur pour qu'ils n'entrent pas en conflit avec Bitwarden." }, "turnOffBrowserAutofill": { "message": "Désactiver le remplissage automatique de $BROWSER$", @@ -1830,7 +1833,7 @@ "message": "Code de sécurité" }, "cardNumber": { - "message": "card number" + "message": "numéro de carte" }, "ex": { "message": "ex." @@ -2166,7 +2169,7 @@ "message": "Définissez votre code PIN pour déverrouiller Bitwarden. Les paramètres relatifs à votre code PIN seront réinitialisés si vous vous déconnectez complètement de l'application." }, "setPinCode": { - "message": "You can use this PIN to unlock Bitwarden. Your PIN will be reset if you ever fully log out of the application." + "message": "Vous pouvez utiliser ce code PIN pour déverrouiller Bitwarden. Votre code PIN sera réinitialisé si vous vous déconnectez complètement de l'application." }, "pinRequired": { "message": "Le code PIN est requis." @@ -2217,7 +2220,7 @@ "message": "Utiliser ce mot de passe" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "Utilisez cette phrase secrète" }, "useThisUsername": { "message": "Utiliser ce nom d'utilisateur" @@ -2497,10 +2500,10 @@ "message": "Une politique d'organisation a bloqué l'import d'éléments dans votre coffre personel." }, "restrictCardTypeImport": { - "message": "Cannot import card item types" + "message": "Impossible d'importer des types d'éléments de carte" }, "restrictCardTypeImportDesc": { - "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + "message": "Une politique définie par 1 ou plusieurs organisations vous empêche d'importer des cartes dans vos coffres." }, "domainsTitle": { "message": "Domaines", @@ -2584,7 +2587,7 @@ } }, "atRiskChangePrompt": { - "message": "Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.", + "message": "Votre mot de passe pour ce site est à risque. $ORGANIZATION$ vous a demandé de le modifier.", "placeholders": { "organization": { "content": "$1", @@ -2594,7 +2597,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.", + "message": "$ORGANIZATION$ souhaite que vous changiez ce mot de passe car il est à risque. Accédez aux paramètres de votre compte pour modifier le mot de passe.", "placeholders": { "organization": { "content": "$1", @@ -2632,14 +2635,14 @@ "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { - "message": "Illustration of a list of logins that are at-risk." + "message": "Illustration d'une liste de connexions à risque." }, "generatePasswordSlideDesc": { "message": "Générez rapidement un mot de passe fort et unique grâce au menu de saisie automatique de Bitwarden sur le site à risque.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password." + "message": "Illustration du menu de remplissage automatique de Bitwarden affichant un mot de passe généré." }, "updateInBitwarden": { "message": "Mettre à jour dans Bitwarden" @@ -2649,7 +2652,7 @@ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." + "message": "Illustration d'une notification de Bitwarden invitant l'utilisateur à mettre à jour la connexion." }, "turnOnAutofill": { "message": "Activer la saisie automatique" @@ -2723,7 +2726,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Max access count reached", + "message": "Nombre maximal d'accès atteint", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { @@ -2929,7 +2932,7 @@ "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité. Vous pouvez vérifier votre courriel dans le coffre web." }, "masterPasswordSuccessfullySet": { - "message": "Master password successfully set" + "message": "Mot de passe principal défini avec succès" }, "updatedMasterPassword": { "message": "Mot de passe principal mis à jour" @@ -3070,13 +3073,13 @@ "message": "Aucun identifiant unique trouvé." }, "removeMasterPasswordForOrganizationUserKeyConnector": { - "message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator." + "message": "Un mot de passe principal n’est plus requis pour les membres de l’organisation suivante. Veuillez confirmer le domaine ci-dessous auprès de l'administrateur de votre organisation." }, "organizationName": { "message": "Nom de l'organisation" }, "keyConnectorDomain": { - "message": "Key Connector domain" + "message": "Domaine du connecteur clé" }, "leaveOrganization": { "message": "Quitter l'organisation" @@ -3112,7 +3115,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "Seuls les éléments individuels du coffre-fort, y compris les pièces jointes associées à $EMAIL$, seront exportés. Les éléments du coffre-fort de l'organisation ne seront pas inclus", "placeholders": { "email": { "content": "$1", @@ -3464,7 +3467,7 @@ "message": "Demande envoyée" }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "Demande de connexion approuvée pour $EMAIL$ sur $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3477,16 +3480,16 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "Vous avez refusé une tentative de connexion depuis un autre appareil. Si c'était vous, essayez de vous reconnecter avec l'appareil." }, "device": { - "message": "Device" + "message": "Appareil" }, "loginStatus": { - "message": "Login status" + "message": "Statut de connexion" }, "masterPasswordChanged": { - "message": "Master password saved" + "message": "Mot de passe principal enregistré" }, "exposedMasterPassword": { "message": "Mot de passe principal exposé" @@ -3582,10 +3585,10 @@ "message": "Mémorisez cet appareil pour faciliter les futures connexions" }, "manageDevices": { - "message": "Manage devices" + "message": "Gérer les appareils" }, "currentSession": { - "message": "Current session" + "message": "Session en cours" }, "mobile": { "message": "Mobile", @@ -3596,14 +3599,14 @@ "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "Bureau", "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "Coffre-fort Web" }, "webApp": { - "message": "Web app" + "message": "Application web" }, "cli": { "message": "CLI" @@ -3613,22 +3616,22 @@ "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "Demande en attente" }, "firstLogin": { - "message": "First login" + "message": "Première connexion" }, "trusted": { - "message": "Trusted" + "message": "Approuvé" }, "needsApproval": { - "message": "Needs approval" + "message": "Requiert une approbation" }, "devices": { - "message": "Devices" + "message": "Appareils" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Tentative d'accès par $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -3637,28 +3640,28 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Confirmer l'accès" }, "denyAccess": { - "message": "Deny access" + "message": "Refuser l'accès" }, "time": { - "message": "Time" + "message": "Temps" }, "deviceType": { - "message": "Device Type" + "message": "Type d'appareil" }, "loginRequest": { - "message": "Login request" + "message": "Demande de connexion" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "Cette demande n'est plus valide." }, "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" + "message": "Essayez-vous d'accéder à votre compte ?" }, "logInConfirmedForEmailOnDevice": { - "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "message": "Connexion confirmée pour $EMAIL$ sur $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3671,16 +3674,16 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + "message": "Vous avez refusé une tentative de connexion depuis un autre appareil. Si c'était vraiment vous, essayez de vous reconnecter avec l'appareil." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "La demande de connexion a déjà expiré." }, "justNow": { - "message": "Just now" + "message": "À l’instant" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Demandé $MINUTES$ il y a quelques minutes", "placeholders": { "minutes": { "content": "$1", @@ -3710,10 +3713,10 @@ "message": "Demander l'approbation de l'administrateur" }, "unableToCompleteLogin": { - "message": "Unable to complete login" + "message": "Impossible de terminer la connexion" }, "loginOnTrustedDeviceOrAskAdminToAssignPassword": { - "message": "You need to log in on a trusted device or ask your administrator to assign you a password." + "message": "Vous devez vous connecter sur un appareil de confiance ou demander à votre administrateur de vous attribuer un mot de passe." }, "ssoIdentifierRequired": { "message": "Identifiant SSO de l'organisation requis." @@ -3786,26 +3789,26 @@ "message": "Ne pas faire confiance" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "L'organisation n'est pas fiable" }, "emergencyAccessTrustWarning": { - "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + "message": "Pour la sécurité de votre compte, ne confirmez que si vous avez accordé un accès d'urgence à cet utilisateur et que son empreinte digitale correspond à ce qui est affiché dans son compte" }, "orgTrustWarning": { - "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + "message": "Pour la sécurité de votre compte, ne procédez que si vous êtes membre de cette organisation, que la récupération de compte est activée et que l'empreinte digitale affichée ci-dessous correspond à l'empreinte digitale de l'organisation." }, "orgTrustWarning1": { - "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." + "message": "Cette organisation dispose d’une politique d’entreprise qui vous inscrira au recouvrement de compte. L'inscription permettra aux administrateurs de l'organisation de modifier votre mot de passe. Ne continuez que si vous reconnaissez cette organisation et que la phrase d'empreinte digitale affichée ci-dessous correspond à l'empreinte digitale de l'organisation." }, "trustUser": { "message": "Faire confiance à l'utilisateur" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "Envoyez des informations sensibles en toute sécurité", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "Partagez des fichiers et des données en toute sécurité avec n'importe qui, sur n'importe quelle plateforme. Vos informations resteront cryptées de bout en bout tout en limitant l'exposition.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -4395,19 +4398,19 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "La détection de correspondance d'URI est la manière dont Bitwarden identifie les suggestions de remplissage automatique.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "message": "\"Expression régulière\" est une option avancée présentant un risque accru d’exposition des informations d’identification.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "message": "\"Commence par\" est une option avancée présentant un risque accru d’exposition des informations d’identification.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "En savoir plus sur la détection des correspondances", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { @@ -4560,7 +4563,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "Afficher l'élément - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4584,7 +4587,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "Remplissage automatique - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4598,7 +4601,7 @@ } }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Copiez $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4754,19 +4757,19 @@ "message": "Télécharger l'application mobile" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "Accédez à vos mots de passe en déplacement avec l'application mobile Bitwarden." }, "getTheDesktopApp": { "message": "Télécharger l'application de bureau" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "Accédez à votre coffre-fort sans navigateur, puis configurez le déverrouillage avec la biométrie pour accélérer le déverrouillage à la fois dans l'application de bureau et dans l'extension du navigateur." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Téléchargez maintenant depuis bitwarden.com" }, "getItOnGooglePlay": { - "message": "Get it on Google Play" + "message": "Obtenez-le sur Google Play" }, "downloadOnTheAppStore": { "message": "Télécharger depuis l’App Store" @@ -5236,13 +5239,13 @@ "message": "Déverouillez votre coffre en quelques secondes" }, "unlockVaultDesc": { - "message": "You can customize your unlock and timeout settings to more quickly access your vault." + "message": "Vous pouvez personnaliser vos paramètres de déverrouillage et de délai d'attente pour accéder plus rapidement à votre coffre-fort." }, "unlockPinSet": { - "message": "Unlock PIN set" + "message": "Déverrouiller l'ensemble de codes PIN" }, "unlockWithBiometricSet": { - "message": "Unlock with biometrics set" + "message": "Déverrouiller avec l'ensemble biométrique" }, "authenticating": { "message": "Authentification" @@ -5256,7 +5259,7 @@ "description": "Notification message for when a password has been regenerated" }, "saveToBitwarden": { - "message": "Save to Bitwarden", + "message": "Enregistrer dans Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { @@ -5464,7 +5467,7 @@ "message": "Options du coffre" }, "emptyVaultDescription": { - "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + "message": "Le coffre-fort protège bien plus que vos mots de passe. Stockez ici en toute sécurité des identifiants, des cartes d'identité, des cartes et des notes sécurisés." }, "introCarouselLabel": { "message": "Bienvenue sur Bitwarden" @@ -5473,34 +5476,34 @@ "message": "Priorité à la sécurité" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Enregistrez les identifiants, les cartes et les identités dans votre coffre-fort sécurisé. Bitwarden utilise un cryptage de bout en bout à connaissance nulle pour protéger ce qui est important pour vous." }, "quickLogin": { "message": "Connexion rapide et facile" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Configurez le déverrouillage biométrique et le remplissage automatique pour vous connecter à vos comptes sans taper une seule lettre." }, "secureUser": { - "message": "Level up your logins" + "message": "Améliorez vos connexions" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Utilisez le générateur pour créer et enregistrer des mots de passe forts et uniques pour tous vos comptes." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Vos données, quand et où vous en avez besoin" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Enregistrez des mots de passe illimités sur un nombre illimité d'appareils avec les applications mobiles, de navigateur et de bureau Bitwarden." }, "nudgeBadgeAria": { "message": "1 notification" }, "emptyVaultNudgeTitle": { - "message": "Import existing passwords" + "message": "Importer des mots de passe existants" }, "emptyVaultNudgeBody": { - "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + "message": "Utilisez l'importateur pour transférer rapidement les connexions vers Bitwarden sans les ajouter manuellement." }, "emptyVaultNudgeButton": { "message": "Importer maintenant" @@ -5509,19 +5512,19 @@ "message": "Bienvenue dans votre coffre !" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "Remplissage automatique des éléments de la page actuelle" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "Articles préférés pour un accès facile" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "Recherchez autre chose dans votre coffre-fort" }, "newLoginNudgeTitle": { "message": "Gagnez du temps avec le remplissage automatique" }, "newLoginNudgeBodyOne": { - "message": "Include a", + "message": "Inclure un", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, @@ -5531,33 +5534,33 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyTwo": { - "message": "so this login appears as an autofill suggestion.", + "message": "cette connexion apparaît donc comme une suggestion de remplissage automatique.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, "newCardNudgeTitle": { - "message": "Seamless online checkout" + "message": "Paiement en ligne transparent" }, "newCardNudgeBody": { - "message": "With cards, easily autofill payment forms securely and accurately." + "message": "Avec les cartes, remplissez facilement et automatiquement les formulaires de paiement de manière sécurisée et précise." }, "newIdentityNudgeTitle": { - "message": "Simplify creating accounts" + "message": "Simplifiez la création de comptes" }, "newIdentityNudgeBody": { - "message": "With identities, quickly autofill long registration or contact forms." + "message": "Avec les identités, remplissez rapidement automatiquement les longs formulaires d'inscription ou de contact." }, "newNoteNudgeTitle": { - "message": "Keep your sensitive data safe" + "message": "Gardez vos données sensibles en sécurité" }, "newNoteNudgeBody": { - "message": "With notes, securely store sensitive data like banking or insurance details." + "message": "Avec des notes, stockez en toute sécurité des données sensibles telles que des informations bancaires ou d’assurance." }, "newSshNudgeTitle": { - "message": "Developer-friendly SSH access" + "message": "Accès SSH convivial pour les développeurs" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "Stockez vos clés et connectez-vous à l'agent SSH pour une authentification rapide et cryptée.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, @@ -5567,27 +5570,27 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Créez rapidement des mots de passe" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "Créez facilement des mots de passe forts et uniques en cliquant sur", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": "to help you keep your logins secure.", + "message": "pour vous aider à garder vos connexions sécurisées.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Créez facilement des mots de passe forts et uniques en cliquant sur le bouton Générer un mot de passe pour vous aider à sécuriser vos connexions.", "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "Vous n'avez pas les autorisations pour consulter cette page. Essayez de vous connecter avec un autre compte." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "WebAssembly n'est pas pris en charge sur votre navigateur ou n'est pas activé. WebAssembly est requis pour utiliser l'application Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 559d0ca82b3..c7d47b61209 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Buscar na caixa forte" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index e4d959785bf..c13bcad10c6 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "חיפוש בכספת" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "ערוך" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 4fd2652d786..c6ae5e33c6b 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "वॉल्ट खोजे" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "संपादन करें" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 07c3e20cd18..97bd79f2950 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Pretraži trezor" }, + "resetSearch": { + "message": "Ponovno postavljanje pretraživanja" + }, "edit": { "message": "Uredi" }, @@ -1830,7 +1833,7 @@ "message": "Sigurnosni kôd" }, "cardNumber": { - "message": "card number" + "message": "broj kartice" }, "ex": { "message": "npr." @@ -3464,7 +3467,7 @@ "message": "Zahtjev poslan" }, "loginRequestApprovedForEmailOnDevice": { - "message": "Login request approved for $EMAIL$ on $DEVICE$", + "message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3477,13 +3480,13 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "Odbijena je prijava na drugom uređaju. Ako si ovo stvarno ti, pokušaj se ponovno prijaviti uređajem." }, "device": { - "message": "Device" + "message": "Uređaj" }, "loginStatus": { - "message": "Login status" + "message": "Status prijave" }, "masterPasswordChanged": { "message": "Glavna lozinka promijenjena" @@ -3582,17 +3585,17 @@ "message": "Zapamti ovaj uređaj kako bi buduće prijave bile brže" }, "manageDevices": { - "message": "Manage devices" + "message": "Upravljaj uređajima" }, "currentSession": { - "message": "Current session" + "message": "Trenutna sesija" }, "mobile": { - "message": "Mobile", + "message": "Mobitel", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "Proširenje", "description": "Browser extension/addon" }, "desktop": { @@ -3600,10 +3603,10 @@ "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "Web trezor" }, "webApp": { - "message": "Web app" + "message": "Web aplikacija" }, "cli": { "message": "CLI" @@ -3613,22 +3616,22 @@ "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "Zahtjev u tijeku" }, "firstLogin": { - "message": "First login" + "message": "Prva prijava" }, "trusted": { - "message": "Trusted" + "message": "Pouzdan" }, "needsApproval": { - "message": "Needs approval" + "message": "Zahtijeva odobrenje" }, "devices": { - "message": "Devices" + "message": "Uređaji" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "$EMAIL$ pokušava pristupiti", "placeholders": { "email": { "content": "$1", @@ -3637,28 +3640,28 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Potvrdi pristup" }, "denyAccess": { - "message": "Deny access" + "message": "Odbij pristup" }, "time": { - "message": "Time" + "message": "Vrijeme" }, "deviceType": { - "message": "Device Type" + "message": "Vrsta uređaja" }, "loginRequest": { - "message": "Login request" + "message": "Zahtjev za prijavu" }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "Ovaj zahtjev više nije valjan." }, "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" + "message": "Pokušavaš li pristupiti svom računu?" }, "logInConfirmedForEmailOnDevice": { - "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -3671,16 +3674,16 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + "message": "Odbijena je prijava na drugom uređaju. Ako si ovo stvarno ti, pokušaj se ponovno prijaviti uređajem." }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "Zahtjev za prijavu je već istekao." }, "justNow": { - "message": "Just now" + "message": "Upravo" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Zatraženo prije $MINUTES$ minute/a", "placeholders": { "minutes": { "content": "$1", @@ -4598,7 +4601,7 @@ } }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Kopiraj $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index b77d613da51..70c053ed148 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Keresés a széfben" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Szerkesztés" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 07e094e10bd..4ce58ee7618 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Cari brankas" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 167cc51c0b1..164d46bcec5 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Cerca nella cassaforte" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Modifica" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 49d22bd065f..977d04ca28a 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "保管庫を検索" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "編集" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 1e75805638c..da70c255803 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "ჩასწორება" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 9a6d9a4d316..f4c58318bc1 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index c7a821de19b..31b285f6780 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "ವಾಲ್ಟ್ ಹುಡುಕಿ" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "ಎಡಿಟ್" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 730d3eeda61..05043de2905 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "보관함 검색" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "편집" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 29815c9de82..5a4f0d8c419 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Ieškoti saugykloje" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Keisti" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index ba43b1e5f44..81447182f26 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Meklēt glabātavā" }, + "resetSearch": { + "message": "Atiestatīt meklēšanu" + }, "edit": { "message": "Labot" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index a39fe07b2c6..11709a4611b 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "വാൾട് തിരയുക" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "തിരുത്തുക" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index c79b8b322a7..e7d06e4d5f9 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "तिजोरीत शोधा" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 9a6d9a4d316..f4c58318bc1 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 01353cac5e0..cf3589c4fc9 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Søk i hvelvet" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Rediger" }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 9a6d9a4d316..f4c58318bc1 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index b5220861652..0c2bd31935f 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Kluis doorzoeken" }, + "resetSearch": { + "message": "Zoekopdracht resetten" + }, "edit": { "message": "Bewerken" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 9a6d9a4d316..f4c58318bc1 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 9a6d9a4d316..f4c58318bc1 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index aa8ab76d543..26f4cbaa5d6 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -26,7 +26,7 @@ "message": "Nowy w Bitwarden?" }, "logInWithPasskey": { - "message": "Logowaniem kluczem dostępu" + "message": "Logowanie kluczem dostępu" }, "useSingleSignOn": { "message": "Użyj logowania jednokrotnego" @@ -96,7 +96,7 @@ "message": "Dołącz do organizacji" }, "joinOrganizationName": { - "message": "Dołącz do $ORGANIZATIONNAME$", + "message": "Dołącz do organizacji $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -547,6 +547,9 @@ "searchVault": { "message": "Szukaj w sejfie" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edytuj" }, @@ -741,10 +744,10 @@ "message": "4 godziny" }, "onLocked": { - "message": "Po zablokowaniu komputera" + "message": "Po zablokowaniu urządzenia" }, "onRestart": { - "message": "Po restarcie przeglądarki" + "message": "Po uruchomieniu przeglądarki" }, "never": { "message": "Nigdy" @@ -860,7 +863,7 @@ "message": "Wylogowano" }, "loggedOutDesc": { - "message": "Zostałeś wylogowany z konta." + "message": "Wylogowano z konta." }, "loginExpired": { "message": "Twoja sesja wygasła." @@ -890,13 +893,13 @@ "message": "Wykonaj poniższe kroki, aby zakończyć logowanie za pomocą klucza bezpieczeństwa." }, "restartRegistration": { - "message": "Zacznij rejestrację od początku" + "message": "Rozpocznij rejestrację od początku" }, "expiredLink": { "message": "Link wygasł" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Zrestartuj rejestrację lub spróbuj się zalogować." + "message": "Rozpocznij rejestrację od początku lub spróbuj się zalogować." }, "youMayAlreadyHaveAnAccount": { "message": "Możesz mieć już konto" @@ -926,7 +929,7 @@ "message": "Logowanie dwustopniowe zwiększa bezpieczeństwo konta, wymagając weryfikacji logowania za pomocą innego urządzenia, takiego jak klucz bezpieczeństwa, aplikacja uwierzytelniająca, wiadomość SMS, połączenie telefoniczne lub wiadomość e-mail. Logowanie dwustopniowe możesz skonfigurować w sejfie internetowym bitwarden.com. Czy chcesz przejść do strony?" }, "twoStepLoginConfirmationContent": { - "message": "Spraw, aby Twoje konto było bezpieczniejsze poprzez skonfigurowanie dwustopniowego logowania w aplikacji internetowej Bitwarden." + "message": "Zwiększ bezpieczeństwo konta, konfigurując logowanie dwustopniowe w aplikacji internetowej Bitwarden." }, "twoStepLoginConfirmationTitle": { "message": "Przejść do aplikacji internetowej?" @@ -1031,7 +1034,7 @@ "message": "Pokaż karty na stronie głównej" }, "showCardsCurrentTabDesc": { - "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." + "message": "Wyświetla karty na głównej karcie sejfu." }, "showIdentitiesInVaultViewV2": { "message": "Pokazuj zawsze tożsamości w sugestiach autouzupełniania" @@ -1040,7 +1043,7 @@ "message": "Pokaż tożsamości na stronie głównej" }, "showIdentitiesCurrentTabDesc": { - "message": "Pokaż elementy tożsamości na stronie głównej, aby ułatwić autouzupełnianie." + "message": "Wyświetla tożsamości na głównej karcie sejfu." }, "clickToAutofillOnVault": { "message": "Kliknij na elementy, aby je uzupełnić" @@ -1129,7 +1132,7 @@ "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Zaktualizuj obecne dane logowania", + "message": "Zaktualizuj dane logowania", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { @@ -1287,7 +1290,7 @@ "message": "Potwierdź eksportowanie sejfu" }, "exportWarningDesc": { - "message": "Plik zawiera dane sejfu w niezaszyfrowanym formacie. Nie powinieneś go przechowywać, ani przesyłać poprzez niezabezpieczone kanały (takie jak poczta e-mail). Skasuj go natychmiast po użyciu." + "message": "Plik zawiera dane sejfu w niezaszyfrowanym formacie. Nie należy go przechowywać ani przesyłać poprzez niezabezpieczone kanały (takie jak poczta e-mail). Usuń go natychmiast po użyciu." }, "encExportKeyWarningDesc": { "message": "Dane eksportu zostaną zaszyfrowane za pomocą klucza szyfrowania konta. Jeśli kiedykolwiek zmienisz ten klucz, wyeksportuj dane ponownie, ponieważ nie będziesz w stanie odszyfrować tego pliku." @@ -1468,7 +1471,7 @@ "message": "Limit czasu uwierzytelniania" }, "authenticationSessionTimedOut": { - "message": "Upłynął limit czasu uwierzytelniania. Uruchom ponownie proces logowania." + "message": "Upłynął limit czasu uwierzytelniania. Zaloguj się ponownie." }, "verificationCodeEmailSent": { "message": "Wiadomość weryfikacyjna została wysłana na adres $EMAIL$.", @@ -1508,10 +1511,10 @@ "message": "Logowanie jest niedostępne" }, "noTwoStepProviders": { - "message": "Konto posiada włączoną opcję logowania dwustopniowego, jednak ta przeglądarka nie wspiera żadnego ze skonfigurowanych mechanizmów autoryzacji dwustopniowej." + "message": "Konto jest zabezpieczone logowaniem dwustopniowym, ale żadna ze skonfigurowanych metod nie jest obsługiwana w tej przeglądarce." }, "noTwoStepProviders2": { - "message": "Proszę użyć obsługiwanej przeglądarki (takiej jak Chrome) i/lub dodać dodatkowych dostawców, którzy są lepiej wspierani przez przeglądarki internetowe (np. aplikacja uwierzytelniająca)." + "message": "Użyj obsługiwanej przeglądarki (np. Chrome) lub dodaj dodatkowe opcje logowania dwustopniowego, które są obsługiwane na różnych przeglądarkach (takie jak aplikacja uwierzytelniająca)." }, "twoStepOptions": { "message": "Opcje logowania dwustopniowego" @@ -1609,7 +1612,7 @@ "message": "Łatwe wyszukiwanie sugestii autouzupełniania" }, "autofillSpotlightDesc": { - "message": "Wyłącz ustawienia autouzupełniania swojej przeglądarki, aby nie kolidowały z Bitwarden." + "message": "Wyłącz autouzupełnianie przeglądarki, aby uniknąć konfliktów z Bitwarden." }, "turnOffBrowserAutofill": { "message": "Wyłącz autouzupełnianie $BROWSER$", @@ -1645,15 +1648,15 @@ "message": "Edytuj ustawienia przeglądarki." }, "autofillOverlayVisibilityOff": { - "message": "Wył.", + "message": "Wyłączone", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "Gdy pole jest zaznaczone", + "message": "Po kliknięciu pola", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "Gdy wybrano ikonę autouzupełniania", + "message": "Po kliknięciu ikony autouzupełniania", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { @@ -1971,7 +1974,7 @@ "message": "Wyczyść historię generatora" }, "cleargGeneratorHistoryDescription": { - "message": "Jeśli zatwierdzisz, wszystkie wygenerowane hasła zostaną usunięte z historii generatora. Czy chcesz kontynuować mimo to?" + "message": "Wszystkie wpisy zostaną trwale usunięte z historii generatora. Czy na pewno chcesz kontynuować?" }, "back": { "message": "Wstecz" @@ -2102,7 +2105,7 @@ "message": "Usuń" }, "default": { - "message": "Domyślne" + "message": "Domyślna" }, "dateUpdated": { "message": "Zaktualizowano", @@ -2163,10 +2166,10 @@ "message": "Ustaw kod PIN" }, "setYourPinCode": { - "message": "Ustaw kod PIN do odblokowywania aplikacji Bitwarden. Ustawienia odblokowywania kodem PIN zostaną zresetowane po wylogowaniu." + "message": "Ustaw kod PIN do odblokowania aplikacji Bitwarden. Ustawienia kodu PIN zostaną zresetowane po wylogowaniu." }, "setPinCode": { - "message": "Możesz użyć tego kodu PIN, aby odblokować Bitwarden. Twój kod PIN zostanie zresetowany, jeśli kiedykolwiek wylogujesz się z aplikacji." + "message": "Możesz użyć tego kodu PIN do odblokowania aplikacji Bitwarden. Kod PIN zostanie zresetowany po wylogowaniu." }, "pinRequired": { "message": "Kod PIN jest wymagany." @@ -2193,7 +2196,7 @@ "message": "Zablokuj hasłem głównym po uruchomieniu przeglądarki" }, "lockWithMasterPassOnRestart1": { - "message": "Wymagaj hasła głównego przy ponownym uruchomieniu przeglądarki" + "message": "Wymagaj hasła głównego po uruchomieniu przeglądarki" }, "selectOneCollection": { "message": "Musisz wybrać co najmniej jedną kolekcję." @@ -2230,7 +2233,7 @@ "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": ", aby utworzyć mocne unikalne hasło", + "message": ", aby utworzyć silne i unikalne hasło", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultCustomization": { @@ -2296,10 +2299,10 @@ "message": "Czy chcesz uzupełnić dane logowania?" }, "autofillIframeWarning": { - "message": "Formularz jest hostowany przez inną domenę niż zapisany adres URI dla tego loginu. Wybierz OK, aby i tak automatycznie wypełnić lub anuluj, aby zatrzymać." + "message": "Formularz jest hostowany przez inną domenę niż URI zapisanych danych logowania. Kliknij OK, aby uzupełnić dane logowania lub Anuluj, aby zatrzymać." }, "autofillIframeWarningTip": { - "message": "Aby zapobiec temu ostrzeżeniu w przyszłości, zapisz ten URI, $HOSTNAME$, dla tej witryny.", + "message": "Aby uniknąć ostrzeżenia w przyszłości, zapisz URI $HOSTNAME$ w danych logowania.", "placeholders": { "hostname": { "content": "$1", @@ -2419,7 +2422,7 @@ "message": "Uruchom aplikację desktopową Bitwarden" }, "startDesktopDesc": { - "message": "Aplikacja desktopowa Bitwarden, przed odblokowaniem danymi biometrycznymi, musi zostać ponownie uruchomiona." + "message": "Aplikacja desktopowa Bitwarden musi zostać uruchomiona przed odblokowaniem za pomocą biometrii." }, "errorEnableBiometricTitle": { "message": "Nie można włączyć biometrii" @@ -2434,7 +2437,7 @@ "message": "Komunikacja z aplikacją desktopową została przerwana" }, "nativeMessagingWrongUserDesc": { - "message": "W aplikacji desktopowej jesteś zalogowany na inne konto. Upewnij się, że w obu aplikacjach jesteś zalogowany na to same konto." + "message": "Aplikacja desktopowa jest zalogowana na inne konto. Upewnij się, że obie aplikacje są zalogowane na to samo konto." }, "nativeMessagingWrongUserTitle": { "message": "Konto jest niezgodne" @@ -2443,7 +2446,7 @@ "message": "Klucz biometrii jest nieprawidłowy" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Odblokowanie biometryczne się nie powiodło. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie." + "message": "Odblokowanie biometrią nie powiodło się. Klucz biometrii nie odblokował sejfu. Spróbuj ponownie skonfigurować biometrię." }, "biometricsNotEnabledTitle": { "message": "Biometria jest wyłączona" @@ -2485,7 +2488,7 @@ "message": "Wystąpił błąd żądania uprawnienia" }, "nativeMessaginPermissionSidebarDesc": { - "message": "Ta operacja nie może zostać wykonana na pasku bocznym. Spróbuj ponownie w nowym oknie." + "message": "Akcji nie można wykonać na pasku bocznym. Otwórz rozszerzenie w oknie." }, "personalOwnershipSubmitError": { "message": "Ze względu na zasadę organizacji, nie możesz zapisywać elementów w osobistym sejfie. Zmień właściciela elementu na organizację i wybierz jedną z dostępnych kolekcji." @@ -2494,7 +2497,7 @@ "message": "Zasada organizacji ma wpływ na opcję własności elementów." }, "personalOwnershipPolicyInEffectImports": { - "message": "Polityka organizacji zablokowała importowanie elementów do Twojego sejfu." + "message": "Zasada organizacji zablokowała importowanie elementów do osobistego sejfu." }, "restrictCardTypeImport": { "message": "Nie można zaimportować karty" @@ -2645,7 +2648,7 @@ "message": "Zaktualizuj w Bitwarden" }, "updateInBitwardenSlideDesc": { - "message": "Bitwarden poprosi Cię o aktualizację hasła w menedżerze haseł.", + "message": "Bitwarden zaproponuje aktualizację hasła w menedżerze haseł.", "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { @@ -2887,7 +2890,7 @@ "message": "Aby wybrać plik, otwórz rozszerzenie w oknie." }, "popOut": { - "message": "Odepnij" + "message": "Otwórz w nowym oknie" }, "sendFileCalloutHeader": { "message": "Zanim zaczniesz" @@ -2960,7 +2963,7 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Uprawnienia w Twojej organizacji zostały zaktualizowane, musisz teraz ustawić hasło główne.", + "message": "Uprawnienia organizacji zostały zaktualizowane. Ustaw hasło główne.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { @@ -3243,7 +3246,7 @@ } }, "forwarderGeneratedBy": { - "message": "Wygenerowane przez Bitwarden.", + "message": "Wygenerowano przez Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { @@ -3365,7 +3368,7 @@ "message": "Klucz API" }, "ssoKeyConnectorError": { - "message": "Błąd serwera Key Connector: upewnij się, że serwer Key Connector jest dostępny i działa poprawnie." + "message": "Wystąpił błąd serwera Key Connector. Upewnij się, że serwer jest dostępny i działa poprawnie." }, "premiumSubcriptionRequired": { "message": "Wymagana jest subskrypcja premium" @@ -3395,7 +3398,7 @@ "message": "Inny dostawca" }, "thirdPartyServerMessage": { - "message": "Połączono z implementacją serwera innego dostawcy, $SERVERNAME$. Zweryfikuj błędy za pomocą oficjalnego serwera lub zgłoś je serwerowi innego dostawcy.", + "message": "Połączono z implementacją serwera innego dostawcy $SERVERNAME$. Zweryfikuj błędy za pomocą oficjalnego serwera lub zgłoś je serwerowi.", "placeholders": { "servername": { "content": "$1", @@ -3428,7 +3431,7 @@ "message": "Unikalny identyfikator konta" }, "fingerprintMatchInfo": { - "message": "Upewnij się, że sejf jest odblokowany, a unikalny identyfikator konta pasuje do drugiego urządzenia." + "message": "Upewnij się, że sejf jest odblokowany, a identyfikator konta pasuje do drugiego urządzenia." }, "resendNotification": { "message": "Wyślij ponownie powiadomienie" @@ -3492,7 +3495,7 @@ "message": "Hasło główne zostało ujawnione" }, "exposedMasterPasswordDesc": { - "message": "Hasło ujawnione w wyniku naruszenia ochrony danych. Użyj unikalnego hasła, aby chronić swoje konto. Czy na pewno chcesz użyć ujawnionego hasła?" + "message": "Hasło zostało ujawnione w wycieku danych. Użyj unikalnego hasła, aby chronić konto. Czy na pewno chcesz użyć ujawnionego hasła?" }, "weakAndExposedMasterPassword": { "message": "Hasło główne jest słabe i ujawnione" @@ -3501,7 +3504,7 @@ "message": "Hasło jest słabe i zostało ujawnione w wycieku danych. Użyj mocnego i unikalnego hasła, aby chronić konto. Czy na pewno chcesz użyć tego hasła?" }, "checkForBreaches": { - "message": "Sprawdź znane naruszenia ochrony danych tego hasła" + "message": "Sprawdź hasło w znanych wyciekach danych" }, "important": { "message": "Ważne:" @@ -3622,7 +3625,7 @@ "message": "Zaufane" }, "needsApproval": { - "message": "Wymagane potwierdzenie" + "message": "Potwierdzenie jest wymagane" }, "devices": { "message": "Urządzenia" @@ -3680,7 +3683,7 @@ "message": "Teraz" }, "requestedXMinutesAgo": { - "message": "Poproszono $MINUTES$ min temu", + "message": "$MINUTES$ min temu", "placeholders": { "minutes": { "content": "$1", @@ -3689,10 +3692,10 @@ } }, "deviceApprovalRequired": { - "message": "Wymagane zatwierdzenie urządzenia. Wybierz opcję zatwierdzenia poniżej:" + "message": "Potwierdzenie urządzenia jest wymagane. Wybierz opcję:" }, "deviceApprovalRequiredV2": { - "message": "Wymagane zatwierdzenie urządzenia" + "message": "Potwierdzenie urządzenia jest wymagane" }, "selectAnApprovalOptionBelow": { "message": "Wybierz opcję potwierdzenia" @@ -3725,7 +3728,7 @@ "message": "Sprawdź swoją pocztę e-mail" }, "followTheLinkInTheEmailSentTo": { - "message": "Kliknij łącze w wiadomości e-mail wysłanej do" + "message": "Kliknij link w wiadomości wysłanej na adres" }, "andContinueCreatingYourAccount": { "message": "i kontynuuj tworzenie konta." @@ -3792,7 +3795,7 @@ "message": "Dla bezpieczeństwa Twojego konta potwierdź tylko, jeśli przyznano temu użytkownikowi dostęp awaryjny i jego odcisk palca pasuje do tego, co widnieje na jego koncie" }, "orgTrustWarning": { - "message": "Dla zapewnienia bezpieczeństwa konta kontynuuj tylko wtedy, gdy jesteś członkiem tej organizacji, włączono odzyskiwanie konta, a odcisk palca wyświetlany poniżej pasuje do odcisku palca organizacji." + "message": "Kontynuuj tylko wtedy, gdy jesteś członkiem organizacji, masz włączone odzyskiwanie konta, a unikalny identyfikator pasuje do organizacji." }, "orgTrustWarning1": { "message": "Zasada organizacji pozwala administratorom organizacji na zmianę Twojego hasła. Kontynuuj tylko wtedy, gdy rozpoznajesz organizację, a unikalny identyfikator pasuje do organizacji." @@ -3958,7 +3961,7 @@ "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Odblokuj swoje konto, aby wyświetlić pasujące elementy", + "message": "Odblokuj konto, aby zobaczy pasujące dane logowania", "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { @@ -3970,7 +3973,7 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Odblokuj swoje konto, otwiera się w nowym oknie", + "message": "Odblokuj konto, otwiera się w nowym oknie", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { @@ -3978,7 +3981,7 @@ "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Pozostały czas do wygaśnięcia bieżącego TOTP", + "message": "Pozostały czas do wygaśnięcia kodu TOTP", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -4006,7 +4009,7 @@ "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Dodaj nowe dane logowania do sejfu, otwiera się w nowym oknie", + "message": "Dodaj nowe dane logowania, otwiera się w nowym oknie", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { @@ -4014,7 +4017,7 @@ "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Dodaj nową kartę do sejfu, otwiera się w nowym oknie", + "message": "Dodaj nową kartę, otwiera się w nowym oknie", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { @@ -4022,7 +4025,7 @@ "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Dodaj nową tożsamość do sejfu, otwiera się w nowym oknie", + "message": "Dodaj nową tożsamość, otwiera się w nowym oknie", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -4118,13 +4121,13 @@ "message": "Konto wymaga logowania dwustopniowego Duo." }, "popoutExtension": { - "message": "Otwórz rozszerzenie w nowym oknie" + "message": "Otwórz rozszerzenie w oknie" }, "launchDuo": { "message": "Uruchom Duo" }, "importFormatError": { - "message": "Dane nie są poprawnie sformatowane. Sprawdź importowany plik i spróbuj ponownie." + "message": "Dane nie są prawidłowo sformatowane. Sprawdź plik i spróbuj ponownie." }, "importNothingError": { "message": "Nic nie zostało zaimportowane." @@ -4133,7 +4136,7 @@ "message": "Wystąpił błąd podczas odszyfrowywania pliku. Klucz szyfrowania nie pasuje do klucza użytego podczas eksportowania danych." }, "invalidFilePassword": { - "message": "Hasło do pliku jest nieprawidłowe. Użyj hasła które podano przy tworzeniu pliku eksportu." + "message": "Hasło pliku jest nieprawidłowe. Użyj prawidłowego hasła." }, "destination": { "message": "Miejsce docelowe" @@ -4148,7 +4151,7 @@ "message": "Wybierz kolekcję" }, "importTargetHint": { - "message": "Wybierz tę opcję, jeśli chcesz, aby zawartość zaimportowanego pliku została przeniesiona do $DESTINATION$", + "message": "Wybierz tę opcję, jeśli chcesz przenieść dane do $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -4189,7 +4192,7 @@ "message": "Potwierdź importowanie sejfu" }, "confirmVaultImportDesc": { - "message": "Plik jest chroniony hasłem. Wprowadź hasło pliku, aby zaimportować dane." + "message": "Plik jest chroniony hasłem. Wpisz hasło pliku, aby zaimportować dane." }, "confirmFilePassword": { "message": "Potwierdź hasło pliku" @@ -4246,7 +4249,7 @@ "message": "Wybierz dane logowania, do których przypisać klucz dostępu" }, "chooseCipherForPasskeyAuth": { - "message": "Wybierz klucz dostępu, żeby się zalogować" + "message": "Wybierz klucz dostępu" }, "passkeyItem": { "message": "Element klucza dostępu" @@ -4261,16 +4264,16 @@ "message": "Funkcja nie jest jeszcze obsługiwana" }, "yourPasskeyIsLocked": { - "message": "Wymagane uwierzytelnienie, aby używać klucza dostępu. Sprawdź swoją tożsamość, aby kontynuować." + "message": "Aby użyć klucza dostępu, wymagane jest uwierzytelnienie. Zweryfikuj swoją tożsamość." }, "multifactorAuthenticationCancelled": { - "message": "Uwierzytelnianie wieloskładnikowe zostało anulowane" + "message": "Logowanie dwustopniowe zostało anulowane" }, "noLastPassDataFound": { "message": "Nie znaleziono danych LastPass" }, "incorrectUsernameOrPassword": { - "message": "Nieprawidłowa nazwa użytkownika lub hasło" + "message": "Nazwa użytkownika lub hasło są nieprawidłowe" }, "incorrectPassword": { "message": "Hasło jest nieprawidłowe" @@ -4282,7 +4285,7 @@ "message": "Kod PIN jest nieprawidłowy" }, "multifactorAuthenticationFailed": { - "message": "Uwierzytelnianie wieloskładnikowe nie powiodło się" + "message": "Logowanie dwustopniowe nie powiodło się" }, "includeSharedFolders": { "message": "Dołącz udostępnione foldery" @@ -4294,10 +4297,10 @@ "message": "Importowanie konta..." }, "lastPassMFARequired": { - "message": "Wymagane jest uwierzytelnianie wieloskładnikowe LastPass" + "message": "Logowanie dwustopniowe LastPass jest wymagane" }, "lastPassMFADesc": { - "message": "Wprowadź jednorazowy kod z aplikacji uwierzytelniającej" + "message": "Wpisz jednorazowy kod z aplikacji uwierzytelniającej" }, "lastPassOOBDesc": { "message": "Zatwierdź żądanie logowania w aplikacji uwierzytelniającej lub wprowadź jednorazowe hasło." @@ -4309,7 +4312,7 @@ "message": "Hasło główne LastPass" }, "lastPassAuthRequired": { - "message": "Wymagane uwierzytelnianie LastPass" + "message": "Uwierzytelnianie LastPass jest wymagane" }, "awaitingSSO": { "message": "Oczekiwanie na logowanie jednokrotne" @@ -4328,7 +4331,7 @@ "message": "Importuj z CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Spróbuj ponownie lub poszukaj wiadomości e-mail od LastPass, aby zweryfikować, że to Ty." + "message": "Spróbuj ponownie lub poszukaj wiadomości od LastPass, aby zweryfikować logowanie." }, "collection": { "message": "Kolekcja" @@ -4373,13 +4376,13 @@ "message": "hostowany w" }, "useDeviceOrHardwareKey": { - "message": "Użyj swojego urządzenia lub klucza sprzętowego" + "message": "Użyj urządzenia lub klucza sprzętowego" }, "justOnce": { "message": "Tylko raz" }, "alwaysForThisSite": { - "message": "Zawsze dla tej witryny" + "message": "Zawsze dla tej strony" }, "domainAddedToExcludedDomains": { "message": "Domena $DOMAIN$ została dodana do wykluczonych domen.", @@ -4411,7 +4414,7 @@ "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { - "message": "Ustawienia zaawansowane", + "message": "Opcje zaawansowane", "description": "Advanced option placeholder for uri option component" }, "confirmContinueToBrowserSettingsTitle": { @@ -4443,7 +4446,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignorowanie tej opcji może spowodować konflikty pomiędzy menu autouzupełniania Bitwarden a przeglądarką.", + "message": "Zignorowanie tej opcji może spowodować konflikty pomiędzy autouzupełnianiem Bitwarden a przeglądarką.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -4748,13 +4751,13 @@ "message": "Pobierz Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Pobierz Bitwarden na wszystkich urządzeniach" + "message": "Pobierz Bitwarden na wszystkie urządzenia" }, "getTheMobileApp": { "message": "Pobierz aplikację mobilną" }, "getTheMobileAppDesc": { - "message": "Uzyskaj dostęp do haseł przy pomocy aplikacji mobilnej Bitwarden." + "message": "Uzyskaj dostęp do haseł za pomocą aplikacji mobilnej Bitwarden." }, "getTheDesktopApp": { "message": "Pobierz aplikację desktopową" @@ -4787,7 +4790,7 @@ "message": "Filtruj sejf" }, "filterApplied": { - "message": "Zastosowano jeden filtr" + "message": "Zastosowano 1 filtr" }, "filterAppliedPlural": { "message": "$COUNT$ filtrów zastosowanych", @@ -4817,7 +4820,7 @@ } }, "cardNumberEndsWith": { - "message": "numer karty kończy się", + "message": "numer karty kończący się", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { @@ -4924,7 +4927,7 @@ "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Logowaniem kluczem dostępu", + "message": "Logowanie kluczem dostępu", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4964,16 +4967,16 @@ "message": "Użyj pól tekstowych dla danych takich jak pytania bezpieczeństwa" }, "hiddenHelpText": { - "message": "Użyj ukrytych pól dla danych poufnych, takich jak hasło" + "message": "Użyj ukrytych pól dla danych poufnych takich jak hasło" }, "checkBoxHelpText": { - "message": "Użyj pól wyboru, jeśli chcesz automatycznie wypełnić pole wyboru formularza, np. zapamiętaj e-mail" + "message": "Użyj pól wyboru, gdy chcesz uzupełnić pole wyboru formularza, np. zapamiętaj adres e-mail" }, "linkedHelpText": { "message": "Użyj powiązanego pola, gdy masz problemy z autouzupełnianiem na konkretnej stronie internetowej." }, "linkedLabelHelpText": { - "message": "Wprowadź atrybut z HTML'a: id, name, aria-label lub placeholder." + "message": "Wpisz identyfikator, nazwę, etykietę lub tekst zastępczy pola HTML." }, "editField": { "message": "Edytuj pole" @@ -5006,7 +5009,7 @@ } }, "reorderToggleButton": { - "message": "Zmień kolejność $LABEL$. Użyj klawiszy ze strzałkami, aby przenieść element w górę lub w dół.", + "message": "Zmień kolejność pola $LABEL$. Użyj klawiszy ze strzałkami, aby przenieść element w górę lub w dół.", "placeholders": { "label": { "content": "$1", @@ -5015,10 +5018,10 @@ } }, "reorderWebsiteUriButton": { - "message": "Zmień kolejność URI strony. Użyj klawiszy ze strzałkami, aby przenieść element w górę lub w dół." + "message": "Zmień kolejność URI stron internetowych. Użyj klawiszy ze strzałkami, aby przenieść element w górę lub w dół." }, "reorderFieldUp": { - "message": "$LABEL$ przeniósł się w górę, pozycja $INDEX$ z $LENGTH$", + "message": "Pole $LABEL$ zostało przeniesione w górę. Pozycja $INDEX$ z $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -5096,7 +5099,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ przeniósł się w dół, pozycja $INDEX$ z $LENGTH$", + "message": "Pole $LABEL$ zostało przeniesione w dół. Pozycja $INDEX$ z $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -5176,7 +5179,7 @@ "message": "Dostępna jest dodatkowa zawartość" }, "fileSavedToDevice": { - "message": "Plik zapisany na urządzeniu. Zarządzaj plikiem na swoim urządzeniu." + "message": "Plik został zapisany na urządzeniu." }, "showCharacterCount": { "message": "Pokaż liczbę znaków" @@ -5200,10 +5203,10 @@ "message": "Przywróć" }, "deleteForever": { - "message": "Usuń na zawsze" + "message": "Usuń trwale" }, "noEditPermissions": { - "message": "Nie masz uprawnień do edycji tego elementu" + "message": "Nie masz uprawnień do edycji elementu" }, "biometricsStatusHelptextUnlockNeeded": { "message": "Odblokowanie biometrią jest niedostępne, ponieważ najpierw wymagane jest odblokowanie kodem PIN lub hasłem." @@ -5410,10 +5413,10 @@ "message": "Szerokość rozszerzenia" }, "wide": { - "message": "Szerokie" + "message": "Szeroka" }, "extraWide": { - "message": "Bardzo szerokie" + "message": "Bardzo szeroka" }, "sshKeyWrongPassword": { "message": "Hasło jest nieprawidłowe." @@ -5461,10 +5464,10 @@ "message": "Zmień zagrożone hasło" }, "settingsVaultOptions": { - "message": "Ustawienia Sejfu" + "message": "Opcje sejfu" }, "emptyVaultDescription": { - "message": "Sejf chroni nie tylko Twoje hasła. Przechowuj tutaj bezpiecznie loginy, identyfikatory, karty i notatki." + "message": "Sejf chroni nie tylko hasła. Przechowuj bezpiecznie dane logowania, identyfikatory, karty i notatki." }, "introCarouselLabel": { "message": "Witaj w Bitwarden" @@ -5509,13 +5512,13 @@ "message": "Witaj w sejfie!" }, "hasItemsVaultNudgeBodyOne": { - "message": "Autouzupełnianie elementów dla bieżącej strony" + "message": "Uzupełniaj elementy na stronie internetowej" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Ulubione elementy dla szybkiego dostępu" + "message": "Dodawaj do ulubionych wybrane elementy" }, "hasItemsVaultNudgeBodyThree": { - "message": "Przeszukaj sejf w poszukiwaniu czegoś innego" + "message": "Przeszukuj sejf w poszukiwaniu czegoś innego" }, "newLoginNudgeTitle": { "message": "Oszczędzaj czas dzięki autouzupełnianiu" diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 7d5509a628b..46fbb9beca3 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Pesquisar no Cofre" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 1e77e1c3035..3cd813847d1 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Procurar no cofre" }, + "resetSearch": { + "message": "Repor pesquisa" + }, "edit": { "message": "Editar" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 7f54640af25..85b1436ed9e 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Căutare în seif" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Editare" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index db236cbfcc8..a5d3c6b54c4 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Поиск в хранилище" }, + "resetSearch": { + "message": "Сбросить поиск" + }, "edit": { "message": "Изменить" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 13e6c2522bf..7bc0ba2694a 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "සුරක්ෂිතාගාරය සොයන්න" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "සංස්කරණය" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e98c643edb9..28a687be339 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Prehľadávať trezor" }, + "resetSearch": { + "message": "Resetovať vyhľadávanie" + }, "edit": { "message": "Upraviť" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 397b7be54e8..2db40266cd7 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Išči v trezorju" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Uredi" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index f68d0b97447..f9534750cef 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Претражи сеф" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Уреди" }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index cbfc3e478f5..49d2765bf6e 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Sök i valvet" }, + "resetSearch": { + "message": "Nollställ sökning" + }, "edit": { "message": "Redigera" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 9a6d9a4d316..f4c58318bc1 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 49515eb1c64..fd92c71c200 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "ค้นหาในตู้นิรภัย" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "แก้ไข" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index cd7c8d3f0b6..2151d7385b7 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Kasada ara" }, + "resetSearch": { + "message": "Aramayı sıfırla" + }, "edit": { "message": "Düzenle" }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 083d89fbd12..16482b9f869 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Пошук" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "Змінити" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index b5de1c2981c..02660dcb38b 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "Tìm kiếm trong kho lưu trữ" }, + "resetSearch": { + "message": "Đặt lại tìm kiếm" + }, "edit": { "message": "Sửa" }, @@ -4312,7 +4315,7 @@ "message": "Yêu cầu xác thực LastPass" }, "awaitingSSO": { - "message": "Đang chờ xác thực Đăng nhập một lần" + "message": "Đang chờ xác thực SSO" }, "awaitingSSODesc": { "message": "Vui lòng tiếp tục đăng nhập bằng thông tin đăng nhập của công ty bạn." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index f44265425e9..2382e6fc971 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "搜索密码库" }, + "resetSearch": { + "message": "重置搜索" + }, "edit": { "message": "编辑" }, @@ -3477,7 +3480,7 @@ } }, "youDeniedLoginAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this was you, try to log in with the device again." + "message": "您拒绝了一个来自其他设备的登录尝试。若确实是您本人,请尝试再次发起设备登录。" }, "device": { "message": "设备" @@ -3585,25 +3588,25 @@ "message": "Manage devices" }, "currentSession": { - "message": "Current session" + "message": "当前会话" }, "mobile": { - "message": "Mobile", + "message": "移动端", "description": "Mobile app" }, "extension": { - "message": "Extension", + "message": "扩展", "description": "Browser extension/addon" }, "desktop": { - "message": "Desktop", + "message": "桌面端", "description": "Desktop app" }, "webVault": { - "message": "Web vault" + "message": "网页密码库" }, "webApp": { - "message": "Web app" + "message": "网页 App" }, "cli": { "message": "CLI" @@ -3613,7 +3616,7 @@ "description": "Software Development Kit" }, "requestPending": { - "message": "Request pending" + "message": "请求待处理" }, "firstLogin": { "message": "First login" @@ -3671,7 +3674,7 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "您拒绝了另一台设备的登录尝试。如果真的是您,请尝试再次使用该设备登录。" + "message": "您拒绝了一个来自其他设备的登录尝试。若确实是您本人,请尝试再次发起设备登录。" }, "loginRequestHasAlreadyExpired": { "message": "登录请求已过期。" @@ -3680,7 +3683,7 @@ "message": "Just now" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "请求于 $MINUTES$ 分钟前", "placeholders": { "minutes": { "content": "$1", @@ -4407,7 +4410,7 @@ "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { - "message": "More about match detection", + "message": "更多关于匹配检测", "description": "Link to match detection docs on warning dialog for advance match strategy" }, "uriAdvancedOption": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index b41b9271c75..d2776cb227d 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -547,6 +547,9 @@ "searchVault": { "message": "搜尋密碼庫" }, + "resetSearch": { + "message": "Reset search" + }, "edit": { "message": "編輯" }, From d9480a8dabe41aa47858ceec49e488b499e4d83d Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:10:35 +0200 Subject: [PATCH 049/179] [PM-17503] Delete old password-strength component (#15652) * Removed flag and components. * More cleanup * Removed ChangePasswordComponent. * Removed old EmergencyAccessTakeover * Removed service initialization. * Fixed test failures. * Fixed tests. * Test changes. * Updated comments * Delete old password-strength component A PasswordStrengthV2Component had been created in July 2024 and now all usages of the old component have been replaced. * Re-add canAccessFeature back into app-routing.module. Messed up by merging main. --------- Co-authored-by: Todd Martin Co-authored-by: Daniel James Smith --- libs/angular/src/jslib.module.ts | 3 - .../password-strength.component.html | 14 --- .../password-strength.component.ts | 118 ------------------ 3 files changed, 135 deletions(-) delete mode 100644 libs/angular/src/tools/password-strength/password-strength.component.html delete mode 100644 libs/angular/src/tools/password-strength/password-strength.component.ts diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 89e6cfeacb7..86042f4b779 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -54,7 +54,6 @@ import { UserTypePipe } from "./pipes/user-type.pipe"; import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe"; import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe"; import { I18nPipe } from "./platform/pipes/i18n.pipe"; -import { PasswordStrengthComponent } from "./tools/password-strength/password-strength.component"; import { IconComponent } from "./vault/components/icon.component"; @NgModule({ @@ -108,7 +107,6 @@ import { IconComponent } from "./vault/components/icon.component"; TrueFalseValueDirective, LaunchClickDirective, UserNamePipe, - PasswordStrengthComponent, UserTypePipe, IfFeatureDirective, FingerprintPipe, @@ -143,7 +141,6 @@ import { IconComponent } from "./vault/components/icon.component"; CopyClickDirective, LaunchClickDirective, UserNamePipe, - PasswordStrengthComponent, UserTypePipe, IfFeatureDirective, FingerprintPipe, diff --git a/libs/angular/src/tools/password-strength/password-strength.component.html b/libs/angular/src/tools/password-strength/password-strength.component.html deleted file mode 100644 index c9eec73899b..00000000000 --- a/libs/angular/src/tools/password-strength/password-strength.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
- - {{ text }} - -
-
diff --git a/libs/angular/src/tools/password-strength/password-strength.component.ts b/libs/angular/src/tools/password-strength/password-strength.component.ts deleted file mode 100644 index ca9892d9c6c..00000000000 --- a/libs/angular/src/tools/password-strength/password-strength.component.ts +++ /dev/null @@ -1,118 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core"; - -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; - -export interface PasswordColorText { - color: string; - text: string; -} - -/** - * @deprecated July 2024: Use new PasswordStrengthV2Component instead - */ -@Component({ - selector: "app-password-strength", - templateUrl: "password-strength.component.html", - standalone: false, -}) -export class PasswordStrengthComponent implements OnChanges { - @Input() showText = false; - @Input() email: string; - @Input() name: string; - @Input() set password(value: string) { - this.updatePasswordStrength(value); - } - @Output() passwordStrengthResult = new EventEmitter(); - @Output() passwordScoreColor = new EventEmitter(); - - masterPasswordScore: number; - scoreWidth = 0; - color = "bg-danger"; - text: string; - - private masterPasswordStrengthTimeout: any; - - //used by desktop and browser to display strength text color - get masterPasswordScoreColor() { - switch (this.masterPasswordScore) { - case 4: - return "success"; - case 3: - return "primary"; - case 2: - return "warning"; - default: - return "danger"; - } - } - - //used by desktop and browser to display strength text - get masterPasswordScoreText() { - switch (this.masterPasswordScore) { - case 4: - return this.i18nService.t("strong"); - case 3: - return this.i18nService.t("good"); - case 2: - return this.i18nService.t("weak"); - default: - return this.masterPasswordScore != null ? this.i18nService.t("weak") : null; - } - } - - constructor( - private i18nService: I18nService, - private passwordStrengthService: PasswordStrengthServiceAbstraction, - ) {} - - ngOnChanges(): void { - this.masterPasswordStrengthTimeout = setTimeout(() => { - this.scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; - - switch (this.masterPasswordScore) { - case 4: - this.color = "bg-success"; - this.text = this.i18nService.t("strong"); - break; - case 3: - this.color = "bg-primary"; - this.text = this.i18nService.t("good"); - break; - case 2: - this.color = "bg-warning"; - this.text = this.i18nService.t("weak"); - break; - default: - this.color = "bg-danger"; - this.text = this.masterPasswordScore != null ? this.i18nService.t("weak") : null; - break; - } - - this.setPasswordScoreText(this.color, this.text); - }, 300); - } - - updatePasswordStrength(password: string) { - const masterPassword = password; - - if (this.masterPasswordStrengthTimeout != null) { - clearTimeout(this.masterPasswordStrengthTimeout); - } - - const strengthResult = this.passwordStrengthService.getPasswordStrength( - masterPassword, - this.email, - this.name?.trim().toLowerCase().split(" "), - ); - this.passwordStrengthResult.emit(strengthResult); - this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; - } - - setPasswordScoreText(color: string, text: string) { - color = color.slice(3); - this.passwordScoreColor.emit({ color: color, text: text }); - } -} From 4989f024bd4e3a38f4ff307ac88e09eaf3421f71 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:11:35 +0200 Subject: [PATCH 050/179] Autosync the updated translations (#15773) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 19 ++++--------- apps/desktop/src/locales/ar/messages.json | 19 ++++--------- apps/desktop/src/locales/az/messages.json | 19 ++++--------- apps/desktop/src/locales/be/messages.json | 19 ++++--------- apps/desktop/src/locales/bg/messages.json | 19 ++++--------- apps/desktop/src/locales/bn/messages.json | 19 ++++--------- apps/desktop/src/locales/bs/messages.json | 19 ++++--------- apps/desktop/src/locales/ca/messages.json | 19 ++++--------- apps/desktop/src/locales/cs/messages.json | 19 ++++--------- apps/desktop/src/locales/cy/messages.json | 19 ++++--------- apps/desktop/src/locales/da/messages.json | 19 ++++--------- apps/desktop/src/locales/de/messages.json | 19 ++++--------- apps/desktop/src/locales/el/messages.json | 19 ++++--------- apps/desktop/src/locales/en_GB/messages.json | 19 ++++--------- apps/desktop/src/locales/en_IN/messages.json | 19 ++++--------- apps/desktop/src/locales/eo/messages.json | 19 ++++--------- apps/desktop/src/locales/es/messages.json | 19 ++++--------- apps/desktop/src/locales/et/messages.json | 19 ++++--------- apps/desktop/src/locales/eu/messages.json | 19 ++++--------- apps/desktop/src/locales/fa/messages.json | 19 ++++--------- apps/desktop/src/locales/fi/messages.json | 19 ++++--------- apps/desktop/src/locales/fil/messages.json | 19 ++++--------- apps/desktop/src/locales/fr/messages.json | 19 ++++--------- apps/desktop/src/locales/gl/messages.json | 19 ++++--------- apps/desktop/src/locales/he/messages.json | 19 ++++--------- apps/desktop/src/locales/hi/messages.json | 19 ++++--------- apps/desktop/src/locales/hr/messages.json | 27 ++++++------------ apps/desktop/src/locales/hu/messages.json | 19 ++++--------- apps/desktop/src/locales/id/messages.json | 19 ++++--------- apps/desktop/src/locales/it/messages.json | 19 ++++--------- apps/desktop/src/locales/ja/messages.json | 19 ++++--------- apps/desktop/src/locales/ka/messages.json | 19 ++++--------- apps/desktop/src/locales/km/messages.json | 19 ++++--------- apps/desktop/src/locales/kn/messages.json | 19 ++++--------- apps/desktop/src/locales/ko/messages.json | 19 ++++--------- apps/desktop/src/locales/lt/messages.json | 19 ++++--------- apps/desktop/src/locales/lv/messages.json | 19 ++++--------- apps/desktop/src/locales/me/messages.json | 19 ++++--------- apps/desktop/src/locales/ml/messages.json | 19 ++++--------- apps/desktop/src/locales/mr/messages.json | 19 ++++--------- apps/desktop/src/locales/my/messages.json | 19 ++++--------- apps/desktop/src/locales/nb/messages.json | 19 ++++--------- apps/desktop/src/locales/ne/messages.json | 19 ++++--------- apps/desktop/src/locales/nl/messages.json | 19 ++++--------- apps/desktop/src/locales/nn/messages.json | 19 ++++--------- apps/desktop/src/locales/or/messages.json | 19 ++++--------- apps/desktop/src/locales/pl/messages.json | 19 ++++--------- apps/desktop/src/locales/pt_BR/messages.json | 19 ++++--------- apps/desktop/src/locales/pt_PT/messages.json | 19 ++++--------- apps/desktop/src/locales/ro/messages.json | 19 ++++--------- apps/desktop/src/locales/ru/messages.json | 19 ++++--------- apps/desktop/src/locales/si/messages.json | 19 ++++--------- apps/desktop/src/locales/sk/messages.json | 19 ++++--------- apps/desktop/src/locales/sl/messages.json | 19 ++++--------- apps/desktop/src/locales/sr/messages.json | 19 ++++--------- apps/desktop/src/locales/sv/messages.json | 19 ++++--------- apps/desktop/src/locales/te/messages.json | 19 ++++--------- apps/desktop/src/locales/th/messages.json | 19 ++++--------- apps/desktop/src/locales/tr/messages.json | 19 ++++--------- apps/desktop/src/locales/uk/messages.json | 19 ++++--------- apps/desktop/src/locales/vi/messages.json | 23 +++++----------- apps/desktop/src/locales/zh_CN/messages.json | 29 +++++++------------- apps/desktop/src/locales/zh_TW/messages.json | 19 ++++--------- 63 files changed, 326 insertions(+), 893 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 3e539e48eb9..4db12ec1a87 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Deursoek kluis" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Voeg item toe" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Ontgrendel met Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Bykomende Windows Hello-instellings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "ontgrendel u kluis" }, - "autoPromptWindowsHello": { - "message": "Vra vir Windows Hello by lansering" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Vra vir Touch ID by lansering" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Aanbeveel vir veiligheid." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 26aaf141dd2..c2e7c2cde6c 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "البحث في الخزانة" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "إضافة عنصر" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "فتح مع Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "إعدادات Windows Hello إضافية" - }, "unlockWithPolkit": { "message": "فتح مع مصادقة النظام" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "فتح خزانتك" }, - "autoPromptWindowsHello": { - "message": "اسأل عن Windows Hello عند التشغيل" - }, - "autoPromptPolkit": { - "message": "طلب مصادقة النظام عند التشغيل" - }, "autoPromptTouchId": { "message": "اطلب معرف اللمس عند التشغيل" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "موصى به للأمان." - }, "lockWithMasterPassOnRestart1": { "message": "قفل مع كلمة المرور الرئيسية عند إعادة تشغيل" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index b04f3204a15..6f3a02502a0 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Seyfdə axtar" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Element əlavə et" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Windows Hello ilə kilidi aç" }, - "additionalWindowsHelloSettings": { - "message": "Əlavə Windows Hello ayarları" - }, "unlockWithPolkit": { "message": "Sistem kimlik doğrulaması ilə kilidi aç" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "seyfinizin kilidini açın" }, - "autoPromptWindowsHello": { - "message": "Açılışda Windows Hello-nu soruşun" - }, - "autoPromptPolkit": { - "message": "Açılışda sistem kimlik doğrulamasını tələb et" - }, "autoPromptTouchId": { "message": "Açılışda Touch ID-ni soruşun" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Tətbiq başladılanda parolu tələb et" }, - "recommendedForSecurity": { - "message": "Güvənlik üçün tövsiyə olunur." - }, "lockWithMasterPassOnRestart1": { "message": "Yenidən başladılanda ana parol ilə kilidlə" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Avto-yazma qısayolunu fəallaşdır" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden, giriş yerlərini doğrulamır, qısayolu istifadə etməzdən əvvəl doğru pəncərədə və xanada olduğunuza əmin olun." diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index f4b58c89cac..5a777151355 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Пошук у сховішчы" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Дадаць элемент" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Разблакіраваць з Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Дадатковыя налады Windows Hello" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "разблакіраваць ваша сховішча" }, - "autoPromptWindowsHello": { - "message": "Пытацца пра Windows Hello пры запуску" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Пытацца пра Touch ID пры запуску" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Рэкамендуецца ў мэтах бяспекі." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 92ee97ba16a..1c4145f73e1 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Търсене в трезора" }, + "resetSearch": { + "message": "Нулиране на търсенето" + }, "addItem": { "message": "Добавяне на елемент" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Отключване с Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Допълнителни настройки на Windows Hello" - }, "unlockWithPolkit": { "message": "Отключване чрез системно удостоверяване" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "отключете трезора си" }, - "autoPromptWindowsHello": { - "message": "Питане за Windows Hello при пускане" - }, - "autoPromptPolkit": { - "message": "Питане за системно удостоверяване при стартиране" - }, "autoPromptTouchId": { "message": "Питане за Touch ID при пускане" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Изискване на парола при стартиране на приложението" }, - "recommendedForSecurity": { - "message": "Препоръчително от гледна точка на сигурността." - }, "lockWithMasterPassOnRestart1": { "message": "Заключване с главната парола при повторно пускане" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Включване на клавишната комбинация за автоматично попълване" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Битуорден не проверява местата за въвеждане, така че се уверете, че сте в правилния прозорец, преди да ползвате клавишната комбинация." diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 4a39428590e..58c484465ab 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "ভল্ট খুঁজুন" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "বস্তু জুড়ুন" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 5be68fe816f..52c8a792296 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Pretraživanje trezora" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Dodaj stavku" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Otključaj koristeći Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "otključaj trezor" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 6534c0eef02..ad6c3bcb5ed 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Cerca en la caixa forta" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Afegeix element" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Desbloqueja amb Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Configuració addicional de Windows Hello" - }, "unlockWithPolkit": { "message": "Desbloqueja amb l'autenticació del sistema" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "desbloqueja la teva caixa forta" }, - "autoPromptWindowsHello": { - "message": "Sol·liciteu Windows Hello en iniciar" - }, - "autoPromptPolkit": { - "message": "Sol·licita l'autenticació del sistema a l'inici" - }, "autoPromptTouchId": { "message": "Sol·liciteu Touch ID en iniciar" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recomanat per seguretat." - }, "lockWithMasterPassOnRestart1": { "message": "Bloqueja amb la contrasenya mestra en reiniciar" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index a21f9b9258e..928092ba5a4 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Prohledat trezor" }, + "resetSearch": { + "message": "Resetovat hledání" + }, "addItem": { "message": "Přidat položku" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Odemknout pomocí Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Další nastavení Windows Hello" - }, "unlockWithPolkit": { "message": "Odemknout pomocí systémového ověření" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Odemknout Váš trezor" }, - "autoPromptWindowsHello": { - "message": "Požádat o Windows Hello při spuštění aplikace" - }, - "autoPromptPolkit": { - "message": "Požádat o systémové ověření při spuštění" - }, "autoPromptTouchId": { "message": "Požádat o Touch ID při spuštění aplikace" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Vyžadovat heslo při spuštění aplikace" }, - "recommendedForSecurity": { - "message": "Doporučeno pro bezpečnost." - }, "lockWithMasterPassOnRestart1": { "message": "Zamknout trezor při restartu pomocí hlavního hesla" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Povolit zkratku automatického psaní" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden neověřuje umístění vstupu. Před použitím zkratky se ujistěte, že jste ve správném okně a poli." diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index e3db70bf152..3b35ad6fbe4 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 47df4e98b3f..2127c13594b 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Søg i boks" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Tilføj emne" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Oplås med Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Yderligere indstillinger for Windows Hello" - }, "unlockWithPolkit": { "message": "Oplås med systemgodkendelse" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "oplås din boks" }, - "autoPromptWindowsHello": { - "message": "Anmod om Windows Hello ved app-start" - }, - "autoPromptPolkit": { - "message": "Anmod om systemgodkendelse ved start" - }, "autoPromptTouchId": { "message": "Anmod om Touch ID ved app-start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Anbefales af sikkerhedshensyn." - }, "lockWithMasterPassOnRestart1": { "message": "Lås med hovedadgangskode ved genstart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 1f60be15374..3838dc6151c 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Tresor durchsuchen" }, + "resetSearch": { + "message": "Suche zurücksetzen" + }, "addItem": { "message": "Eintrag hinzufügen" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Mit Windows Hello entsperren" }, - "additionalWindowsHelloSettings": { - "message": "Zusätzliche Einstellungen für Windows Hello" - }, "unlockWithPolkit": { "message": "Mit Systemauthentifizierung entsperren" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Deinen Tresor entsperren" }, - "autoPromptWindowsHello": { - "message": "Beim Start nach Windows Hello fragen" - }, - "autoPromptPolkit": { - "message": "Beim Start nach Systemauthentifizierung fragen" - }, "autoPromptTouchId": { "message": "Beim Start nach Touch ID fragen" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Ein Passwort beim Starten der App verlangen" }, - "recommendedForSecurity": { - "message": "Aus Sicherheitsgründen empfohlen." - }, "lockWithMasterPassOnRestart1": { "message": "Beim Neustart mit Master-Passwort sperren" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Auto-Schreiben Tastenkombination aktivieren" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden überprüft die Eingabestellen nicht. Vergewissere dich, dass du dich im richtigen Fenster und Feld befindest, bevor du die Tastenkombination verwendest." diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 2f2a3cf914c..8421ef0e6ea 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Αναζήτηση κρύπτης" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Προσθήκη στοιχείου" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Ξεκλειδώστε με το Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Πρόσθετες ρυθμίσεις του Windows Hello" - }, "unlockWithPolkit": { "message": "Ξεκλείδωμα με αυθεντικοποίηση συστήματος" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Ξεκλειδώστε το vault σας" }, - "autoPromptWindowsHello": { - "message": "Να ζητείται Windows Hello κατά την εκκίνηση της εφαρμογής" - }, - "autoPromptPolkit": { - "message": "Ρώτησε με για αυθεντικοποίηση συστήματος κατά την εκκίνηση" - }, "autoPromptTouchId": { "message": "Ερώτηση για το Touch ID κατά την εκκίνηση" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Συνίσταται για ασφάλεια." - }, "lockWithMasterPassOnRestart1": { "message": "Κλείδωμα με τον κύριο κωδικό πρόσβασης κατά την επανεκκίνηση" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index f73e373d825..aa430c36465 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on launch" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on launch" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index ee0e0dc276f..9536bb263a1 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Verify for Bitwarden." }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on launch" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on launch" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 5bfde4d51c0..d0f191d7413 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Traserĉi la trezorejon" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Aldoni eron" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "malŝlosi vian trezorejon" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Ŝlosi per la ĉefa pasvorto ĉe relanĉo" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index f839e9c8634..a387afa3659 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Buscar en caja fuerte" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Añadir elemento" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Desbloquear con Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Ajustes adicionales de Windows Hello" - }, "unlockWithPolkit": { "message": "Desbloquear con la autenticación del sistema" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Verificar para Bitwarden." }, - "autoPromptWindowsHello": { - "message": "Solicitar Windows Hello al iniciar" - }, - "autoPromptPolkit": { - "message": "Solicitar autenticación de sistema al iniciar" - }, "autoPromptTouchId": { "message": "Solicitar Touch ID al iniciar" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Solicitar contraseña al iniciar la aplicación" }, - "recommendedForSecurity": { - "message": "Recomendado por seguridad." - }, "lockWithMasterPassOnRestart1": { "message": "Bloquear con contraseña maestra al reiniciar" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Activar atajo de autoescritura" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden no valida las ubicaciones de entrada, asegúrate de que estás en la ventana y en el capo correctos antes de usar el atajo." diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index be7628d2d34..598ea554f41 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Otsi hoidlast" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Lisa kirje" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Lukusta lahti Windows Helloga" }, - "additionalWindowsHelloSettings": { - "message": "Windows Hello lisaseaded" - }, "unlockWithPolkit": { "message": "Ava arvuti autentimissüsteemiga" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Kinnita Bitwardenisse sisselogimine." }, - "autoPromptWindowsHello": { - "message": "Küsi avamisel Windows Hello tuvastust" - }, - "autoPromptPolkit": { - "message": "Kasuta käivitamisel arvuti autentimissüsteemi" - }, "autoPromptTouchId": { "message": "Küsi avamisel Touch ID tuvastust" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Nõua parooli rakenduse käivitamisel" }, - "recommendedForSecurity": { - "message": "Soovitatud turvalisuse huvides." - }, "lockWithMasterPassOnRestart1": { "message": "Lukusta ülemparooliga, kui rakendus taaskäivitatakse" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 0c4b2f4d836..6db97dce6f1 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Kutxa Gotorrean bilatu" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Gehitu elementua" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Desblokeatu Windows Hello-rekin" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "desblokeatu kutxa gotorra" }, - "autoPromptWindowsHello": { - "message": "Eskatu Windows Hello abiaraztean" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Eskatu Touch ID abiaraztean" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 8aefda20bf4..82fb4b084b5 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "جستجوی گاوصندوق" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "افزودن مورد" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "قفل گشایی با Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "تنظیمات اضافی Windows Hello" - }, "unlockWithPolkit": { "message": "باز کردن قفل با احراز هویت سیستم" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "قفل گاوصندوق خود را باز کنید" }, - "autoPromptWindowsHello": { - "message": "درخواست Windows Hello در هنگام راه‌اندازی" - }, - "autoPromptPolkit": { - "message": "در زمان اجرا درخواست احراز هویت سیستم را بده" - }, "autoPromptTouchId": { "message": "درخواست Touch ID در هنگام راه‌اندازی" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "هنگام شروع برنامه، کلمه عبور مورد نیاز است" }, - "recommendedForSecurity": { - "message": "برای امنیت توصیه می‌شود." - }, "lockWithMasterPassOnRestart1": { "message": "در زمان شروع مجدد، با کلمه عبور اصلی قفل کن" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index b334bc61ece..c885d33ab78 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Etsi holvista" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Lisää kohde" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Avaa Windows Hellolla" }, - "additionalWindowsHelloSettings": { - "message": "Windows Hello -lisäasetukset." - }, "unlockWithPolkit": { "message": "Avaa järjestelmän tunnistautumisella" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "avaa holvisi" }, - "autoPromptWindowsHello": { - "message": "Pyydä Windows Hello -todennusta käynnistettäessä" - }, - "autoPromptPolkit": { - "message": "Pyydä järjestelmän tunnistautumista käynnistettäessä" - }, "autoPromptTouchId": { "message": "Pyydä Touch ID -todennusta käynnistettäessä" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Vaadi salasana sovelluksen käynnistyessä" }, - "recommendedForSecurity": { - "message": "Suositeltavaa parempaa suojausta varten." - }, "lockWithMasterPassOnRestart1": { "message": "Lukitse pääsalasanalla uudelleenkäynnistyksen yhteydessä" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 723d324bf70..1118f76a4a2 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Hanapin ang vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Magdagdag ng item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "I-unlock gamit ang Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "i-unlock ang iyong vault" }, - "autoPromptWindowsHello": { - "message": "Humingi ng Windows Hello sa paglulunsad" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Humingi ng Touch ID sa paglulunsad" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index b06308b2906..47765ae773a 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Rechercher dans le coffre" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Ajouter un élément" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Déverrouiller avec Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Paramètres supplémentaires de Windows Hello" - }, "unlockWithPolkit": { "message": "Déverrouiller avec l'authentification du système" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "déverouiller votre coffre" }, - "autoPromptWindowsHello": { - "message": "Demander à Windows Hello au démarrage" - }, - "autoPromptPolkit": { - "message": "Demander l'authentification du système au lancement" - }, "autoPromptTouchId": { "message": "Demander Touch ID au démarrage de l'application" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Exiger un mot de passe au démarrage de l'application" }, - "recommendedForSecurity": { - "message": "Recommandé pour la sécurité." - }, "lockWithMasterPassOnRestart1": { "message": "Verrouiller avec le mot de passe principal au redémarrage" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 3d240ff77e8..15b71ce7f7a 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 5b76be7fff3..10b6c43386b 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "חיפוש בכספת" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "הוסף פריט" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "שחרור נעילה עם Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "הגדרות Windows Hello נוספות" - }, "unlockWithPolkit": { "message": "בטל נעילה עם אימות מערכת" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "פתח את הכספת שלך" }, - "autoPromptWindowsHello": { - "message": "הצג את Windows Hello בפתיחת האפליקציה" - }, - "autoPromptPolkit": { - "message": "בקש אימות מערכת בפתיחה" - }, "autoPromptTouchId": { "message": "הצג בקשה של Touch ID בפתיחת האפליקציה" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "דרוש סיסמה בפתיחת היישום" }, - "recommendedForSecurity": { - "message": "מומלץ לאבטחה." - }, "lockWithMasterPassOnRestart1": { "message": "נעל בעזרת הסיסמה הראשית בהפעלה מחדש" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "הפעלת קיצור הקלדה אוטומטית" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden לא מאמת את מקומות הקלט, נא לוודא שזה החלון והשדה הנכונים בטרם שימוש בקיצור הדרך." diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 77b416118c9..4f813445418 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index b86e57a4811..21e4b11dfe6 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Pretraživanje trezora" }, + "resetSearch": { + "message": "Ponovno postavljanje pretraživanja" + }, "addItem": { "message": "Dodaj stavku" }, @@ -241,7 +244,7 @@ "message": "SSH agent je servis namijenjen developerima koji omogućuje potpisivanje SSH zahtjeva izravno iz tvojeg Bitwarden trezora." }, "sshAgentPromptBehavior": { - "message": "Zahtjevaj autorizaciju pri korištenju SSH agenta" + "message": "Traži autorizaciju pri korištenju SSH agenta" }, "sshAgentPromptBehaviorDesc": { "message": "Odaberi kako će se postupati sa zahtjevima za autorizaciju SSH agenta." @@ -573,7 +576,7 @@ "message": "Kopiraj kôd za provjeru (TOTP)" }, "copyFieldCipherName": { - "message": "Copy $FIELD$, $CIPHERNAME$", + "message": "Kopiraj $FIELD$, $CIPHERNAME$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -1440,7 +1443,7 @@ "description": "Copy credit card security code (CVV)" }, "cardNumber": { - "message": "card number" + "message": "broj kartice" }, "premiumMembership": { "message": "Premium članstvo" @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Otključaj koristeći Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Dodatne Windows Hello postavke" - }, "unlockWithPolkit": { "message": "Otključaj autentifikacijom sustava" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Otključaj trezor" }, - "autoPromptWindowsHello": { - "message": "Zahtijevaj Windows Hello pri pokretanju" - }, - "autoPromptPolkit": { - "message": "Traži autentifikaciju sustava pri pokretanju" - }, "autoPromptTouchId": { "message": "Zahtijevaj Touch ID pri pokretanju" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Zahtijevaj lozinku pri pokretanju" }, - "recommendedForSecurity": { - "message": "Preporučeno za sigurnost." - }, "lockWithMasterPassOnRestart1": { "message": "Zaključaj glavnom lozinkom kod svakog pokretanja" }, @@ -4015,10 +4006,10 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { - "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." + "message": "Bitwarden ne provjerava lokacije unosa, prije korištenja prečaca provjeri da si u pravom prozoru i polju." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 0f5e14596d7..6fa7abf94fe 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Keresés a széfben" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Elem hozzáadása" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Feloldás Windows Hello segítségével" }, - "additionalWindowsHelloSettings": { - "message": "Kiegészítő Windows Hello beállítások" - }, "unlockWithPolkit": { "message": "Feloldás rendszer hitelesítéssel" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Bitwarden feloldás" }, - "autoPromptWindowsHello": { - "message": "Windows Hello kérése indításkor" - }, - "autoPromptPolkit": { - "message": "Rendszer hiteletesítés bekérése indításkor" - }, "autoPromptTouchId": { "message": "Érintés AZ kérése indításkor" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Jelszó szükséges az alkalmazás indításakor" }, - "recommendedForSecurity": { - "message": "Biztonsági szempontból ajánlott." - }, "lockWithMasterPassOnRestart1": { "message": "Lezárás mesterjelszóval újraindításkor" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Automatikus típusú parancsikon engedélyezése" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "A Bitwarden nem érvényesíti a beviteli helyeket, győződjünk meg róla, hogy a megfelelő ablakban és mezőben vagyunk, mielőtt a parancsikont használnánk." diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index f4627f805e0..feb05102c1e 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Cari brankas" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Tambah Item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Buka dengan Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Verifikasi untuk Bitwarden." }, - "autoPromptWindowsHello": { - "message": "Minta Windows Hello saat diluncurkan" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Minta Touch ID saat diluncurkan" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Direkomendasikan untuk keamanan." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index b89b25a745c..d97f4527ef9 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Cerca nella cassaforte" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Aggiungi elemento" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Sblocca con Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Impostazioni aggiuntive di Windows Hello" - }, "unlockWithPolkit": { "message": "Sblocca con l'autenticazione di sistema" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "sblocca la tua cassaforte" }, - "autoPromptWindowsHello": { - "message": "Richiedi Windows Hello all'avvio" - }, - "autoPromptPolkit": { - "message": "Chiedi autenticazione di sistema all'avvio" - }, "autoPromptTouchId": { "message": "Richiedi Touch ID all'avvio" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Richiedi parola d'accesso all'avvio dell'app" }, - "recommendedForSecurity": { - "message": "Consigliato per la sicurezza." - }, "lockWithMasterPassOnRestart1": { "message": "Blocca con password principale al riavvio" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 19a91367567..5830b3b1db9 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "保管庫を検索" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "アイテムの追加" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Windows Hello でロック解除" }, - "additionalWindowsHelloSettings": { - "message": "追加の Windows Hello 設定" - }, "unlockWithPolkit": { "message": "システム認証でロック解除" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Bitwarden の認証を行います。" }, - "autoPromptWindowsHello": { - "message": "起動時に Windows Hello を要求する" - }, - "autoPromptPolkit": { - "message": "起動時にシステム認証を要求する" - }, "autoPromptTouchId": { "message": "起動時に Touch ID を要求する" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "アプリ起動時にパスワードを要求" }, - "recommendedForSecurity": { - "message": "セキュリティ向上のためおすすめします。" - }, "lockWithMasterPassOnRestart1": { "message": "再起動時にマスターパスワードでロック" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 5f9d2fe17b3..3c158a14fdf 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "ელემენტის დამატება" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 3d240ff77e8..15b71ce7f7a 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index b7664ad90bf..ad2995ca097 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "ವಾಲ್ಟ್ ಹುಡುಕಿ" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "ಐಟಂ ಸೇರಿಸಿ" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "ವಿಂಡೋಸ್ ಹಲೋನೊಂದಿಗೆ ಅನ್ಲಾಕ್ ಮಾಡಿ" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಿ" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index ca23d5107c7..4e484629d1e 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "보관함 검색" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "항목 추가" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Windows Hello를 사용하여 잠금 해제" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "보관함 잠금 해제" }, - "autoPromptWindowsHello": { - "message": "실행 시 Windows Hello 요구하기" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "실행 시 Touch ID 요구하기" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 99c63ab4dab..d2d78941464 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Ieškoti saugykloje" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Pridėti elementą" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Atrakinti naudojant Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Papildomi Windows Hello nustatymai" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "atrakinti saugyklą" }, - "autoPromptWindowsHello": { - "message": "Paprašyti Windows Hello paleidus programą" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Prašyti Touch ID paleidus programėlę" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Rekomenduojama dėl saugumo." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 0849bf29b45..163b88c5c7c 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Meklēt glabātavā" }, + "resetSearch": { + "message": "Atiestatīt meklēšanu" + }, "addItem": { "message": "Pievienot vienumu" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Atslēgt ar Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Windows Hello papildu iestatījumi" - }, "unlockWithPolkit": { "message": "Atslēgt ar sistēmas autentifikāciju" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "atslēgt glabātavu" }, - "autoPromptWindowsHello": { - "message": "Palaišanā vaicāt pēc Windows Hello" - }, - "autoPromptPolkit": { - "message": "Palaišanas laikā vaicāt pēc autentifikācijas" - }, "autoPromptTouchId": { "message": "Palaišanā vaicāt pēc Touch ID" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Pieprasīt paroli pēc lietotnes palaišanas" }, - "recommendedForSecurity": { - "message": "Ieteicams drošībai." - }, "lockWithMasterPassOnRestart1": { "message": "Aizslēgt ar galveno paroli pēc pārsāknēšanas" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Iespējot automātiskās ievades saīsni" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden nepārbauda ievades atrašanās vietas, jāpārliecinās, ka atrodies pareizajā logā un laukā, pirms saīsnes izmantošanas." diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 0929250c06f..0b908f476ab 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Pretraži trezor" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Dodaj stavku" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Otključaj sa Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Verifikuj za Bitwarden." }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index a77ab04c0aa..e1fb7dc2463 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "വാൾട് തിരയുക" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "ഇനം ചേർക്കുക" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Windows Hello ഉപയോഗിച്ച് അൺലോക്കുചെയ്യുക" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "Bitwarden വേണ്ടി പരിശോധിച്ചുറപ്പിക്കുക." }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 3d240ff77e8..15b71ce7f7a 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index b09ea6cdbf2..4a5d71ddb2e 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index dfa381fc3d0..d6083e9b404 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Søk i hvelvet" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Legg til element" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Lås opp med Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Ytterligere Windows Hello-innstillinger" - }, "unlockWithPolkit": { "message": "Lås opp med systemautentisering" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "lås opp hvelvet ditt" }, - "autoPromptWindowsHello": { - "message": "Spør etter Windows Hello ved oppstart" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Spør om Touch ID ved oppstart" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Anbefalt for sikkerhet." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 56ccc79775c..4716057bb03 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "भल्टमा खोज्नुहोस्" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "आइटम थप्नुहोस्" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 5301982ecdc..86def3188fd 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Kluis doorzoeken" }, + "resetSearch": { + "message": "Zoekopdracht resetten" + }, "addItem": { "message": "Item toevoegen" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Ontgrendelen met Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Extra Windows Hello-instellingen" - }, "unlockWithPolkit": { "message": "Ontgrendel met systeemauthenticatie" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "ontgrendel je kluis" }, - "autoPromptWindowsHello": { - "message": "Vraag om Windows Hello bij opstarten" - }, - "autoPromptPolkit": { - "message": "Vraag naar systeemverificatie bij het opstarten" - }, "autoPromptTouchId": { "message": "Vraag om Touch ID bij opstarten" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Wachtwoord vereisen bij starten van de app" }, - "recommendedForSecurity": { - "message": "Aanbevolen voor veiligheid." - }, "lockWithMasterPassOnRestart1": { "message": "Bij herstart vergrendelen met hoofdwachtwoord" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Snelkoppeling autotype inschakelen" + "enableAutotypeTransitionKey": { + "message": "Autotypen inschakelen" }, "enableAutotypeDescription": { "message": "Bitwarden valideert de invoerlocaties niet, zorg ervoor dat je je in het juiste venster en veld bevindt voordat je de snelkoppeling gebruikt." diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 4c86afccbeb..459a068007f 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Søk i kvelven" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Legg til ei oppføring" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Lås opp med Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 6db35fd307e..df8249b40de 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 9852c85554a..4c8a5d6c004 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Szukaj w sejfie" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Dodaj element" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Odblokuj za pomocą Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Dodatkowe ustawienia Windows Hello" - }, "unlockWithPolkit": { "message": "Odblokuj za pomocą uwierzytelniania systemowego" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "odblokuj sejf" }, - "autoPromptWindowsHello": { - "message": "Poproś o Windows Hello przy uruchomieniu" - }, - "autoPromptPolkit": { - "message": "Zapytaj o uwierzytelnianie systemowe przy uruchomieniu" - }, "autoPromptTouchId": { "message": "Poproś o Touch ID przy uruchomieniu" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Wymagaj hasła przy starcie aplikacji" }, - "recommendedForSecurity": { - "message": "Zalecane dla bezpieczeństwa." - }, "lockWithMasterPassOnRestart1": { "message": "Zablokuj hasłem głównym po uruchomieniu ponownym" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 5e570920f55..5468e23aeaa 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Pesquisar no Cofre" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Adicionar Item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Desbloquear com o Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Configurações adicionais do Windows Hello" - }, "unlockWithPolkit": { "message": "Desbloquear com autenticação de sistema" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "desbloquear o seu cofre" }, - "autoPromptWindowsHello": { - "message": "Perguntar para iniciar o Hello do Windows" - }, - "autoPromptPolkit": { - "message": "Pedir autenticação do sistema na inicialização" - }, "autoPromptTouchId": { "message": "Pedir pelo Touch ID ao iniciar" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Exigir senha ao iniciar o app" }, - "recommendedForSecurity": { - "message": "Recomendado para segurança." - }, "lockWithMasterPassOnRestart1": { "message": "Bloquear com senha mestra ao reiniciar" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index d3764bad580..4651a3381d8 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Procurar no cofre" }, + "resetSearch": { + "message": "Repor pesquisa" + }, "addItem": { "message": "Adicionar item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Desbloquear com o Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Definições adicionais do Windows Hello" - }, "unlockWithPolkit": { "message": "Desbloqueio com autenticação do sistema" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "desbloquear o cofre" }, - "autoPromptWindowsHello": { - "message": "Pedir o Windows Hello ao iniciar a app" - }, - "autoPromptPolkit": { - "message": "Pedir a autenticação do sistema no arranque" - }, "autoPromptTouchId": { "message": "Pedir o Touch ID ao iniciar a app" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Exigir palavra-passe ao iniciar a app" }, - "recommendedForSecurity": { - "message": "Recomendado por segurança." - }, "lockWithMasterPassOnRestart1": { "message": "Bloquear com a palavra-passe mestra ao reiniciar" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Ativar o atalho de introdução automática" + "enableAutotypeTransitionKey": { + "message": "Ativar introdução automática" }, "enableAutotypeDescription": { "message": "O Bitwarden não valida a introdução de localizações. Certifique-se de que está na janela e no campo corretos antes de utilizar o atalho." diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 41a92900d63..0a9a01e9d09 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Căutare seif" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Adăugare articol" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Deblocare cu Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "deblocați-vă seiful" }, - "autoPromptWindowsHello": { - "message": "Solicitați Windows Hello la pornire" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Solicitați Touch ID la pornire" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index b87ffc3cbcd..fcdf91a519e 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Поиск в хранилище" }, + "resetSearch": { + "message": "Сбросить поиск" + }, "addItem": { "message": "Добавить элемент" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Разблокировать с Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Дополнительные настройки Windows Hello" - }, "unlockWithPolkit": { "message": "Разблокировать с помощью системной аутентификации" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "разблокировать ваше хранилище" }, - "autoPromptWindowsHello": { - "message": "Запрашивать Windows Hello при запуске приложения" - }, - "autoPromptPolkit": { - "message": "Запрашивать системную аутентификацию при запуске" - }, "autoPromptTouchId": { "message": "Запрашивать Touch ID при запуске приложения" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Требовать пароль при запуске приложения" }, - "recommendedForSecurity": { - "message": "Рекомендуется для обеспечения безопасности." - }, "lockWithMasterPassOnRestart1": { "message": "Блокировать мастер-паролем при перезапуске" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Включить автоввод ярлыка" + "enableAutotypeTransitionKey": { + "message": "Включить автоввод" }, "enableAutotypeDescription": { "message": "Bitwarden не проверяет местоположение ввода, поэтому, прежде чем использовать ярлык, убедитесь, что вы находитесь в нужном окне и поле." diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 5567654af99..ba6bad46f69 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 49651fbcb7e..843b1c4da26 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Prehľadávať trezor" }, + "resetSearch": { + "message": "Resetovať vyhľadávanie" + }, "addItem": { "message": "Pridať položku" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Odomknúť pomocou Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Ďalšie nastavenia Windows Hello" - }, "unlockWithPolkit": { "message": "Odomknúť systémovým overením" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "odomknúť svoj trezor" }, - "autoPromptWindowsHello": { - "message": "Pri spustení požiadať o Windows Hello" - }, - "autoPromptPolkit": { - "message": "Pri spustení požiadať o systémové overenie" - }, "autoPromptTouchId": { "message": "Pri spustení požiadať o Touch ID" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Pri spustení aplikácie vyžadovať heslo" }, - "recommendedForSecurity": { - "message": "Odporúča sa z hľadiska bezpečnosti." - }, "lockWithMasterPassOnRestart1": { "message": "Pri reštarte zamknúť hlavným heslom" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Povoliť skratku automatického písania" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden neoveruje miesto stupu, pred použitím skratky sa uistite, že ste v správnom okne a poli." diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 23bbae2d523..acf461313b5 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Išči v trezorju" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Dodaj element" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Odkleni z WindowsHello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "odklenite vaš trezor" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Biometrično preverjanje ob zagonu aplikacije" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index c69b4ca8340..dd159108cf7 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Претражи сеф" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Додај ставку" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Откључај са Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Додатна подешавања Windows Hello" - }, "unlockWithPolkit": { "message": "Откључати системском аутентификацијом" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "откључај свој сеф" }, - "autoPromptWindowsHello": { - "message": "Захтевај Windows Hello при покретању" - }, - "autoPromptPolkit": { - "message": "Затражите аутентификацију система при покретању" - }, "autoPromptTouchId": { "message": "Захтевај Touch ID при покретању" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Захтевај лозинку при покретању" }, - "recommendedForSecurity": { - "message": "Препоручује се за сигурност." - }, "lockWithMasterPassOnRestart1": { "message": "Закључајте са главном лозинком при поновном покретању" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 54fb7f1f643..39185c3a1f6 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Sök i valv" }, + "resetSearch": { + "message": "Nollställ sökning" + }, "addItem": { "message": "Lägg till objekt" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Lås upp med Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Ytterligare inställningar för Windows Hello" - }, "unlockWithPolkit": { "message": "Lås upp med systemautentisering" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "lås upp ditt valv" }, - "autoPromptWindowsHello": { - "message": "Be om Windows Hello vid appstart" - }, - "autoPromptPolkit": { - "message": "Be om systemautentisering vid start" - }, "autoPromptTouchId": { "message": "Be om Touch ID vid appstart" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Kräv lösenord vid appstart" }, - "recommendedForSecurity": { - "message": "Rekommenderas för säkerhet." - }, "lockWithMasterPassOnRestart1": { "message": "Lås med huvudlösenord vid omstart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Aktivera genväg för automatisk inmatning" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden validerar inte inmatningsplatser, så se till att du är i rätt fönster och fält innan du använder genvägen." diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 3d240ff77e8..15b71ce7f7a 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Add item" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Unlock with Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 031c9bb6364..81dcca53711 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Search vault" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "เพิ่มรายการ" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "ปลดล็อก ด้วย Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "unlock your vault" }, - "autoPromptWindowsHello": { - "message": "Ask for Windows Hello on app start" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "Recommended for security." - }, "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index b95c585c28f..149766d6b72 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Kasada ara" }, + "resetSearch": { + "message": "Aramayı sıfırla" + }, "addItem": { "message": "Kayıt ekle" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Kilidi Windows Hello ile aç" }, - "additionalWindowsHelloSettings": { - "message": "Ekstra Windows Hello ayarları" - }, "unlockWithPolkit": { "message": "Unlock with system authentication" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "kasanızın kilidini açma" }, - "autoPromptWindowsHello": { - "message": "Uygulamayı başlatırken Windows Hello doğrulaması iste" - }, - "autoPromptPolkit": { - "message": "Ask for system authentication on launch" - }, "autoPromptTouchId": { "message": "Uygulamayı başlatırken Touch ID iste" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Uygulamayı başlatırken parola iste" }, - "recommendedForSecurity": { - "message": "Güvenliğiniz için önerilir." - }, "lockWithMasterPassOnRestart1": { "message": "Yeniden başlatmada ana parola ile kilitle" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 724959446d6..5a35187d9fb 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Шукати у сховищі" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "Додати запис" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Розблокувати з Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Додаткові налаштування Windows Hello" - }, "unlockWithPolkit": { "message": "Розблокувати за допомогою системної автентифікації" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "розблокувати сховище" }, - "autoPromptWindowsHello": { - "message": "Запитувати Windows Hello під час запуску" - }, - "autoPromptPolkit": { - "message": "Запитувати системну автентифікацію під час запуску" - }, "autoPromptTouchId": { "message": "Запитувати Touch ID під час запуску" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Вимагати пароль під час запуску" }, - "recommendedForSecurity": { - "message": "Рекомендовано для безпеки." - }, "lockWithMasterPassOnRestart1": { "message": "Блокувати головним паролем при перезапуску" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index d0ff3cca7bc..5232ca6f81e 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "Tìm kiếm trong Kho" }, + "resetSearch": { + "message": "Đặt lại tìm kiếm" + }, "addItem": { "message": "Thêm mục" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "Mở khóa với Windows Hello" }, - "additionalWindowsHelloSettings": { - "message": "Cài đặt bổ sung cho Windows Hello" - }, "unlockWithPolkit": { "message": "Mở khóa bằng bảo mật hệ thống" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "mở khoá kho của bạn" }, - "autoPromptWindowsHello": { - "message": "Yêu cầu xác minh Windows Hello khi mở ứng dụng" - }, - "autoPromptPolkit": { - "message": "Yêu cầu xác thực hệ thống khi khởi động" - }, "autoPromptTouchId": { "message": "Yêu cầu xác minh Touch ID khi mở ứng dụng" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Yêu cầu mật khẩu khi khởi động ứng dụng" }, - "recommendedForSecurity": { - "message": "Khuyến nghị để bảo mật." - }, "lockWithMasterPassOnRestart1": { "message": "Khóa bằng mật khẩu chính khi khởi động lại" }, @@ -3180,7 +3171,7 @@ "message": "Khu vực" }, "ssoIdentifierRequired": { - "message": "Cần có mã định danh Đăng nhập một lần của tổ chức." + "message": "Cần có mã định danh SSO của tổ chức." }, "eu": { "message": "Châu Âu", @@ -3539,7 +3530,7 @@ "message": "Yêu cầu xác thực LastPass" }, "awaitingSSO": { - "message": "Đang chờ xác thực Đăng nhập một lần" + "message": "Đang chờ xác thực SSO" }, "awaitingSSODesc": { "message": "Vui lòng tiếp tục đăng nhập bằng thông tin đăng nhập của công ty bạn." @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Bật phím tắt tự động điền" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden không kiểm tra vị trí nhập liệu, hãy đảm bảo bạn đang ở trong đúng cửa sổ và trường nhập liệu trước khi dùng phím tắt." diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index c0bb7e0f0d3..8337a71ecbe 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "搜索密码库" }, + "resetSearch": { + "message": "重置搜索" + }, "addItem": { "message": "添加项目" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "使用 Windows Hello 解锁" }, - "additionalWindowsHelloSettings": { - "message": "额外的 Windows Hello 设置" - }, "unlockWithPolkit": { "message": "使用系统身份验证解锁" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "解锁您的密码库" }, - "autoPromptWindowsHello": { - "message": "应用程序启动时提示 Windows Hello" - }, - "autoPromptPolkit": { - "message": "启动时提示系统身份验证" - }, "autoPromptTouchId": { "message": "应用程序启动时提示触控 ID" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "应用程序启动时要求密码" }, - "recommendedForSecurity": { - "message": "安全起见,推荐设置。" - }, "lockWithMasterPassOnRestart1": { "message": "重启后使用主密码锁定" }, @@ -3048,7 +3039,7 @@ "message": "刚刚" }, "requestedXMinutesAgo": { - "message": "$MINUTES$ 分钟前已发出请求", + "message": "请求于 $MINUTES$ 分钟前", "placeholders": { "minutes": { "content": "$1", @@ -3568,15 +3559,15 @@ "description": "Label indicating the most common import formats" }, "uriMatchDefaultStrategyHint": { - "message": "URI match detection is how Bitwarden identifies autofill suggestions.", + "message": "Bitwarden 根据 URI 匹配检测来识别自动填充建议。", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { - "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", + "message": "「正则表达方式」是一种高级选项,会增加暴露凭据的风险。", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, "startsWithAdvancedOptionWarning": { - "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", + "message": "「开始于」是一种高级选项,会增加暴露凭据的风险。", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, "uriMatchWarningDialogLink": { @@ -4015,10 +4006,10 @@ } } }, - "enableAutotype": { - "message": "启用自动类型快捷方式" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { - "message": "Bitwarden 不验证输入位置,请确保您在使用快捷键之前在正确的窗口和字段中。" + "message": "Bitwarden 不会验证输入位置,在使用快捷键之前,请确保您位于正确的窗口和字段中。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index c2253c56760..f823a8877e2 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -41,6 +41,9 @@ "searchVault": { "message": "搜尋密碼庫" }, + "resetSearch": { + "message": "Reset search" + }, "addItem": { "message": "新增項目" }, @@ -1813,9 +1816,6 @@ "unlockWithWindowsHello": { "message": "使用 Windows Hello 解鎖" }, - "additionalWindowsHelloSettings": { - "message": "額外的 Windows Hello 設定" - }, "unlockWithPolkit": { "message": "使用系統驗證解鎖" }, @@ -1831,12 +1831,6 @@ "touchIdConsentMessage": { "message": "解鎖您的密碼庫" }, - "autoPromptWindowsHello": { - "message": "啟動應用程式時詢問 Windows Hello" - }, - "autoPromptPolkit": { - "message": "在啟動時詢問系統驗證" - }, "autoPromptTouchId": { "message": "啟動應用程式時要求 Touch ID" }, @@ -1846,9 +1840,6 @@ "requirePasswordWithoutPinOnStart": { "message": "Require password on app start" }, - "recommendedForSecurity": { - "message": "為提升安全,建議使用。" - }, "lockWithMasterPassOnRestart1": { "message": "重啟後使用主密碼鎖定" }, @@ -4015,8 +4006,8 @@ } } }, - "enableAutotype": { - "message": "Enable autotype shortcut" + "enableAutotypeTransitionKey": { + "message": "Enable Autotype" }, "enableAutotypeDescription": { "message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut." From 1491445392701a04d4127c2b18e309a967df4d75 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 25 Jul 2025 12:21:15 +0200 Subject: [PATCH 051/179] [PM-24042] Migrate AC owned abstract services to strict TS (#15733) * Migrate remaining AC owned abstract services to strict TS * Remove now unused service --- .../abstractions/collection-admin.service.ts | 16 +++++++-------- .../abstractions/collection.service.ts | 20 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts index 083539e9f6e..61faabb16b8 100644 --- a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -4,20 +4,20 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CollectionAccessSelectionView, CollectionAdminView } from "../models"; export abstract class CollectionAdminService { - abstract getAll: (organizationId: string) => Promise; - abstract get: ( + abstract getAll(organizationId: string): Promise; + abstract get( organizationId: string, collectionId: string, - ) => Promise; - abstract save: ( + ): Promise; + abstract save( collection: CollectionAdminView, userId: UserId, - ) => Promise; - abstract delete: (organizationId: string, collectionId: string) => Promise; - abstract bulkAssignAccess: ( + ): Promise; + abstract delete(organizationId: string, collectionId: string): Promise; + abstract bulkAssignAccess( organizationId: string, collectionIds: string[], users: CollectionAccessSelectionView[], groups: CollectionAccessSelectionView[], - ) => Promise; + ): Promise; } diff --git a/libs/admin-console/src/common/collections/abstractions/collection.service.ts b/libs/admin-console/src/common/collections/abstractions/collection.service.ts index e69f96232da..dabaf078e16 100644 --- a/libs/admin-console/src/common/collections/abstractions/collection.service.ts +++ b/libs/admin-console/src/common/collections/abstractions/collection.service.ts @@ -7,25 +7,25 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionData, Collection, CollectionView } from "../models"; export abstract class CollectionService { - abstract encryptedCollections$: (userId: UserId) => Observable; - abstract decryptedCollections$: (userId: UserId) => Observable; - abstract upsert: (collection: CollectionData, userId: UserId) => Promise; - abstract replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise; + abstract encryptedCollections$(userId: UserId): Observable; + abstract decryptedCollections$(userId: UserId): Observable; + abstract upsert(collection: CollectionData, userId: UserId): Promise; + abstract replace(collections: { [id: string]: CollectionData }, userId: UserId): Promise; /** * @deprecated This method will soon be made private, use `decryptedCollections$` instead. */ - abstract decryptMany$: ( + abstract decryptMany$( collections: Collection[], orgKeys: Record, - ) => Observable; - abstract delete: (ids: CollectionId[], userId: UserId) => Promise; - abstract encrypt: (model: CollectionView, userId: UserId) => Promise; + ): Observable; + abstract delete(ids: CollectionId[], userId: UserId): Promise; + abstract encrypt(model: CollectionView, userId: UserId): Promise; /** * Transforms the input CollectionViews into TreeNodes */ - abstract getAllNested: (collections: CollectionView[]) => TreeNode[]; + abstract getAllNested(collections: CollectionView[]): TreeNode[]; /* * Transforms the input CollectionViews into TreeNodes and then returns the Treenode with the specified id */ - abstract getNested: (collections: CollectionView[], id: string) => TreeNode; + abstract getNested(collections: CollectionView[], id: string): TreeNode; } From 594455af8803615ec4315eff7ad6fee2940dbe6e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 25 Jul 2025 13:19:48 +0200 Subject: [PATCH 052/179] [PM-23099] Prevent private key regen / private key generation on v2 accounts (#15413) * Prevent private key regen / private key generation on v2 accounts * Fix tests * Fix build * Fix tests --- .../login-strategies/login.strategy.spec.ts | 10 ++++++++++ .../common/login-strategies/login.strategy.ts | 5 +++++ ...asymmetric-key-regeneration.service.spec.ts | 18 ++++++++++++++++++ ...user-asymmetric-key-regeneration.service.ts | 8 ++++++++ 4 files changed, 41 insertions(+) diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 78561b443a3..1a6592887ba 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -327,6 +327,7 @@ describe("LoginStrategy", () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.privateKey = null; keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); + keyService.getUserKey.mockResolvedValue(userKey); apiService.postIdentityToken.mockResolvedValue(tokenResponse); masterPasswordService.masterKeySubject.next(masterKey); @@ -343,6 +344,15 @@ describe("LoginStrategy", () => { expect(apiService.postAccountKeys).toHaveBeenCalled(); }); + + it("throws if userKey is CoseEncrypt0 (V2 encryption) in createKeyPairForOldAccount", async () => { + keyService.getUserKey.mockResolvedValue({ + inner: () => ({ type: 7 }), + } as UserKey); + await expect(passwordLoginStrategy["createKeyPairForOldAccount"](userId)).resolves.toBe( + undefined, + ); + }); }); describe("Two-factor authentication", () => { diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index b8d5f64bfcc..53e34147d9f 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -31,6 +31,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { @@ -326,6 +327,10 @@ export abstract class LoginStrategy { protected async createKeyPairForOldAccount(userId: UserId) { try { const userKey = await this.keyService.getUserKey(userId); + if (userKey.inner().type == EncryptionType.CoseEncrypt0) { + throw new Error("Cannot create key pair for account on V2 encryption"); + } + const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); if (!privateKey.encryptedString) { throw new Error("Failed to create encrypted private key"); diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts index abccfee4c59..3a622fe72c0 100644 --- a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts @@ -354,4 +354,22 @@ describe("regenerateIfNeeded", () => { ).not.toHaveBeenCalled(); expect(keyService.setPrivateKey).not.toHaveBeenCalled(); }); + + it("should not regenerate when userKey type is CoseEncrypt0 (V2 encryption)", async () => { + const mockUserKey = { + keyB64: "mockKeyB64", + inner: () => ({ type: 7 }), + } as unknown as UserKey; + keyService.userKey$.mockReturnValue(of(mockUserKey)); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + expect(logService.error).toHaveBeenCalledWith( + "[UserAsymmetricKeyRegeneration] Cannot regenerate asymmetric keys for accounts on V2 encryption.", + ); + }); }); diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts index 3e837237895..335f45b0ce2 100644 --- a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts @@ -6,6 +6,7 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -60,6 +61,13 @@ export class DefaultUserAsymmetricKeysRegenerationService return false; } + if (userKey.inner().type === EncryptionType.CoseEncrypt0) { + this.logService.error( + "[UserAsymmetricKeyRegeneration] Cannot regenerate asymmetric keys for accounts on V2 encryption.", + ); + return false; + } + const [userKeyEncryptedPrivateKey, publicKeyResponse] = await firstValueFrom( combineLatest([ this.keyService.userEncryptedPrivateKey$(userId), From 37987f4f972767f5f3607bbb250cc6a9d673f8d6 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Fri, 25 Jul 2025 08:34:39 -0400 Subject: [PATCH 053/179] [CL-801] Move popover in kitchen sink story to avoid scrolling (#15767) --- .../components/kitchen-sink-main.component.ts | 24 ++++++++++++++++++- .../kitchen-sink/kitchen-sink.stories.ts | 12 +++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index 767659de3cb..6083b3d66e1 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -105,7 +105,21 @@ class KitchenSinkDialog {

Bitwarden Kitchen Sink

- Learn more + This is a link +

+  and this is a link button popover trigger:  +

+
@@ -149,6 +163,14 @@ class KitchenSinkDialog { + + +
You can learn more things at:
+
    +
  • Help center
  • +
  • Support
  • +
+
`, }) export class KitchenSinkMainComponent { diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 671a8d9ad82..cb8a72e1b3f 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -6,7 +6,6 @@ import { userEvent, getAllByRole, getByRole, - getByLabelText, fireEvent, getByText, getAllByLabelText, @@ -155,16 +154,11 @@ export const PopoverOpen: Story = { render: Default.render, play: async (context) => { const canvas = context.canvasElement; - const passwordLabelIcon = getByLabelText(canvas, "A random password (required)", { - selector: "button", + const popoverLink = getByRole(canvas, "button", { + name: "Popover trigger link", }); - await userEvent.click(passwordLabelIcon); - }, - parameters: { - chromatic: { - disableSnapshot: true, - }, + await userEvent.click(popoverLink); }, }; From b358d5663d788be59e84dbb619cae94717ec729d Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:43:41 -0400 Subject: [PATCH 054/179] [PM-23822] [PM-23823] Organization integration and configuration api services (#15763) * Adding the organization integration api service and test cases * Adding configuration api files and test cases. Fixing the id guids and integration type and event type nullable * Adding get endpoint methods to the integration and config service and test cases * fixing type check issues * lowercase directory name --- .../bit-common/src/dirt/integrations/index.ts | 1 + ...ation-integration-configuration-request.ts | 20 +++ ...tion-integration-configuration-response.ts | 20 +++ .../organization-integration-request.ts | 11 ++ .../organization-integration-response.ts | 15 ++ .../organization-integration-service-type.ts | 6 + .../models/organization-integration-type.ts | 10 ++ .../src/dirt/integrations/services/index.ts | 2 + ...ganization-integration-api.service.spec.ts | 115 +++++++++++++++ .../organization-integration-api.service.ts | 67 +++++++++ ...egration-configuration-api.service.spec.ts | 132 ++++++++++++++++++ ...n-integration-configuration-api.service.ts | 75 ++++++++++ libs/common/src/types/guid.ts | 5 + 13 files changed, 479 insertions(+) create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/index.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-request.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-response.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-service-type.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-type.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/services/index.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.spec.ts create mode 100644 bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.ts diff --git a/bitwarden_license/bit-common/src/dirt/integrations/index.ts b/bitwarden_license/bit-common/src/dirt/integrations/index.ts new file mode 100644 index 00000000000..b2221a94a89 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/index.ts @@ -0,0 +1 @@ +export * from "./services"; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-request.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-request.ts new file mode 100644 index 00000000000..58c59304479 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-request.ts @@ -0,0 +1,20 @@ +import { EventType } from "@bitwarden/common/enums"; + +export class OrganizationIntegrationConfigurationRequest { + eventType?: EventType | null = null; + configuration?: string | null = null; + filters?: string | null = null; + template?: string | null = null; + + constructor( + eventType?: EventType | null, + configuration?: string | null, + filters?: string | null, + template?: string | null, + ) { + this.eventType = eventType; + this.configuration = configuration; + this.filters = filters; + this.template = template; + } +} diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-response.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-response.ts new file mode 100644 index 00000000000..47baf3276ad --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-configuration-response.ts @@ -0,0 +1,20 @@ +import { EventType } from "@bitwarden/common/enums"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { OrganizationIntegrationConfigurationId } from "@bitwarden/common/types/guid"; + +export class OrganizationIntegrationConfigurationResponse extends BaseResponse { + id: OrganizationIntegrationConfigurationId; + eventType?: EventType; + configuration?: string; + filters?: string; + template?: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.eventType = this.getResponseProperty("EventType"); + this.configuration = this.getResponseProperty("Configuration"); + this.filters = this.getResponseProperty("Filters"); + this.template = this.getResponseProperty("Template"); + } +} diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts new file mode 100644 index 00000000000..95f7d180dae --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-request.ts @@ -0,0 +1,11 @@ +import { OrganizationIntegrationType } from "./organization-integration-type"; + +export class OrganizationIntegrationRequest { + type: OrganizationIntegrationType; + configuration?: string; + + constructor(integrationType: OrganizationIntegrationType, configuration?: string) { + this.type = integrationType; + this.configuration = configuration; + } +} diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts new file mode 100644 index 00000000000..00880ea4740 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { OrganizationIntegrationId } from "@bitwarden/common/types/guid"; + +import { OrganizationIntegrationType } from "./organization-integration-type"; + +export class OrganizationIntegrationResponse extends BaseResponse { + id: OrganizationIntegrationId; + organizationIntegrationType: OrganizationIntegrationType; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.organizationIntegrationType = this.getResponseProperty("Type"); + } +} diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-service-type.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-service-type.ts new file mode 100644 index 00000000000..dd1b4fb3f6c --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-service-type.ts @@ -0,0 +1,6 @@ +export const OrganizationIntegrationServiceType = Object.freeze({ + CrowdStrike: "CrowdStrike", +} as const); + +export type OrganizationIntegrationServiceType = + (typeof OrganizationIntegrationServiceType)[keyof typeof OrganizationIntegrationServiceType]; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-type.ts b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-type.ts new file mode 100644 index 00000000000..1c98e174836 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/models/organization-integration-type.ts @@ -0,0 +1,10 @@ +export const OrganizationIntegrationType = Object.freeze({ + CloudBillingSync: 1, + Scim: 2, + Slack: 3, + Webhook: 4, + Hec: 5, +} as const); + +export type OrganizationIntegrationType = + (typeof OrganizationIntegrationType)[keyof typeof OrganizationIntegrationType]; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/index.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/index.ts new file mode 100644 index 00000000000..68a673854ae --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/index.ts @@ -0,0 +1,2 @@ +export * from "./organization-integration-api.service"; +export * from "./organization-integration-configuration-api.service"; diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts new file mode 100644 index 00000000000..bf3e16ed430 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.spec.ts @@ -0,0 +1,115 @@ +import { mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid"; + +import { OrganizationIntegrationRequest } from "../models/organization-integration-request"; +import { OrganizationIntegrationServiceType } from "../models/organization-integration-service-type"; +import { OrganizationIntegrationType } from "../models/organization-integration-type"; + +import { OrganizationIntegrationApiService } from "./organization-integration-api.service"; + +export const mockIntegrationResponse: any = { + id: "1" as OrganizationIntegrationId, + organizationIntegrationType: OrganizationIntegrationType.Hec, +}; + +export const mockIntegrationResponses: any[] = [ + { + id: "1" as OrganizationIntegrationId, + OrganizationIntegrationType: OrganizationIntegrationType.Hec, + }, + { + id: "2" as OrganizationIntegrationId, + OrganizationIntegrationType: OrganizationIntegrationType.Webhook, + }, +]; + +describe("OrganizationIntegrationApiService", () => { + let service: OrganizationIntegrationApiService; + const apiService = mock(); + + beforeEach(() => { + service = new OrganizationIntegrationApiService(apiService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should call apiService.send with correct parameters for getOrganizationIntegrations", async () => { + const orgId = "org1" as OrganizationId; + + apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponses)); + + const result = await service.getOrganizationIntegrations(orgId); + expect(result).toEqual(mockIntegrationResponses); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `organizations/${orgId}/integrations`, + null, + true, + true, + ); + }); + + it("should call apiService.send with correct parameters for createOrganizationIntegration", async () => { + const request = new OrganizationIntegrationRequest( + OrganizationIntegrationType.Hec, + `{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceType.CrowdStrike}' }`, + ); + const orgId = "org1" as OrganizationId; + + apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); + + const result = await service.createOrganizationIntegration(orgId, request); + expect(result.organizationIntegrationType).toEqual( + mockIntegrationResponse.organizationIntegrationType, + ); + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `organizations/${orgId.toString()}/integrations`, + request, + true, + true, + ); + }); + + it("should call apiService.send with the correct parameters for updateOrganizationIntegration", async () => { + const request = new OrganizationIntegrationRequest( + OrganizationIntegrationType.Hec, + `{ 'uri:' 'test.com', 'scheme:' 'bearer', 'token:' '123456789', 'service:' '${OrganizationIntegrationServiceType.CrowdStrike}' }`, + ); + const orgId = "org1" as OrganizationId; + const integrationId = "integration1" as OrganizationIntegrationId; + + apiService.send.mockReturnValue(Promise.resolve(mockIntegrationResponse)); + + const result = await service.updateOrganizationIntegration(orgId, integrationId, request); + expect(result.organizationIntegrationType).toEqual( + mockIntegrationResponse.organizationIntegrationType, + ); + expect(apiService.send).toHaveBeenCalledWith( + "PUT", + `organizations/${orgId}/integrations/${integrationId}`, + request, + true, + true, + ); + }); + + it("should call apiService.send with the correct parameters for deleteOrganizationIntegration", async () => { + const orgId = "org1" as OrganizationId; + const integrationId = "integration1" as OrganizationIntegrationId; + + await service.deleteOrganizationIntegration(orgId, integrationId); + + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + `organizations/${orgId}/integrations/${integrationId}`, + null, + true, + false, + ); + }); +}); diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts new file mode 100644 index 00000000000..5cf8efefb05 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-api.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId, OrganizationIntegrationId } from "@bitwarden/common/types/guid"; + +import { OrganizationIntegrationRequest } from "../models/organization-integration-request"; +import { OrganizationIntegrationResponse } from "../models/organization-integration-response"; + +@Injectable() +export class OrganizationIntegrationApiService { + constructor(private apiService: ApiService) {} + + async getOrganizationIntegrations( + orgId: OrganizationId, + ): Promise { + const response = await this.apiService.send( + "GET", + `organizations/${orgId}/integrations`, + null, + true, + true, + ); + return response; + } + + async createOrganizationIntegration( + orgId: OrganizationId, + request: OrganizationIntegrationRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + `organizations/${orgId}/integrations`, + request, + true, + true, + ); + return response; + } + + async updateOrganizationIntegration( + orgId: OrganizationId, + integrationId: OrganizationIntegrationId, + request: OrganizationIntegrationRequest, + ): Promise { + const response = await this.apiService.send( + "PUT", + `organizations/${orgId}/integrations/${integrationId}`, + request, + true, + true, + ); + return response; + } + + async deleteOrganizationIntegration( + orgId: OrganizationId, + integrationId: OrganizationIntegrationId, + ): Promise { + await this.apiService.send( + "DELETE", + `organizations/${orgId}/integrations/${integrationId}`, + null, + true, + false, + ); + } +} diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.spec.ts new file mode 100644 index 00000000000..48612efdd13 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.spec.ts @@ -0,0 +1,132 @@ +import { mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + OrganizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, +} from "@bitwarden/common/types/guid"; + +import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request"; + +import { OrganizationIntegrationConfigurationApiService } from "./organization-integration-configuration-api.service"; + +export const mockConfigurationResponse: any = { + id: "1" as OrganizationIntegrationConfigurationId, + template: "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }", +}; + +export const mockConfigurationResponses: any[] = [ + { + id: "1" as OrganizationIntegrationConfigurationId, + template: "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }", + }, + { + id: "2" as OrganizationIntegrationConfigurationId, + template: "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'otherIndex' }", + }, +]; + +describe("OrganizationIntegrationConfigurationApiService", () => { + let service: OrganizationIntegrationConfigurationApiService; + const apiService = mock(); + + beforeEach(() => { + service = new OrganizationIntegrationConfigurationApiService(apiService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should call apiService.send with correct parameters for getOrganizationIntegrationConfigurations", async () => { + const orgId = "org1" as OrganizationId; + const integrationId = "integration1" as OrganizationIntegrationId; + + apiService.send.mockReturnValue(Promise.resolve(mockConfigurationResponses)); + + const result = await service.getOrganizationIntegrationConfigurations(orgId, integrationId); + expect(result).toEqual(mockConfigurationResponses); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `organizations/${orgId}/integrations/${integrationId}/configurations`, + null, + true, + true, + ); + }); + + it("should call apiService.send with correct parameters for createOrganizationIntegrationConfiguration", async () => { + const request = new OrganizationIntegrationConfigurationRequest( + null, + null, + null, + "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }", + ); + const orgId = "org1" as OrganizationId; + const integrationId = "integration1" as OrganizationIntegrationId; + + apiService.send.mockReturnValue(Promise.resolve(mockConfigurationResponse)); + + const result = await service.createOrganizationIntegrationConfiguration( + orgId, + integrationId, + request, + ); + expect(result.eventType).toEqual(mockConfigurationResponse.eventType); + expect(result.template).toEqual(mockConfigurationResponse.template); + expect(apiService.send).toHaveBeenCalledWith( + "POST", + `organizations/${orgId}/integrations/${integrationId}/configurations`, + request, + true, + true, + ); + }); + + it("should call apiService.send with correct parameters for updateOrganizationIntegrationConfiguration", async () => { + const request = new OrganizationIntegrationConfigurationRequest( + null, + null, + null, + "{ 'event': '#EventMessage#', 'source': 'Bitwarden', 'index': 'testIndex' }", + ); + const orgId = "org1" as OrganizationId; + const integrationId = "integration1" as OrganizationIntegrationId; + const configurationId = "configurationId" as OrganizationIntegrationConfigurationId; + + apiService.send.mockReturnValue(Promise.resolve(mockConfigurationResponse)); + + const result = await service.updateOrganizationIntegrationConfiguration( + orgId, + integrationId, + configurationId, + request, + ); + expect(result.eventType).toEqual(mockConfigurationResponse.eventType); + expect(result.template).toEqual(mockConfigurationResponse.template); + expect(apiService.send).toHaveBeenCalledWith( + "PUT", + `organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`, + request, + true, + true, + ); + }); + + it("should call apiService.send with correct parameters for deleteOrganizationIntegrationConfiguration", async () => { + const orgId = "org1" as OrganizationId; + const integrationId = "integration1" as OrganizationIntegrationId; + const configurationId = "configurationId" as OrganizationIntegrationConfigurationId; + + await service.deleteOrganizationIntegrationConfiguration(orgId, integrationId, configurationId); + + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + `organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`, + null, + true, + false, + ); + }); +}); diff --git a/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.ts b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.ts new file mode 100644 index 00000000000..b5bac73b280 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/integrations/services/organization-integration-configuration-api.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + OrganizationId, + OrganizationIntegrationConfigurationId, + OrganizationIntegrationId, +} from "@bitwarden/common/types/guid"; + +import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request"; +import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response"; + +@Injectable() +export class OrganizationIntegrationConfigurationApiService { + constructor(private apiService: ApiService) {} + + async getOrganizationIntegrationConfigurations( + orgId: OrganizationId, + integrationId: OrganizationIntegrationId, + ): Promise { + const responses = await this.apiService.send( + "GET", + `organizations/${orgId}/integrations/${integrationId}/configurations`, + null, + true, + true, + ); + return responses; + } + + async createOrganizationIntegrationConfiguration( + orgId: OrganizationId, + integrationId: OrganizationIntegrationId, + request: OrganizationIntegrationConfigurationRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + `organizations/${orgId}/integrations/${integrationId}/configurations`, + request, + true, + true, + ); + return response; + } + + async updateOrganizationIntegrationConfiguration( + orgId: OrganizationId, + integrationId: OrganizationIntegrationId, + configurationId: OrganizationIntegrationConfigurationId, + request: OrganizationIntegrationConfigurationRequest, + ): Promise { + const response = await this.apiService.send( + "PUT", + `organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`, + request, + true, + true, + ); + return response; + } + + async deleteOrganizationIntegrationConfiguration( + orgId: OrganizationId, + integrationId: OrganizationIntegrationId, + configurationId: OrganizationIntegrationConfigurationId, + ): Promise { + await this.apiService.send( + "DELETE", + `organizations/${orgId}/integrations/${integrationId}/configurations/${configurationId}`, + null, + true, + false, + ); + } +} diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 5edd34e4fc5..bd0980cd36c 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -15,3 +15,8 @@ export type IndexedEntityId = Opaque; export type SecurityTaskId = Opaque; export type NotificationId = Opaque; export type EmergencyAccessId = Opaque; +export type OrganizationIntegrationId = Opaque; +export type OrganizationIntegrationConfigurationId = Opaque< + string, + "OrganizationIntegrationConfigurationId" +>; From 2db31d122881517b9a0adb8137f2dac663c2928d Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:37:04 -0500 Subject: [PATCH 055/179] [PM-22611] Require userid for masterKey methods on the key service (#15663) * Require userId on targeted methods. * update method consumers * unit tests --- apps/cli/src/auth/commands/login.command.ts | 3 +- .../account/change-email.component.spec.ts | 2 +- .../change-kdf-confirmation.component.ts | 19 +- .../src/abstractions/key.service.ts | 22 +- libs/key-management/src/key.service.spec.ts | 215 +++++++++++++++--- libs/key-management/src/key.service.ts | 60 +++-- 6 files changed, 233 insertions(+), 88 deletions(-) diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 3ceac859c43..d25e9a70d88 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -428,7 +428,8 @@ export class LoginCommand { ); const request = new PasswordRequest(); - request.masterPasswordHash = await this.keyService.hashMasterKey(currentPassword, null); + const masterKey = await this.keyService.getOrDeriveMasterKey(currentPassword, userId); + request.masterPasswordHash = await this.keyService.hashMasterKey(currentPassword, masterKey); request.masterPasswordHint = hint; request.newMasterPasswordHash = newPasswordHash; request.key = newUserKey[1].encryptedString; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.spec.ts b/apps/web/src/app/auth/settings/account/change-email.component.spec.ts index f5c0733e5b0..bd0d9df9f06 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.spec.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.spec.ts @@ -89,7 +89,7 @@ describe("ChangeEmailComponent", () => { }); keyService.getOrDeriveMasterKey - .calledWith("password", "UserId") + .calledWith("password", "UserId" as UserId) .mockResolvedValue("getOrDeriveMasterKey" as any); keyService.hashMasterKey .calledWith("password", "getOrDeriveMasterKey" as any) diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0bfc46eea96..5aa8eeb907c 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -2,14 +2,13 @@ // @ts-strict-ignore import { Component, Inject } from "@angular/core"; import { FormGroup, FormControl, Validators } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DIALOG_DATA, ToastService } from "@bitwarden/components"; import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management"; @@ -31,7 +30,6 @@ export class ChangeKdfConfirmationComponent { constructor( private apiService: ApiService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private keyService: KeyService, private messagingService: MessagingService, @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, @@ -58,6 +56,10 @@ export class ChangeKdfConfirmationComponent { }; private async makeKeyAndSaveAsync() { + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount == null) { + throw new Error("No active account found."); + } const masterPassword = this.form.value.masterPassword; // Ensure the KDF config is valid. @@ -70,13 +72,14 @@ export class ChangeKdfConfirmationComponent { request.kdfMemory = this.kdfConfig.memory; request.kdfParallelism = this.kdfConfig.parallelism; } - const masterKey = await this.keyService.getOrDeriveMasterKey(masterPassword); + const masterKey = await this.keyService.getOrDeriveMasterKey(masterPassword, activeAccount.id); request.masterPasswordHash = await this.keyService.hashMasterKey(masterPassword, masterKey); - const email = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); - const newMasterKey = await this.keyService.makeMasterKey(masterPassword, email, this.kdfConfig); + const newMasterKey = await this.keyService.makeMasterKey( + masterPassword, + activeAccount.email, + this.kdfConfig, + ); request.newMasterPasswordHash = await this.keyService.hashMasterKey( masterPassword, newMasterKey, diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index 3c0d6c8a138..3e2fbf6c63b 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -163,11 +163,14 @@ export abstract class KeyService { */ abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId: string): Promise; /** - * @throws Error when userId is null and no active user + * Retrieves the user's master key if it is in state, or derives it from the provided password * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user + * @throws Error when userId is null/undefined. + * @throws Error when email or Kdf configuration cannot be found for the user. + * @returns The user's master key if it exists, or a newly derived master key. */ - abstract getOrDeriveMasterKey(password: string, userId?: string): Promise; + abstract getOrDeriveMasterKey(password: string, userId: UserId): Promise; /** * Generates a master key from the provided password * @param password The user's master password @@ -175,7 +178,7 @@ export abstract class KeyService { * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise; + abstract makeMasterKey(password: string, email: string, kdfConfig: KdfConfig): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -191,24 +194,25 @@ export abstract class KeyService { * Creates a master password hash from the user's master password. Can * be used for local authentication or for server authentication depending * on the hashPurpose provided. - * @throws Error when password is null or key is null and no active user or active user have no master key * @param password The user's master password * @param key The user's master key or active's user master key. - * @param hashPurpose The iterations to use for the hash + * @param hashPurpose The iterations to use for the hash. Defaults to {@link HashPurpose.ServerAuthorization}. + * @throws Error when password is null/undefined or key is null/undefined. * @returns The user's master password hash */ abstract hashMasterKey( password: string, - key: MasterKey | null, + key: MasterKey, hashPurpose?: HashPurpose, ): Promise; /** * Compares the provided master password to the stored password hash. * @param masterPassword The user's master password - * @param key The user's master key + * @param masterKey The user's master key * @param userId The id of the user to do the operation for. - * @returns True if the provided master password matches either the stored - * key hash or the server key hash + * @throws Error when master key is null/undefined. + * @returns True if the derived master password hash matches the stored + * key hash, false otherwise. */ abstract compareKeyHash( masterPassword: string, diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 7a033792c79..27d838c6fb3 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -18,7 +18,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/ke import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -47,6 +47,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { KdfConfigService } from "./abstractions/kdf-config.service"; import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service"; import { DefaultKeyService } from "./key.service"; +import { KdfConfig } from "./models/kdf-config"; describe("keyService", () => { let keyService: DefaultKeyService; @@ -817,55 +818,160 @@ describe("keyService", () => { }); describe("getOrDeriveMasterKey", () => { + beforeEach(() => { + masterPasswordService.masterKeySubject.next(null); + }); + + test.each([null as unknown as UserId, undefined as unknown as UserId])( + "throws when the provided userId is %s", + async (userId) => { + await expect(keyService.getOrDeriveMasterKey("password", userId)).rejects.toThrow( + "User ID is required.", + ); + }, + ); + it("returns the master key if it is already available", async () => { - const getMasterKey = jest - .spyOn(masterPasswordService, "masterKey$") - .mockReturnValue(of("masterKey" as any)); + const masterKey = makeSymmetricCryptoKey(32) as MasterKey; + masterPasswordService.masterKeySubject.next(masterKey); const result = await keyService.getOrDeriveMasterKey("password", mockUserId); - expect(getMasterKey).toHaveBeenCalledWith(mockUserId); - expect(result).toEqual("masterKey"); + expect(kdfConfigService.getKdfConfig$).not.toHaveBeenCalledWith(mockUserId); + expect(result).toEqual(masterKey); }); - it("derives the master key if it is not available", async () => { - const getMasterKey = jest - .spyOn(masterPasswordService, "masterKey$") - .mockReturnValue(of(null as any)); + it("throws an error if user's email is not available", async () => { + accountService.accounts$ = of({}); - const deriveKeyFromPassword = jest - .spyOn(keyGenerationService, "deriveKeyFromPassword") - .mockResolvedValue("mockMasterKey" as any); - - kdfConfigService.getKdfConfig$.mockReturnValue(of("mockKdfConfig" as any)); - - const result = await keyService.getOrDeriveMasterKey("password", mockUserId); - - expect(getMasterKey).toHaveBeenCalledWith(mockUserId); - expect(deriveKeyFromPassword).toHaveBeenCalledWith("password", "email", "mockKdfConfig"); - expect(result).toEqual("mockMasterKey"); - }); - - it("throws an error if no user is found", async () => { - accountService.activeAccountSubject.next(null); - - await expect(keyService.getOrDeriveMasterKey("password")).rejects.toThrow("No user found"); + await expect(keyService.getOrDeriveMasterKey("password", mockUserId)).rejects.toThrow( + "No email found for user " + mockUserId, + ); + expect(kdfConfigService.getKdfConfig$).not.toHaveBeenCalled(); }); it("throws an error if no kdf config is found", async () => { - jest.spyOn(masterPasswordService, "masterKey$").mockReturnValue(of(null as any)); kdfConfigService.getKdfConfig$.mockReturnValue(of(null)); await expect(keyService.getOrDeriveMasterKey("password", mockUserId)).rejects.toThrow( "No kdf found for user", ); }); + + it("derives the master key if it is not available", async () => { + keyGenerationService.deriveKeyFromPassword.mockReturnValue("mockMasterKey" as any); + kdfConfigService.getKdfConfig$.mockReturnValue(of("mockKdfConfig" as any)); + + const result = await keyService.getOrDeriveMasterKey("password", mockUserId); + + expect(kdfConfigService.getKdfConfig$).toHaveBeenCalledWith(mockUserId); + expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith( + "password", + "email", + "mockKdfConfig", + ); + expect(result).toEqual("mockMasterKey"); + }); + }); + + describe("makeMasterKey", () => { + const password = "testPassword"; + let email = "test@example.com"; + const masterKey = makeSymmetricCryptoKey(32) as MasterKey; + const kdfConfig = mock(); + + it("derives a master key from password and email", async () => { + keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey); + + const result = await keyService.makeMasterKey(password, email, kdfConfig); + + expect(result).toEqual(masterKey); + }); + + it("trims and lowercases the email for key generation call", async () => { + keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey); + email = "TEST@EXAMPLE.COM"; + + await keyService.makeMasterKey(password, email, kdfConfig); + + expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith( + password, + email.trim().toLowerCase(), + kdfConfig, + ); + }); + + it("should log the time taken to derive the master key", async () => { + keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey); + jest.spyOn(Date.prototype, "getTime").mockReturnValueOnce(1000).mockReturnValueOnce(1500); + + await keyService.makeMasterKey(password, email, kdfConfig); + + expect(logService.info).toHaveBeenCalledWith("[KeyService] Deriving master key took 500ms"); + }); + }); + + describe("hashMasterKey", () => { + const password = "testPassword"; + const masterKey = makeSymmetricCryptoKey(32) as MasterKey; + + test.each([null as unknown as string, undefined as unknown as string])( + "throws when the provided password is %s", + async (password) => { + await expect(keyService.hashMasterKey(password, masterKey)).rejects.toThrow( + "password is required.", + ); + }, + ); + + test.each([null as unknown as MasterKey, undefined as unknown as MasterKey])( + "throws when the provided key is %s", + async (key) => { + await expect(keyService.hashMasterKey("password", key)).rejects.toThrow("key is required."); + }, + ); + + it("hashes master key with default iterations when no hashPurpose is provided", async () => { + const mockReturnedHashB64 = "bXlfaGFzaA=="; + cryptoFunctionService.pbkdf2.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHashB64)); + + const result = await keyService.hashMasterKey(password, masterKey); + + expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith( + masterKey.inner().encryptionKey, + password, + "sha256", + 1, + ); + expect(result).toBe(mockReturnedHashB64); + }); + + test.each([ + [2, HashPurpose.LocalAuthorization], + [1, HashPurpose.ServerAuthorization], + ])( + "hashes master key with %s iterations when hashPurpose is %s", + async (expectedIterations, hashPurpose) => { + const mockReturnedHashB64 = "bXlfaGFzaA=="; + cryptoFunctionService.pbkdf2.mockResolvedValue(Utils.fromB64ToArray(mockReturnedHashB64)); + + const result = await keyService.hashMasterKey(password, masterKey, hashPurpose); + + expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith( + masterKey.inner().encryptionKey, + password, + "sha256", + expectedIterations, + ); + expect(result).toBe(mockReturnedHashB64); + }, + ); }); describe("compareKeyHash", () => { type TestCase = { masterKey: MasterKey; - masterPassword: string | null; + masterPassword: string; storedMasterKeyHash: string | null; mockReturnedHash: string; expectedToMatch: boolean; @@ -873,26 +979,33 @@ describe("keyService", () => { const data: TestCase[] = [ { - masterKey: makeSymmetricCryptoKey(64), + masterKey: makeSymmetricCryptoKey(32), masterPassword: "my_master_password", storedMasterKeyHash: "bXlfaGFzaA==", mockReturnedHash: "bXlfaGFzaA==", expectedToMatch: true, }, { - masterKey: makeSymmetricCryptoKey(64), - masterPassword: null, + masterKey: makeSymmetricCryptoKey(32), + masterPassword: null as unknown as string, storedMasterKeyHash: "bXlfaGFzaA==", mockReturnedHash: "bXlfaGFzaA==", expectedToMatch: false, }, { - masterKey: makeSymmetricCryptoKey(64), - masterPassword: null, + masterKey: makeSymmetricCryptoKey(32), + masterPassword: null as unknown as string, storedMasterKeyHash: null, mockReturnedHash: "bXlfaGFzaA==", expectedToMatch: false, }, + { + masterKey: makeSymmetricCryptoKey(32), + masterPassword: "my_master_password", + storedMasterKeyHash: "bXlfaGFzaA==", + mockReturnedHash: "zxccbXlfaGFzaA==", + expectedToMatch: false, + }, ]; it.each(data)( @@ -907,7 +1020,7 @@ describe("keyService", () => { masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash); cryptoFunctionService.pbkdf2 - .calledWith(masterKey.inner().encryptionKey, masterPassword as string, "sha256", 2) + .calledWith(masterKey.inner().encryptionKey, masterPassword, "sha256", 2) .mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash)); const actualDidMatch = await keyService.compareKeyHash( @@ -919,6 +1032,38 @@ describe("keyService", () => { expect(actualDidMatch).toBe(expectedToMatch); }, ); + + test.each([null as unknown as MasterKey, undefined as unknown as MasterKey])( + "throws an error if masterKey is %s", + async (masterKey) => { + await expect( + keyService.compareKeyHash("my_master_password", masterKey, mockUserId), + ).rejects.toThrow("'masterKey' is required to be non-null."); + }, + ); + + test.each([null as unknown as string, undefined as unknown as string])( + "returns false when masterPassword is %s", + async (masterPassword) => { + const result = await keyService.compareKeyHash( + masterPassword, + makeSymmetricCryptoKey(32), + mockUserId, + ); + expect(result).toBe(false); + }, + ); + + it("returns false when storedMasterKeyHash is null", async () => { + masterPasswordService.masterKeyHashSubject.next(null); + + const result = await keyService.compareKeyHash( + "my_master_password", + makeSymmetricCryptoKey(32), + mockUserId, + ); + expect(result).toBe(false); + }); }); describe("userPrivateKey$", () => { diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 0f4b101d9b2..7cdc104c36a 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -259,28 +259,28 @@ export class DefaultKeyService implements KeyServiceAbstraction { } } - // TODO: Move to MasterPasswordService - async getOrDeriveMasterKey(password: string, userId?: UserId) { - const [resolvedUserId, email] = await firstValueFrom( - combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe( - map(([activeAccount, accounts]) => { - userId ??= activeAccount?.id; - if (userId == null || accounts[userId] == null) { - throw new Error("No user found"); - } - return [userId, accounts[userId].email]; - }), - ), - ); - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(resolvedUserId)); + async getOrDeriveMasterKey(password: string, userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey != null) { return masterKey; } - const kdf = await firstValueFrom(this.kdfConfigService.getKdfConfig$(resolvedUserId)); - if (kdf == null) { - throw new Error("No kdf found for user"); + const email = await firstValueFrom( + this.accountService.accounts$.pipe(map((accounts) => accounts[userId]?.email)), + ); + if (email == null) { + throw new Error("No email found for user " + userId); } + + const kdf = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId)); + if (kdf == null) { + throw new Error("No kdf found for user " + userId); + } + return await this.makeMasterKey(password, email, kdf); } @@ -289,14 +289,14 @@ export class DefaultKeyService implements KeyServiceAbstraction { * * @remarks * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. - * TODO: Move to MasterPasswordService */ - async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise { + async makeMasterKey(password: string, email: string, kdfConfig: KdfConfig): Promise { const start = new Date().getTime(); + email = email.trim().toLowerCase(); const masterKey = (await this.keyGenerationService.deriveKeyFromPassword( password, email, - KdfConfig, + kdfConfig, )) as MasterKey; const end = new Date().getTime(); this.logService.info(`[KeyService] Deriving master key took ${end - start}ms`); @@ -312,23 +312,16 @@ export class DefaultKeyService implements KeyServiceAbstraction { return await this.buildProtectedSymmetricKey(masterKey, userKey); } - // TODO: move to MasterPasswordService async hashMasterKey( password: string, - key: MasterKey | null, + key: MasterKey, hashPurpose?: HashPurpose, ): Promise { - if (key == null) { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - if (userId == null) { - throw new Error("No active user found."); - } - - key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + if (password == null) { + throw new Error("password is required."); } - - if (password == null || key == null) { - throw new Error("Invalid parameters."); + if (key == null) { + throw new Error("key is required."); } const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1; @@ -341,9 +334,8 @@ export class DefaultKeyService implements KeyServiceAbstraction { return Utils.fromBufferToB64(hash); } - // TODO: move to MasterPasswordService async compareKeyHash( - masterPassword: string | null, + masterPassword: string, masterKey: MasterKey, userId: UserId, ): Promise { From 22b8fc5f7d9967bcd155990bc28d339d06bcfef2 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 25 Jul 2025 17:52:01 +0200 Subject: [PATCH 056/179] [CL-660] Forbid non tailwind classes from web and libs (#14422) * Forbid non tailwind classes from web and libs * Ignore vault filter section --- .../vault-filter-section.component.html | 1 + eslint.config.mjs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html index 1485c1f5343..01a38a02d51 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html @@ -1,3 +1,4 @@ +
- - - {{ - "vaultTimeoutPolicyWithActionInEffect" - | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) - }} - - - {{ - "vaultTimeoutPolicyInEffect" - | i18n: policy.timeout.hours : policy.timeout.minutes - }} - - - {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} - - - - -
- -
- -
- {{ - "vaultTimeoutActionLockDesc" | i18n - }} -
- -
- {{ - "vaultTimeoutActionLogOutDesc" | i18n - }} -
-
+ +

{{ "vaultTimeoutHeader" | i18n }}

+
+ + - {{ - "unlockMethodNeededToChangeTimeoutActionDesc" | i18n - }} -
-
-
+ + + + {{ "vaultTimeoutAction1" | i18n }} + + + + + + + {{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
+
+
+ + + {{ "vaultTimeoutPolicyAffectingOptions" | i18n }} + + +