diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 99bea676bfb..e9360c73ab9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -85,9 +85,13 @@ apps/web/src/app/shared @bitwarden/team-platform-dev apps/web/src/translation-constants.ts @bitwarden/team-platform-dev # Workflows .github/workflows/brew-bump-desktop.yml @bitwarden/team-platform-dev +.github/workflows/build-browser-target.yml @bitwarden/team-platform-dev .github/workflows/build-browser.yml @bitwarden/team-platform-dev +.github/workflows/build-cli-target.yml @bitwarden/team-platform-dev .github/workflows/build-cli.yml @bitwarden/team-platform-dev +.github/workflows/build-desktop-target.yml @bitwarden/team-platform-dev .github/workflows/build-desktop.yml @bitwarden/team-platform-dev +.github/workflows/build-web-target.yml @bitwarden/team-platform-dev .github/workflows/build-web.yml @bitwarden/team-platform-dev .github/workflows/chromatic.yml @bitwarden/team-platform-dev .github/workflows/lint.yml @bitwarden/team-platform-dev diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml new file mode 100644 index 00000000000..11a268466f1 --- /dev/null +++ b/.github/workflows/build-browser-target.yml @@ -0,0 +1,39 @@ +name: Build Browser on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/browser/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + workflow_call: + inputs: {} + workflow_dispatch: + inputs: + sdk_branch: + description: "Custom SDK branch" + required: false + type: string + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build Browser on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-browser.yml + secrets: inherit + diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 7740e418e7b..56a980bf0f9 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,7 +1,7 @@ name: Build Browser on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -38,19 +38,14 @@ defaults: shell: bash jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -74,6 +69,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - 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 != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + locales-test: name: Locales Test @@ -281,6 +284,7 @@ jobs: needs: - setup - locales-test + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml new file mode 100644 index 00000000000..658d8f922ba --- /dev/null +++ b/.github/workflows/build-cli-target.yml @@ -0,0 +1,39 @@ +name: Build CLI on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/cli/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + - '.github/workflows/build-cli.yml' + - 'bitwarden_license/bit-cli/**' + workflow_dispatch: + inputs: + sdk_branch: + description: "Custom SDK branch" + required: false + type: string + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build CLI on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-cli.yml + secrets: inherit + diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d480879fb15..35970a8b307 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,7 +1,7 @@ name: Build CLI on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -27,6 +27,8 @@ on: - '!*.txt' - '.github/workflows/build-cli.yml' - 'bitwarden_license/bit-cli/**' + workflow_call: + inputs: {} workflow_dispatch: inputs: sdk_branch: @@ -39,18 +41,13 @@ defaults: working-directory: apps/cli jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -71,6 +68,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - 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 != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + cli: name: CLI ${{ matrix.os.base }} - ${{ matrix.license_type.readable }} strategy: @@ -117,7 +122,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -130,7 +135,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -272,7 +277,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -285,7 +290,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml new file mode 100644 index 00000000000..47f85d69163 --- /dev/null +++ b/.github/workflows/build-desktop-target.yml @@ -0,0 +1,38 @@ +name: Build Desktop on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/desktop/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + - '.github/workflows/build-desktop.yml' + workflow_dispatch: + inputs: + sdk_branch: + description: "Custom SDK branch" + required: false + type: string + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build Desktop on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-desktop.yml + secrets: inherit + diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 5e6ff63d684..81af513e55a 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,7 +1,7 @@ name: Build Desktop on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -25,6 +25,8 @@ on: - '!*.md' - '!*.txt' - '.github/workflows/build-desktop.yml' + workflow_call: + inputs: {} workflow_dispatch: inputs: sdk_branch: @@ -37,15 +39,9 @@ defaults: shell: bash jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 - needs: - - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -67,8 +63,6 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: package_version: ${{ steps.retrieve-version.outputs.package_version }} release_channel: ${{ steps.release-channel.outputs.channel }} @@ -76,6 +70,7 @@ jobs: rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }} hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} defaults: run: working-directory: apps/desktop @@ -138,6 +133,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - 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 != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + linux: name: Linux Build # Note, before updating the ubuntu version of the workflow, ensure the snap base image @@ -333,12 +336,14 @@ jobs: rustup show - name: Login to Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" @@ -353,7 +358,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -366,7 +371,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -386,7 +391,17 @@ jobs: working-directory: apps/desktop/desktop_native run: node build.js cross-platform - - name: Build & Sign (dev) + - name: Build + run: | + npm run build + + - name: Pack + if: ${{ needs.setup.outputs.has_secrets == 'false' }} + run: | + npm run pack:win + + - name: Pack & Sign (dev) + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: ELECTRON_BUILDER_SIGN: 1 SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }} @@ -395,10 +410,10 @@ jobs: SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }} SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }} run: | - npm run build npm run pack:win - name: Rename appx files for store + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx" ` -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx" @@ -408,6 +423,7 @@ jobs: -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx" - name: Package for Chocolatey + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item -Path ./stores/chocolatey -Destination ./dist/chocolatey -Recurse Copy-Item -Path ./dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe ` @@ -419,6 +435,7 @@ jobs: choco pack ./dist/chocolatey/bitwarden.nuspec --version "$env:_PACKAGE_VERSION" --out ./dist/chocolatey - name: Fix NSIS artifact names for auto-updater + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z ` -NewName bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -435,6 +452,7 @@ jobs: if-no-files-found: error - name: Upload installer exe artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe @@ -442,6 +460,7 @@ jobs: if-no-files-found: error - name: Upload appx ia32 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx @@ -449,6 +468,7 @@ jobs: if-no-files-found: error - name: Upload store appx ia32 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx @@ -456,6 +476,7 @@ jobs: if-no-files-found: error - name: Upload NSIS ia32 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -463,6 +484,7 @@ jobs: if-no-files-found: error - name: Upload appx x64 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx @@ -470,6 +492,7 @@ jobs: if-no-files-found: error - name: Upload store appx x64 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx @@ -477,6 +500,7 @@ jobs: if-no-files-found: error - name: Upload NSIS x64 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z @@ -484,6 +508,7 @@ jobs: if-no-files-found: error - name: Upload appx ARM64 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx @@ -491,6 +516,7 @@ jobs: if-no-files-found: error - name: Upload store appx ARM64 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx @@ -498,6 +524,7 @@ jobs: if-no-files-found: error - name: Upload NSIS ARM64 artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z @@ -505,6 +532,7 @@ jobs: if-no-files-found: error - name: Upload nupkg artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg @@ -512,6 +540,7 @@ jobs: if-no-files-found: error - name: Upload auto-update artifact + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: ${{ needs.setup.outputs.release_channel }}.yml @@ -574,11 +603,13 @@ jobs: key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - name: Login to Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Download Provisioning Profiles secrets + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles @@ -591,6 +622,7 @@ jobs: --output none - name: Get certificates + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | mkdir -p $HOME/certificates @@ -613,6 +645,7 @@ jobs: jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 - name: Set up keychain + if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | @@ -642,6 +675,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - name: Set up provisioning profiles + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile @@ -661,7 +695,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -674,7 +708,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -701,6 +735,7 @@ jobs: browser-build: name: Browser Build needs: setup + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: ./.github/workflows/build-browser.yml secrets: inherit @@ -708,6 +743,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets runs-on: macos-13 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -949,6 +985,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset runs-on: macos-13 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -1216,6 +1253,7 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset runs-on: macos-13 + if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml new file mode 100644 index 00000000000..a27af0b0870 --- /dev/null +++ b/.github/workflows/build-web-target.yml @@ -0,0 +1,41 @@ +name: Build Web on PR Target + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: + - 'l10n_master' + - 'cf-pages' + paths: + - 'apps/web/**' + - 'libs/**' + - '*' + - '!*.md' + - '!*.txt' + - '.github/workflows/build-web.yml' + workflow_dispatch: + inputs: + custom_tag_extension: + description: "Custom image tag extension" + required: false + sdk_branch: + description: "Custom SDK branch" + required: false + type: string + +defaults: + run: + shell: bash + +jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + + run-workflow: + name: Run Build Web on PR Target + needs: check-run + if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ./.github/workflows/build-web.yml + secrets: inherit + diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 6e5e11c3361..2360f876826 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,7 +1,7 @@ name: Build Web on: - pull_request_target: + pull_request: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -27,6 +27,8 @@ on: - '.github/workflows/build-web.yml' release: types: [published] + workflow_call: + inputs: {} workflow_dispatch: inputs: custom_tag_extension: @@ -41,18 +43,13 @@ env: _AZ_REGISTRY: bitwardenprod.azurecr.io jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - setup: name: Setup runs-on: ubuntu-22.04 - needs: - - check-run outputs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} + has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -70,6 +67,14 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + - 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 != '' }} + echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 @@ -128,7 +133,7 @@ jobs: run: npm ci - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -141,7 +146,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} working-directory: ./ run: | ls -l ../ @@ -210,19 +215,23 @@ jobs: ########## ACRs ########## - name: Login to Prod Azure + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - name: Log into Prod container registry + if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: az acr login -n bitwardenprod - name: Login to Azure - CI Subscription + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve github PAT secrets + if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: retrieve-secret-pat uses: bitwarden/gh-actions/get-keyvault-secrets@main with: @@ -270,6 +279,7 @@ jobs: run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: apps/web diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9dc72c7fdda..1b738bd7bcf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,12 +1,20 @@ name: Lint on: - push: + pull_request: + types: [opened, synchronize] branches-ignore: - 'l10n_master' - 'cf-pages' paths-ignore: - '.github/workflows/**' + push: + branches: + - 'main' + - 'rc' + - 'hotfix-rc-*' + paths-ignore: + - '.github/workflows/**' workflow_dispatch: inputs: {} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index b5270c0ff87..8fedb88b5dc 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -20,7 +20,7 @@ "**/node_modules/@bitwarden/desktop-napi/index.js", "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], - "electronVersion": "32.1.1", + "electronVersion": "33.2.1", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -241,7 +241,17 @@ "autoStart": true, "base": "core22", "confinement": "strict", - "plugs": ["default", "network-bind", "password-manager-service", "u2f-devices"], + "plugs": [ + "default", + "network-bind", + "u2f-devices", + "password-manager-service", + { + "polkit": { + "action-prefix": "com.bitwarden.Bitwarden" + } + } + ], "stagePackages": ["default"] }, "protocols": [ diff --git a/apps/desktop/package.json b/apps/desktop/package.json index eab9a7d7119..f546563ed18 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -35,7 +35,7 @@ "clean:dist": "rimraf ./dist", "pack:dir": "npm run clean:dist && electron-builder --dir -p never", "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", - "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never", + "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && mksquashfs ./dist/tmp-snap/ $SNAP_FILE -noappend -comp lzo -no-fragments && rm -rf ./dist/tmp-snap/", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", diff --git a/apps/desktop/resources/com.bitwarden.desktop.policy b/apps/desktop/resources/com.bitwarden.desktop.policy new file mode 100644 index 00000000000..e48bc6b8fbb --- /dev/null +++ b/apps/desktop/resources/com.bitwarden.desktop.policy @@ -0,0 +1,16 @@ + + + + + + Unlock Bitwarden + Authenticate to unlock Bitwarden + + no + no + auth_self + + + diff --git a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts index 771f1ea3a1c..f2bcf62e03e 100644 --- a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts +++ b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts @@ -87,8 +87,8 @@ export default class BiometricUnixMain implements OsBiometricService { } async authenticateBiometric(): Promise { - const hwnd = this.windowMain.win.getNativeWindowHandle(); - return await biometrics.prompt(hwnd, this.i18nservice.t("polkitConsentMessage")); + const hwnd = Buffer.from(""); + return await biometrics.prompt(hwnd, ""); } async osSupportsBiometric(): Promise { @@ -98,10 +98,14 @@ export default class BiometricUnixMain implements OsBiometricService { // This could be dynamically detected on dbus in the future. // We should check if a libsecret implementation is available on the system // because otherwise we cannot offlod the protected userkey to secure storage. - return (await passwords.isAvailable()) && !isSnapStore(); + return await passwords.isAvailable(); } async osBiometricsNeedsSetup(): Promise { + if (isSnapStore()) { + return false; + } + // check whether the polkit policy is loaded via dbus call to polkit return !(await biometrics.available()); } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index e4c235dada9..f8f81a5ac2c 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1734,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/main/tray.main.ts b/apps/desktop/src/main/tray.main.ts index 641af8db0ad..52a8615a1da 100644 --- a/apps/desktop/src/main/tray.main.ts +++ b/apps/desktop/src/main/tray.main.ts @@ -64,9 +64,8 @@ export class TrayMain { } setupWindowListeners(win: BrowserWindow) { - win.on("minimize", async (e: Event) => { + win.on("minimize", async () => { if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) { - e.preventDefault(); // 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.hideToTray(); diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index fa4d027d0f6..8387c53e5e3 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -98,7 +98,7 @@ *ngIf="canAccessExport$ | async" > diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 91c965658a3..6ead83b01d8 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -23,6 +23,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { ProductTierType } from "@bitwarden/common/billing/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 { getById } from "@bitwarden/common/platform/misc"; import { BannerModule, IconModule } from "@bitwarden/components"; @@ -49,6 +50,7 @@ export class OrganizationLayoutComponent implements OnInit { protected readonly logo = AdminConsoleLogo; protected orgFilter = (org: Organization) => canAccessOrgAdmin(org); + protected domainVerificationNavigationTextKey: string; protected integrationPageEnabled$: Observable; @@ -67,6 +69,7 @@ export class OrganizationLayoutComponent implements OnInit { private configService: ConfigService, private policyService: PolicyService, private providerService: ProviderService, + private i18nService: I18nService, ) {} async ngOnInit() { @@ -116,6 +119,12 @@ export class OrganizationLayoutComponent implements OnInit { org.productTierType === ProductTierType.Enterprise && featureFlagEnabled, ), ); + + this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag( + FeatureFlag.AccountDeprovisioning, + )) + ? "claimedDomains" + : "domainVerification"; } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 412423a3a24..aedad9b26ea 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -6,7 +6,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DeviceType, EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @Injectable() @@ -16,6 +18,7 @@ export class EventService { constructor( private i18nService: I18nService, policyService: PolicyService, + private configService: ConfigService, ) { policyService.policies$.subscribe((policies) => { this.policies = policies; @@ -451,10 +454,20 @@ export class EventService { msg = humanReadableMsg = this.i18nService.t("removedDomain", ev.domainName); break; case EventType.OrganizationDomain_Verified: - msg = humanReadableMsg = this.i18nService.t("domainVerifiedEvent", ev.domainName); + msg = humanReadableMsg = this.i18nService.t( + (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "domainClaimedEvent" + : "domainVerifiedEvent", + ev.domainName, + ); break; case EventType.OrganizationDomain_NotVerified: - msg = humanReadableMsg = this.i18nService.t("domainNotVerifiedEvent", ev.domainName); + msg = humanReadableMsg = this.i18nService.t( + (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "domainNotClaimedEvent" + : "domainNotVerifiedEvent", + ev.domainName, + ); break; // Secrets Manager case EventType.Secret_Retrieved: diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 06728929912..b1203230688 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9801,8 +9801,8 @@ "selfHostingTitleProper": { "message": "Self-Hosting" }, - "verified-domain-single-org-warning" : { - "message": "Verifying a domain will turn on the single organization policy." + "claim-domain-single-org-warning" : { + "message": "Claiming a domain will turn on the single organization policy." }, "single-org-revoked-user-warning": { "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." @@ -9902,5 +9902,62 @@ }, "removeMembers": { "message": "Remove members" + }, + "claimedDomains": { + "message": "Claimed domains" + }, + "claimDomain": { + "message": "Claim domain" + }, + "reclaimDomain": { + "message": "Reclaim domain" + }, + "claimDomainNameInputHint": { + "message": "Example: mydomain.com. Subdomains require separate entries to be claimed." + }, + "automaticClaimedDomains": { + "message": "Automatic Claimed Domains" + }, + "automaticDomainClaimProcess": { + "message": "Bitwarden will attempt to claim the domain 3 times during the first 72 hours. If the domain can’t be claimed, check the DNS record in your host and manually claim. The domain will be removed from your organization in 7 days if it is not claimed." + }, + "domainNotClaimed": { + "message": "$DOMAIN$ not claimed. Check your DNS records.", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } + }, + "domainStatusClaimed": { + "message": "Claimed" + }, + "domainStatusUnderVerification": { + "message": "Under verification" + }, + "claimedDomainsDesc": { + "message": "Claim a domain to own all member accounts whose email address matches the domain. Members will be able to skip the SSO identifier when logging in. Administrators will also be able to delete member accounts." + }, + "invalidDomainNameClaimMessage": { + "message": "Input is not a valid format. Format: mydomain.com. Subdomains require separate entries to be claimed." + }, + "domainClaimedEvent": { + "message": "$DOMAIN$ claimed", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } + }, + "domainNotClaimedEvent": { + "message": "$DOMAIN$ not claimed", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html index 15120eed92a..7226c957598 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html @@ -6,24 +6,37 @@ {{ "newDomain" | i18n }} - {{ "verifyDomain" | i18n }} + + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n + }} {{ data.orgDomain.domainName }} {{ - "domainStatusUnverified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n }} {{ - "domainStatusVerified" | i18n + ((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified") + | i18n }}
{{ "domainName" | i18n }} - {{ "domainNameInputHint" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) + ? "claimDomainNameInputHint" + : "domainNameInputHint" + ) | i18n + }} @@ -42,18 +55,29 @@ - {{ "automaticDomainVerificationProcess" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) + ? "automaticDomainClaimProcess" + : "automaticDomainVerificationProcess" + ) | i18n + }}
+

+ {{ "claimedDomainsDesc" | i18n }} + + + +

+ {{ - "domainStatusUnverified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n }} {{ - "domainStatusVerified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusClaimed" + : "domainStatusVerified" + ) | i18n }} @@ -70,7 +90,10 @@ type="button" > - {{ "verifyDomain" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") + | i18n + }} + + + + + {{ "loading" | i18n }} + + + +

{{ "noClientsInList" | i18n }}

+ + + + {{ "name" | i18n }} + {{ "numberOfUsers" | i18n }} + {{ "billingPlan" | i18n }} + + + + + + + + {{ row.organizationName }} + + + {{ row.userCount }} + / {{ row.seats }} + + + {{ row.plan }} + + + + + + + +
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts new file mode 100644 index 00000000000..2be38477d4c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts @@ -0,0 +1,167 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; +import { firstValueFrom, from, map } from "rxjs"; +import { debounceTime, first, switchMap } from "rxjs/operators"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { PlanType } from "@bitwarden/common/billing/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; + +import { WebProviderService } from "../services/web-provider.service"; + +import { AddOrganizationComponent } from "./add-organization.component"; + +const DisallowedPlanTypes = [ + PlanType.Free, + PlanType.FamiliesAnnually2019, + PlanType.FamiliesAnnually, + PlanType.TeamsStarter2023, + PlanType.TeamsStarter, +]; + +@Component({ + templateUrl: "vnext-clients.component.html", + standalone: true, + imports: [ + SharedOrganizationModule, + HeaderModule, + CommonModule, + JslibModule, + AvatarModule, + RouterModule, + TableModule, + ], +}) +export class vNextClientsComponent { + providerId: string = ""; + addableOrganizations: Organization[] = []; + loading = true; + manageOrganizations = false; + showAddExisting = false; + dataSource: TableDataSource = + new TableDataSource(); + protected searchControl = new FormControl("", { nonNullable: true }); + + constructor( + private router: Router, + private providerService: ProviderService, + private apiService: ApiService, + private organizationService: OrganizationService, + private organizationApiService: OrganizationApiServiceAbstraction, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) { + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + this.activatedRoute.parent?.params + ?.pipe( + switchMap((params) => { + this.providerId = params.providerId; + return this.providerService.get$(this.providerId).pipe( + map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((isBillable) => { + if (isBillable) { + return from( + this.router.navigate(["../manage-client-organizations"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } + + async load() { + const response = await this.apiService.getProviderClients(this.providerId); + const clients = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = clients; + this.manageOrganizations = + (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + const candidateOrgs = (await this.organizationService.getAll()).filter( + (o) => o.isOwner && o.providerId == null, + ); + const allowedOrgsIds = await Promise.all( + candidateOrgs.map((o) => this.organizationApiService.get(o.id)), + ).then((orgs) => + orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id), + ); + this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id)); + + this.showAddExisting = this.addableOrganizations.length !== 0; + this.loading = false; + } + + async addExistingOrganization() { + const dialogRef = AddOrganizationComponent.open(this.dialogService, { + providerId: this.providerId, + organizations: this.addableOrganizations, + }); + + if (await firstValueFrom(dialogRef.closed)) { + await this.load(); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 00c944e69bb..09276263332 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,8 +2,10 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -12,10 +14,12 @@ import { ProviderSubscriptionComponent, hasConsolidatedBilling, ProviderBillingHistoryComponent, + vNextManageClientsComponent, } from "../../billing/providers"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; +import { vNextClientsComponent } from "./clients/vnext-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; @@ -82,13 +86,25 @@ const routes: Routes = [ children: [ { path: "", pathMatch: "full", redirectTo: "clients" }, { path: "clients/create", component: CreateOrganizationComponent }, - { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, - { - path: "manage-client-organizations", - canActivate: [hasConsolidatedBilling], - component: ManageClientsComponent, - data: { titleId: "clients" }, - }, + ...featureFlaggedRoute({ + defaultComponent: ClientsComponent, + flaggedComponent: vNextClientsComponent, + featureFlag: FeatureFlag.PM12443RemovePagingLogic, + routeOptions: { + path: "clients", + data: { titleId: "clients" }, + }, + }), + ...featureFlaggedRoute({ + defaultComponent: ManageClientsComponent, + flaggedComponent: vNextManageClientsComponent, + featureFlag: FeatureFlag.PM12443RemovePagingLogic, + routeOptions: { + path: "manage-client-organizations", + data: { titleId: "clients" }, + canActivate: [hasConsolidatedBilling], + }, + }), { path: "manage", children: [ diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index fa9665ff457..0731820e413 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -31,7 +31,10 @@ {{ "ssoIdentifierHintPartOne" | i18n }} - {{ "domainVerification" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimedDomains" : "domainVerification") + | i18n + }} diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 87f2a3dd9d9..6449ef7a701 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,7 +9,7 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Subject, takeUntil } from "rxjs"; +import { concatMap, Observable, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -28,6 +28,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +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"; @@ -185,6 +186,8 @@ export class SsoComponent implements OnInit, OnDestroy { return this.ssoConfigForm?.controls?.configType as FormControl; } + accountDeprovisioningEnabled$: Observable; + constructor( private formBuilder: FormBuilder, private route: ActivatedRoute, @@ -195,7 +198,11 @@ export class SsoComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private toastService: ToastService, - ) {} + ) { + this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + } async ngOnInit() { this.enabledCtrl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled) => { diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts index ae7bf487f99..f8b344372ef 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -3,3 +3,4 @@ export * from "./manage-clients.component"; export * from "./manage-client-name-dialog.component"; export * from "./manage-client-subscription-dialog.component"; export * from "./no-clients.component"; +export * from "./vnext-manage-clients.component"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html new file mode 100644 index 00000000000..c54965bbdb6 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html @@ -0,0 +1,83 @@ + + + + + {{ "addNewOrganization" | i18n }} + + + + + + {{ "loading" | i18n }} + + + + + + {{ "client" | i18n }} + {{ "assigned" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} + {{ "billingPlan" | i18n }} + + + + + + + + + + + {{ row.seats }} + + + {{ row.occupiedSeats }} + + + {{ row.remainingSeats }} + + + {{ row.plan }} + + + + + + + + + + + +
+ +
+
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts new file mode 100644 index 00000000000..4c0837d6da2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts @@ -0,0 +1,201 @@ +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; +import { debounceTime, first, switchMap } from "rxjs/operators"; + +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +import { + CreateClientDialogResultType, + openCreateClientDialog, +} from "./create-client-dialog.component"; +import { + ManageClientNameDialogResultType, + openManageClientNameDialog, +} from "./manage-client-name-dialog.component"; +import { + ManageClientSubscriptionDialogResultType, + openManageClientSubscriptionDialog, +} from "./manage-client-subscription-dialog.component"; +import { vNextNoClientsComponent } from "./vnext-no-clients.component"; + +@Component({ + templateUrl: "vnext-manage-clients.component.html", + standalone: true, + imports: [ + AvatarModule, + TableModule, + HeaderModule, + SharedOrganizationModule, + vNextNoClientsComponent, + ], +}) +export class vNextManageClientsComponent { + providerId: string = ""; + provider: Provider | undefined; + loading = true; + isProviderAdmin = false; + dataSource: TableDataSource = + new TableDataSource(); + + protected searchControl = new FormControl("", { nonNullable: true }); + protected plans: PlanResponse[] = []; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + private providerService: ProviderService, + private router: Router, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) { + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + this.activatedRoute.parent?.params + ?.pipe( + switchMap((params) => { + this.providerId = params.providerId; + return this.providerService.get$(this.providerId).pipe( + map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((isBillable) => { + if (!isBillable) { + return from( + this.router.navigate(["../clients"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } + + async load() { + this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); + + this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; + + const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) + .data; + + clients.forEach((client) => (client.plan = client.plan?.replace(" (Monthly)", ""))); + + this.dataSource.data = clients; + + this.plans = (await this.billingApiService.getPlans()).data; + + this.loading = false; + } + + createClient = async () => { + const reference = openCreateClientDialog(this.dialogService, { + data: { + providerId: this.providerId, + plans: this.plans, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === CreateClientDialogResultType.Submitted) { + await this.load(); + } + }; + + manageClientName = async (organization: ProviderOrganizationOrganizationDetailsResponse) => { + const dialogRef = openManageClientNameDialog(this.dialogService, { + data: { + providerId: this.providerId, + organization: { + id: organization.id, + name: organization.organizationName, + seats: organization.seats ? organization.seats : 0, + }, + }, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result === ManageClientNameDialogResultType.Submitted) { + await this.load(); + } + }; + + manageClientSubscription = async ( + organization: ProviderOrganizationOrganizationDetailsResponse, + ) => { + const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { + data: { + organization, + provider: this.provider!, + }, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result === ManageClientSubscriptionDialogResultType.Submitted) { + await this.load(); + } + }; + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts new file mode 100644 index 00000000000..5ad19945c51 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts @@ -0,0 +1,50 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { svgIcon } from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; + +const gearIcon = svgIcon` + + + + + + + + + + + + + + + + +`; + +@Component({ + selector: "app-no-clients", + standalone: true, + imports: [SharedOrganizationModule], + template: `
+ +

{{ "noClients" | i18n }}

+ + + {{ "addNewOrganization" | i18n }} + +
`, +}) +export class vNextNoClientsComponent { + icon = gearIcon; + @Input() showAddOrganizationButton = true; + @Output() addNewOrganizationClicked = new EventEmitter(); + + addNewOrganization = () => this.addNewOrganizationClicked.emit(); +} diff --git a/libs/angular/src/directives/text-drag.directive.ts b/libs/angular/src/directives/text-drag.directive.ts index da3e70d1de2..443fbdac157 100644 --- a/libs/angular/src/directives/text-drag.directive.ts +++ b/libs/angular/src/directives/text-drag.directive.ts @@ -17,6 +17,6 @@ export class TextDragDirective { @HostListener("dragstart", ["$event"]) onDragStart(event: DragEvent) { - event.dataTransfer.setData("text", this.data); + event.dataTransfer?.setData("text", this.data); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f79ebf8aa55..6597c97b641 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -40,6 +40,7 @@ export enum FeatureFlag { DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", MacOsNativeCredentialSync = "macos-native-credential-sync", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", + PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -90,6 +91,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, + [FeatureFlag.PM12443RemovePagingLogic]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/package-lock.json b/package-lock.json index 64a7c926ca2..2da7d9e6255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,7 +132,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "32.1.1", + "electron": "33.2.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", @@ -15745,9 +15745,9 @@ } }, "node_modules/electron": { - "version": "32.1.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-32.1.1.tgz", - "integrity": "sha512-NlWvG6kXOJbZbELmzP3oV7u50I3NHYbCeh+AkUQ9vGyP7b74cFMx9HdTzejODeztW1jhr3SjIBbUZzZ45zflfQ==", + "version": "33.2.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", + "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15986,9 +15986,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", - "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "version": "20.17.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", + "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5573332db1a..aa567f18df6 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "32.1.1", + "electron": "33.2.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1",