From 3c3a0e65d86f6ac41e572b22c31656f70c22a379 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 16 Apr 2025 15:06:36 -0400 Subject: [PATCH 01/30] [PM-20310] Icon updates followup (#14312) * remove brand icon story from lit storybook * replace users icon with family icon * update collection icon shape and name --- .../cipher/cipher-indicator-icons.ts | 4 ++-- .../{collection.ts => collection-shared.ts} | 4 ++-- .../content/components/icons/family.ts | 19 +++++++++++++++++++ .../content/components/icons/index.ts | 4 ++-- .../content/components/icons/users.ts | 18 ------------------ .../lit-stories/icons/icons.lit-stories.ts | 13 +++++-------- .../components/notification/button-row.ts | 4 ++-- 7 files changed, 32 insertions(+), 34 deletions(-) rename apps/browser/src/autofill/content/components/icons/{collection.ts => collection-shared.ts} (65%) create mode 100644 apps/browser/src/autofill/content/components/icons/family.ts delete mode 100644 apps/browser/src/autofill/content/components/icons/users.ts diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index e4fe012a678..5e78fd5658a 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -4,7 +4,7 @@ import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; -import { Business, Users } from "../../../content/components/icons"; +import { Business, Family } from "../../../content/components/icons"; import { OrganizationCategories, OrganizationCategory } from "./types"; @@ -13,7 +13,7 @@ const cipherIndicatorIconsMap: Record< (args: { color: string; theme: Theme }) => TemplateResult > = { [OrganizationCategories.business]: Business, - [OrganizationCategories.family]: Users, + [OrganizationCategories.family]: Family, }; export function CipherInfoIndicatorIcons({ diff --git a/apps/browser/src/autofill/content/components/icons/collection.ts b/apps/browser/src/autofill/content/components/icons/collection-shared.ts similarity index 65% rename from apps/browser/src/autofill/content/components/icons/collection.ts rename to apps/browser/src/autofill/content/components/icons/collection-shared.ts index fb2c58647c5..de366b88e92 100644 --- a/apps/browser/src/autofill/content/components/icons/collection.ts +++ b/apps/browser/src/autofill/content/components/icons/collection-shared.ts @@ -4,14 +4,14 @@ import { html } from "lit"; import { IconProps } from "../common-types"; import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; -export function Collection({ color, disabled, theme }: IconProps) { +export function CollectionShared({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index de39b70ab24..65ec6301ac4 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -3,12 +3,12 @@ export { AngleUp } from "./angle-up"; export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; -export { Collection } from "./collection"; +export { CollectionShared } from "./collection-shared"; export { ExclamationTriangle } from "./exclamation-triangle"; export { ExternalLink } from "./external-link"; +export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; export { User } from "./user"; -export { Users } from "./users"; diff --git a/apps/browser/src/autofill/content/components/icons/users.ts b/apps/browser/src/autofill/content/components/icons/users.ts deleted file mode 100644 index eb7840104f0..00000000000 --- a/apps/browser/src/autofill/content/components/icons/users.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { IconProps } from "../common-types"; -import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; - -export function Users({ color, disabled, theme }: IconProps) { - const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; - - return html` - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index fc5db1c7c2c..c74895e1dea 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -43,26 +43,23 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj => { render: (args) => Template(args, Icons[iconName]), } as StoryObj; - if (iconName !== "BrandIconContainer") { - story.argTypes = { - iconLink: { table: { disable: true } }, - }; - } + story.argTypes = { + iconLink: { table: { disable: true } }, + }; return story; }; export const AngleDownIcon = createIconStory("AngleDown"); export const AngleUpIcon = createIconStory("AngleUp"); -export const BrandIcon = createIconStory("BrandIconContainer"); export const BusinessIcon = createIconStory("Business"); export const CloseIcon = createIconStory("Close"); -export const CollectionIcon = createIconStory("Collection"); +export const CollectionSharedIcon = createIconStory("CollectionShared"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); export const ExternalLinkIcon = createIconStory("ExternalLink"); +export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); export const UserIcon = createIconStory("User"); -export const UsersIcon = createIconStory("Users"); diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 6fa32f11aa2..8661f5957e1 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -4,14 +4,14 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { Theme } from "@bitwarden/common/platform/enums"; import { Option, OrgView, FolderView } from "../common-types"; -import { Business, Users, Folder, User } from "../icons"; +import { Business, Family, Folder, User } from "../icons"; import { ButtonRow } from "../rows/button-row"; function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] { switch (productTierType) { case ProductTierType.Free: case ProductTierType.Families: - return Users; + return Family; case ProductTierType.Teams: case ProductTierType.Enterprise: case ProductTierType.TeamsStarter: From 4b45bfaeeb4be5cfacb3f8c2d3febaf99dda2ee0 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 16 Apr 2025 17:03:51 -0400 Subject: [PATCH 02/30] [PM-20236] update routing for intro carousel so path is not saved during popout (#14300) --- apps/browser/src/popup/app-routing.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8ebf6eb6110..21ac4c19700 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -593,6 +593,7 @@ const routes: Routes = [ path: "intro-carousel", component: ExtensionAnonLayoutWrapperComponent, canActivate: [], + data: { elevation: 0, doNotSaveUrl: true } satisfies RouteDataProperties, children: [ { path: "", From 9f61b6aaa7bbadadca59419741b30e59953823ba Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 16 Apr 2025 17:14:27 -0400 Subject: [PATCH 03/30] [PM-20182] update colors for intro carousel svg for dark theme (#14280) --- .../intro-carousel/intro-carousel.component.html | 2 +- libs/vault/src/icons/login-cards.ts | 8 ++++---- libs/vault/src/icons/secure-devices.ts | 10 +++++----- libs/vault/src/icons/secure-user.ts | 4 ++-- libs/vault/src/icons/security-handshake.ts | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html index 3c061109945..ff7bf25b86b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html @@ -43,7 +43,7 @@ bitButton buttonType="secondary" (click)="navigateToLogin()" - class="tw-w-full tw-mt-4" + class="tw-w-full tw-mt-2" > {{ "logIn" | i18n }} diff --git a/libs/vault/src/icons/login-cards.ts b/libs/vault/src/icons/login-cards.ts index 01baf308412..6e066a0d924 100644 --- a/libs/vault/src/icons/login-cards.ts +++ b/libs/vault/src/icons/login-cards.ts @@ -2,10 +2,10 @@ import { svgIcon } from "@bitwarden/components"; export const LoginCards = svgIcon` - - - + + + - + `; diff --git a/libs/vault/src/icons/secure-devices.ts b/libs/vault/src/icons/secure-devices.ts index ee3a6ea6b90..4e123afad40 100644 --- a/libs/vault/src/icons/secure-devices.ts +++ b/libs/vault/src/icons/secure-devices.ts @@ -3,14 +3,14 @@ import { svgIcon } from "@bitwarden/components"; export const SecureDevices = svgIcon` - - - + + + - + - + `; diff --git a/libs/vault/src/icons/secure-user.ts b/libs/vault/src/icons/secure-user.ts index f8f126adbac..39d9957030c 100644 --- a/libs/vault/src/icons/secure-user.ts +++ b/libs/vault/src/icons/secure-user.ts @@ -2,8 +2,8 @@ import { svgIcon } from "@bitwarden/components"; export const SecureUser = svgIcon` - - + + diff --git a/libs/vault/src/icons/security-handshake.ts b/libs/vault/src/icons/security-handshake.ts index 5a598fd180d..d68f8a948d3 100644 --- a/libs/vault/src/icons/security-handshake.ts +++ b/libs/vault/src/icons/security-handshake.ts @@ -2,11 +2,11 @@ import { svgIcon } from "@bitwarden/components"; export const SecurityHandshake = svgIcon` - - - + + + - + `; From 88638c09b39ce499f4ce60e290b05ab4b29c7690 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 16 Apr 2025 19:58:31 -0400 Subject: [PATCH 04/30] Fix(login): [PM-20287] Initialize login email state when email is remembered --- libs/auth/src/angular/login/login.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 8a198663e06..4ca18b4985e 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -536,6 +536,10 @@ export class LoginComponent implements OnInit, OnDestroy { if (storedEmail) { this.formGroup.controls.email.setValue(storedEmail); this.formGroup.controls.rememberEmail.setValue(true); + // If we load an email into the form, we need to initialize it for the login process as well + // so that other login components can use it. + // We do this here as it's possible that a user doesn't edit the email field before submitting. + this.loginEmailService.setLoginEmail(storedEmail); } else { this.formGroup.controls.rememberEmail.setValue(false); } From c7259b4cb12dcc3a99a2700ce35785a4e17eaaa9 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:21:24 -0400 Subject: [PATCH 05/30] BRE-536/Add-ARM-targz-builds-for-desktop-and-cli (#14270) * Test ARM64 build * Remove sudo * Change to public preview runner * Change cache key for architectures * Test * Test * Test * remove x86 musl target - troubleshooting build error * native module troubleshooting * remove cross-platform for testing * attempt to resolve cross-platform issue * support more arm64 build types * fix missed amd to arm update * missing dependency during env setup * lxd troubleshooting * install lxd with snap instead * electron-builder debug * simplified script for testing * testing * 22.04 to 20.04 * try ubuntu 24.04 runner * add dist script * update build command * troubleshoot 24.04 compatibility * remove lxd before merging main * add comment, bump arm runner down to 22.04 * revert to tar.gz support only for this PR * testing cli arm builds * fix build target designation * adjust runner designation * runner name typo * not needed currently * adjust build.js logic and call in workflow * address styling feedback and unnecessary rust toolchain call * simplify build cli os matrix * revert x86 linux builds to cross-platform command for build.js --------- Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --- .github/workflows/build-cli.yml | 5 +- .github/workflows/build-desktop.yml | 109 +++++++++++++++++++++++++-- apps/cli/package.json | 4 + apps/desktop/desktop_native/build.js | 22 ++++-- apps/desktop/package.json | 2 + 5 files changed, 129 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index a78d3bda5ad..e113d5c253b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -87,6 +87,7 @@ jobs: os: [ { base: "linux", distro: "ubuntu-22.04", target_suffix: "" }, + { base: "linux", distro: "ubuntu-22.04-arm", target_suffix: "-arm64" }, { base: "mac", distro: "macos-13", target_suffix: "" }, { base: "mac", distro: "macos-14", target_suffix: "-arm64" } ] @@ -130,7 +131,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -306,7 +307,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 4c35eb4c42f..99a7548aa14 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -201,7 +201,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -298,6 +298,103 @@ jobs: if-no-files-found: error + linux-arm64: + name: Linux ARM64 Build + # Note, before updating the ubuntu version of the workflow, ensure the snap base image + # is equal or greater than the new version. Otherwise there might be GLIBC version issues. + # The snap base for desktop is defined in `apps/desktop/electron-builder.json` + # We intentionally keep this runner on the oldest supported OS in GitHub Actions + # for maximum compatibility across GLIBC versions + runs-on: ubuntu-22.04-arm + needs: setup + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + _NODE_VERSION: ${{ needs.setup.outputs.node_version }} + NODE_OPTIONS: --max_old_space_size=4096 + defaults: + run: + working-directory: apps/desktop + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + node-version: ${{ env._NODE_VERSION }} + + - name: Set up environment + run: | + sudo apt-get update + sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder + + - name: Print environment + run: | + node --version + npm --version + snap --version + snapcraft --version || echo 'snapcraft unavailable' + + - name: Install Node dependencies + run: npm ci + working-directory: ./ + + - name: Download SDK Artifacts + if: ${{ inputs.sdk_branch != '' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: sdk-internal + repo: bitwarden/sdk-internal + path: ../sdk-internal + if_no_artifact_found: fail + + - name: Override SDK + if: ${{ inputs.sdk_branch != '' }} + working-directory: ./ + run: | + ls -l ../ + npm link ../sdk-internal + + - name: Cache Native Module + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: cache + with: + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* + ${{ env.RUNNER_TEMP }}/.cargo/registry + ${{ env.RUNNER_TEMP }}/.cargo/git + key: rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} + + - name: Build Native Module + if: steps.cache.outputs.cache-hit != 'true' + working-directory: apps/desktop/desktop_native + env: + PKG_CONFIG_ALLOW_CROSS: true + PKG_CONFIG_ALL_STATIC: true + TARGET: musl + run: | + rustup target add aarch64-unknown-linux-musl + node build.js --target=aarch64-unknown-linux-musl --release + + - name: Build application + run: npm run dist:lin:arm64 + + - name: Upload tar.gz artifact + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.tar.gz + path: apps/desktop/dist/bitwarden_desktop_arm64.tar.gz + if-no-files-found: error + windows: name: Windows Build runs-on: windows-2022 @@ -369,7 +466,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -705,7 +802,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -895,7 +992,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -1144,7 +1241,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -1425,7 +1522,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} diff --git a/apps/cli/package.json b/apps/cli/package.json index 7d9f4af0ffe..15dc470a95e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -34,18 +34,22 @@ "dist:oss:mac": "npm run build:oss:prod && npm run clean && npm run package:oss:mac", "dist:oss:mac-arm64": "npm run build:oss:prod && npm run clean && npm run package:oss:mac-arm64", "dist:oss:lin": "npm run build:oss:prod && npm run clean && npm run package:oss:lin", + "dist:oss:lin-arm64": "npm run build:oss:prod && npm run clean && npm run package:oss:lin-arm64", "dist:bit:win": "npm run build:bit:prod && npm run clean && npm run package:bit:win", "dist:bit:mac": "npm run build:bit:prod && npm run clean && npm run package:bit:mac", "dist:bit:mac-arm64": "npm run build:bit:prod && npm run clean && npm run package:bit:mac-arm64", "dist:bit:lin": "npm run build:bit:prod && npm run clean && npm run package:bit:lin", + "dist:bit:lin-arm64": "npm run build:bit:prod && npm run clean && npm run package:bit:lin-arm64", "package:oss:win": "pkg . --targets win-x64 --output ./dist/oss/windows/bw.exe", "package:oss:mac": "pkg . --targets macos-x64 --output ./dist/oss/macos/bw", "package:oss:mac-arm64": "pkg . --targets macos-arm64 --output ./dist/oss/macos-arm64/bw", "package:oss:lin": "pkg . --targets linux-x64 --output ./dist/oss/linux/bw", + "package:oss:lin-arm64": "pkg . --targets linux-arm64 --output ./dist/oss/linux-arm64/bw", "package:bit:win": "pkg . --targets win-x64 --output ./dist/bit/windows/bw.exe", "package:bit:mac": "pkg . --targets macos-x64 --output ./dist/bit/macos/bw", "package:bit:mac-arm64": "pkg . --targets macos-arm64 --output ./dist/bit/macos-arm64/bw", "package:bit:lin": "pkg . --targets linux-x64 --output ./dist/bit/linux/bw", + "package:bit:lin-arm64": "pkg . --targets linux-arm64 --output ./dist/bit/linux-arm64/bw", "test": "jest", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll" diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index f2f012bf088..ec20dce4116 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -3,6 +3,10 @@ const child_process = require("child_process"); const fs = require("fs"); const path = require("path"); const process = require("process"); +const args = process.argv.slice(2); // Get arguments passed to the script +const mode = args.includes("--release") ? "release" : "debug"; +const targetArg = args.find(arg => arg.startsWith("--target=")); +const target = targetArg ? targetArg.split("=")[1] : null; let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform"; @@ -18,10 +22,17 @@ function buildProxyBin(target, release = true) { return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); } -if (!crossPlatform) { - console.log("Building native modules in debug mode for the native architecture"); - buildNapiModule(false, false); - buildProxyBin(false, false); +if (!crossPlatform && !target) { + console.log(`Building native modules in ${mode} mode for the native architecture`); + buildNapiModule(false, mode === "release"); + buildProxyBin(false, mode === "release"); + return; +} + +if (target) { + console.log(`Building for target: ${target} in ${mode} mode`); + buildNapiModule(target, mode === "release"); + buildProxyBin(target, mode === "release"); return; } @@ -47,7 +58,8 @@ switch (process.platform) { default: targets = [ - ['x86_64-unknown-linux-musl', 'x64'] + ['x86_64-unknown-linux-musl', 'x64'], + ['aarch64-unknown-linux-musl', 'arm64'] ]; process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a4365b4c06a..ae34deee5b8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -36,6 +36,7 @@ "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 && 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 && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/", + "pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", "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", @@ -45,6 +46,7 @@ "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", "dist:lin": "npm run build && npm run pack:lin", + "dist:lin:arm64": "npm run build && npm run pack:lin:arm64", "dist:mac": "npm run build && npm run pack:mac", "dist:mac:mas": "npm run build && npm run pack:mac:mas", "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", From c4c9db51213ff0c8744341f926e40a0b7adefb5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:49:07 -0400 Subject: [PATCH 06/30] [deps] Vault: Update koa to v2.16.1 [SECURITY] (#14197) 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 15dc470a95e..04fe4290d31 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -76,7 +76,7 @@ "inquirer": "8.2.6", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", diff --git a/package-lock.json b/package-lock.json index cb9baf4fafe..c101444cd8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "jquery": "3.7.1", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lit": "3.2.1", @@ -210,7 +210,7 @@ "inquirer": "8.2.6", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -24789,9 +24789,9 @@ } }, "node_modules/koa": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.4.tgz", - "integrity": "sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", + "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", "license": "MIT", "dependencies": { "accepts": "^1.3.5", diff --git a/package.json b/package.json index 28d243e1c32..a39004dcc03 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "jquery": "3.7.1", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lit": "3.2.1", From 170f97da8e0f06ecbeb4c021d1e32ad8e4ae6dfe Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:58:16 +0200 Subject: [PATCH 07/30] [PM-20333] Remove "export-attachments" feature flag (#14310) * Remove usage of export-attachments feature flag * Remove export-attachments feature flag definition * Update export.command documentation --------- Co-authored-by: Daniel James Smith --- apps/cli/src/tools/export.command.ts | 10 ---------- apps/cli/src/vault.program.ts | 5 ++--- libs/common/src/enums/feature-flag.enum.ts | 2 -- .../src/components/export.component.ts | 16 +++------------- 4 files changed, 5 insertions(+), 28 deletions(-) diff --git a/apps/cli/src/tools/export.command.ts b/apps/cli/src/tools/export.command.ts index f5fea794eef..3fbc466efe0 100644 --- a/apps/cli/src/tools/export.command.ts +++ b/apps/cli/src/tools/export.command.ts @@ -10,8 +10,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; -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 { ExportFormat, @@ -30,7 +28,6 @@ export class ExportCommand { private policyService: PolicyService, private eventCollectionService: EventCollectionService, private accountService: AccountService, - private configService: ConfigService, ) {} async run(options: OptionValues): Promise { @@ -55,13 +52,6 @@ export class ExportCommand { const format = password && options.format == "json" ? "encrypted_json" : (options.format ?? "csv"); - if ( - format == "zip" && - !(await this.configService.getFeatureFlag(FeatureFlag.ExportAttachments)) - ) { - return Response.badRequest("Exporting attachments is not supported in this environment."); - } - if (!this.isSupportedExportFormat(format)) { return Response.badRequest( `'${format}' is not a supported export format. Supported formats: ${EXPORT_FORMATS.join( diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index c004d3597c1..ce6ac2af94e 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -464,7 +464,7 @@ export class VaultProgram extends BaseProgram { private exportCommand(): Command { return new Command("export") - .description("Export vault data to a CSV or JSON file.") + .description("Export vault data to a CSV, JSON or ZIP file.") .option("--output ", "Output directory or filename.") .option("--format ", "Export file format.") .option( @@ -476,7 +476,7 @@ export class VaultProgram extends BaseProgram { writeLn("\n Notes:"); writeLn(""); writeLn( - " Valid formats are `csv`, `json`, and `encrypted_json`. Default format is `csv`.", + " Valid formats are `csv`, `json`, `encrypted_json` and zip. Default format is `csv`.", ); writeLn(""); writeLn( @@ -504,7 +504,6 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.policyService, this.serviceContainer.eventCollectionService, this.serviceContainer.accountService, - this.serviceContainer.configService, ); const response = await command.run(options); this.processResponse(response); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f9e2d9757bc..1d0b1521db6 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -46,7 +46,6 @@ export enum FeatureFlag { CriticalApps = "pm-14466-risk-insights-critical-application", EnableRiskInsightsNotifications = "enable-risk-insights-notifications", DesktopSendUIRefresh = "desktop-send-ui-refresh", - ExportAttachments = "export-attachments", /* Vault */ PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", @@ -97,7 +96,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, - [FeatureFlag.ExportAttachments]: FALSE, /* Vault */ [FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE, 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 69f77c6ca32..4e9b4175838 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 @@ -39,8 +39,6 @@ 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 { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -184,10 +182,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { private onlyManagedCollections = true; private onGenerate$ = new Subject(); - private isExportAttachmentsEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.ExportAttachments, - ); - constructor( protected i18nService: I18nService, protected toastService: ToastService, @@ -202,7 +196,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { protected organizationService: OrganizationService, private accountService: AccountService, private collectionService: CollectionService, - private configService: ConfigService, ) {} async ngOnInit() { @@ -225,17 +218,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { ), ); - combineLatest([ - this.exportForm.controls.vaultSelector.valueChanges, - this.isExportAttachmentsEnabled$, - ]) + this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(([value, isExportAttachmentsEnabled]) => { + .subscribe(([value]) => { this.organizationId = value !== "myVault" ? value : undefined; this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip"); this.exportForm.get("format").setValue("json"); - if (value === "myVault" && isExportAttachmentsEnabled) { + if (value === "myVault") { this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" }); } }); From 08b966409f4dbaffd74313b85e9485501e6ae3c7 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:18:39 -0400 Subject: [PATCH 08/30] linux-x86-builds-fix (#14321) * readd rust toolchain commands * revert native module build call --- .github/workflows/build-desktop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 99a7548aa14..365d29f17f7 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -237,7 +237,7 @@ jobs: TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - node build.js cross-platform + node build.js --target=x86_64-unknown-linux-musl --release - name: Build application run: npm run dist:lin From fa268437efe5745af22e554cc325f090dc1dfc78 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:59:09 +0100 Subject: [PATCH 09/30] [PM-17774] Build page for admin sponsored families (#14243) * Added nav item for f4e in org admin console * shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table * Resolved issue with members nav item also being selected when f4e is selected * Separated out billing's logic from the org layout component * Removed unused observable * Moved logic to existing f4e policy service and added unit tests * Resolved script typescript error * Resolved goofy switchMap * Add changes for the issue orgs * Added changes for the dialog * Rename the files properly * Remove the commented code * Change the implement to align with design * Add todo comments * Remove the comment todo * Fix the uni test error * Resolve the unit test * Resolve the unit test issue * Resolve the pr comments on any and route * remove the any * remove the generic validator * Resolve the unit test * Resolve the wrong message * Resolve the duplicate route --------- Co-authored-by: Conner Turnbull Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../members/members-routing.module.ts | 7 +- .../can-access-sponsored-families.guard.ts | 26 ++++ .../add-sponsorship-dialog.component.html | 45 ++++++ .../add-sponsorship-dialog.component.ts | 135 ++++++++++++++++++ .../free-bitwarden-families.component.html | 23 +++ .../free-bitwarden-families.component.ts | 62 ++++++++ ...rganization-member-families.component.html | 47 ++++++ .../organization-member-families.component.ts | 34 +++++ ...nization-sponsored-families.component.html | 87 +++++++++++ ...ganization-sponsored-families.component.ts | 39 +++++ .../billing/members/types/sponsored-family.ts | 5 + .../src/app/shared/loose-components.module.ts | 9 ++ apps/web/src/images/search.svg | 12 ++ apps/web/src/locales/en/messages.json | 30 ++++ ...organization-sponsorship-create.request.ts | 1 + 15 files changed, 559 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts create mode 100644 apps/web/src/app/billing/members/add-sponsorship-dialog.component.html create mode 100644 apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts create mode 100644 apps/web/src/app/billing/members/free-bitwarden-families.component.html create mode 100644 apps/web/src/app/billing/members/free-bitwarden-families.component.ts create mode 100644 apps/web/src/app/billing/members/organization-member-families.component.html create mode 100644 apps/web/src/app/billing/members/organization-member-families.component.ts create mode 100644 apps/web/src/app/billing/members/organization-sponsored-families.component.html create mode 100644 apps/web/src/app/billing/members/organization-sponsored-families.component.ts create mode 100644 apps/web/src/app/billing/members/types/sponsored-family.ts create mode 100644 apps/web/src/images/search.svg diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index 9666630fc08..153a2f3a956 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -3,9 +3,10 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component"; +import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; +import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard"; import { MembersComponent } from "./members.component"; const routes: Routes = [ @@ -19,8 +20,8 @@ const routes: Routes = [ }, { path: "sponsored-families", - component: SponsoredFamiliesComponent, - canActivate: [organizationPermissionsGuard(canAccessMembersTab)], + component: FreeBitwardenFamiliesComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies], data: { titleId: "sponsoredFamilies", }, diff --git a/apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts b/apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts new file mode 100644 index 00000000000..9bc6778f0b0 --- /dev/null +++ b/apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts @@ -0,0 +1,26 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; +import { firstValueFrom, switchMap, filter } from "rxjs"; + +import { 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 { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { getById } from "@bitwarden/common/platform/misc"; + +import { FreeFamiliesPolicyService } from "../services/free-families-policy.service"; + +export const canAccessSponsoredFamilies: CanActivateFn = async (route: ActivatedRouteSnapshot) => { + const freeFamiliesPolicyService = inject(FreeFamiliesPolicyService); + const organizationService = inject(OrganizationService); + const accountService = inject(AccountService); + + const org = accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => organizationService.organizations$(userId)), + getById(route.params.organizationId), + filter((org): org is Organization => org !== undefined), + ); + + return await firstValueFrom(freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(org)); +}; diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html new file mode 100644 index 00000000000..2dbcc577e54 --- /dev/null +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html @@ -0,0 +1,45 @@ +
+ + {{ "addSponsorship" | i18n }} + +
+ +
+
+ + {{ "email" | i18n }}: + + +
+
+ + {{ "notes" | i18n }}: + + +
+
+ +
+ + + + + +
+ diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts new file mode 100644 index 00000000000..54d9ae90009 --- /dev/null +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts @@ -0,0 +1,135 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component } from "@angular/core"; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from "@angular/forms"; +import { firstValueFrom, map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ButtonModule, DialogModule, DialogService, FormFieldModule } from "@bitwarden/components"; + +interface RequestSponsorshipForm { + sponsorshipEmail: FormControl; + sponsorshipNote: FormControl; +} + +export interface AddSponsorshipDialogResult { + action: AddSponsorshipDialogAction; + value: Partial | null; +} + +interface AddSponsorshipFormValue { + sponsorshipEmail: string; + sponsorshipNote: string; + status: string; +} + +enum AddSponsorshipDialogAction { + Saved = "saved", + Canceled = "canceled", +} + +@Component({ + templateUrl: "add-sponsorship-dialog.component.html", + standalone: true, + imports: [ + JslibModule, + ButtonModule, + DialogModule, + FormsModule, + ReactiveFormsModule, + FormFieldModule, + ], +}) +export class AddSponsorshipDialogComponent { + sponsorshipForm: FormGroup; + loading = false; + + constructor( + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + private accountService: AccountService, + private i18nService: I18nService, + ) { + this.sponsorshipForm = this.formBuilder.group({ + sponsorshipEmail: new FormControl("", { + validators: [Validators.email, Validators.required], + asyncValidators: [this.validateNotCurrentUserEmail.bind(this)], + updateOn: "change", + }), + sponsorshipNote: new FormControl("", {}), + }); + } + + static open(dialogService: DialogService): DialogRef { + return dialogService.open(AddSponsorshipDialogComponent); + } + + protected async save() { + if (this.sponsorshipForm.invalid) { + return; + } + + this.loading = true; + // TODO: This is a mockup implementation - needs to be updated with actual API integration + await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call + + const formValue = this.sponsorshipForm.getRawValue(); + const dialogValue: Partial = { + status: "Sent", + sponsorshipEmail: formValue.sponsorshipEmail ?? "", + sponsorshipNote: formValue.sponsorshipNote ?? "", + }; + + this.dialogRef.close({ + action: AddSponsorshipDialogAction.Saved, + value: dialogValue, + }); + + this.loading = false; + } + + protected close = () => { + this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null }); + }; + + get sponsorshipEmailControl() { + return this.sponsorshipForm.controls.sponsorshipEmail; + } + + get sponsorshipNoteControl() { + return this.sponsorshipForm.controls.sponsorshipNote; + } + + private async validateNotCurrentUserEmail( + control: AbstractControl, + ): Promise { + const value = control.value; + if (!value) { + return null; + } + + const currentUserEmail = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.email ?? "")), + ); + + if (!currentUserEmail) { + return null; + } + + if (value.toLowerCase() === currentUserEmail.toLowerCase()) { + return { currentUserEmail: true }; + } + + return null; + } +} diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html new file mode 100644 index 00000000000..fe1dd15ab15 --- /dev/null +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +

{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}

diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts new file mode 100644 index 00000000000..af43e5a4bc1 --- /dev/null +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -0,0 +1,62 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; + +import { FreeFamiliesPolicyService } from "../services/free-families-policy.service"; + +import { + AddSponsorshipDialogComponent, + AddSponsorshipDialogResult, +} from "./add-sponsorship-dialog.component"; +import { SponsoredFamily } from "./types/sponsored-family"; + +@Component({ + selector: "app-free-bitwarden-families", + templateUrl: "free-bitwarden-families.component.html", +}) +export class FreeBitwardenFamiliesComponent implements OnInit { + tabIndex = 0; + sponsoredFamilies: SponsoredFamily[] = []; + + constructor( + private router: Router, + private dialogService: DialogService, + private freeFamiliesPolicyService: FreeFamiliesPolicyService, + ) {} + + async ngOnInit() { + await this.preventAccessToFreeFamiliesPage(); + } + + async addSponsorship() { + const addSponsorshipDialogRef: DialogRef = + AddSponsorshipDialogComponent.open(this.dialogService); + + const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed); + + if (dialogRef?.value) { + this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies]; + } + } + + removeSponsorhip(sponsorship: any) { + const index = this.sponsoredFamilies.findIndex( + (e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail, + ); + this.sponsoredFamilies.splice(index, 1); + } + + private async preventAccessToFreeFamiliesPage() { + const showFreeFamiliesPage = await firstValueFrom( + this.freeFamiliesPolicyService.showFreeFamilies$, + ); + + if (!showFreeFamiliesPage) { + await this.router.navigate(["/"]); + return; + } + } +} diff --git a/apps/web/src/app/billing/members/organization-member-families.component.html b/apps/web/src/app/billing/members/organization-member-families.component.html new file mode 100644 index 00000000000..c5b7283d9d9 --- /dev/null +++ b/apps/web/src/app/billing/members/organization-member-families.component.html @@ -0,0 +1,47 @@ + + +

+ {{ "membersWithSponsoredFamilies" | i18n }} +

+ +

{{ "memberFamilies" | i18n }}

+ + @if (loading) { + + + {{ "loading" | i18n }} + + } + + @if (!loading && memberFamilies?.length > 0) { + + + + + {{ "member" | i18n }} + {{ "status" | i18n }} + + + + + @for (o of memberFamilies; let i = $index; track i) { + + + {{ o.sponsorshipEmail }} + {{ o.status }} + + + } + + +
+
+ } @else { +
+ Search +

{{ "noMemberFamilies" | i18n }}

+

{{ "noMemberFamiliesDescription" | i18n }}

+
+ } +
+
diff --git a/apps/web/src/app/billing/members/organization-member-families.component.ts b/apps/web/src/app/billing/members/organization-member-families.component.ts new file mode 100644 index 00000000000..52c95646a11 --- /dev/null +++ b/apps/web/src/app/billing/members/organization-member-families.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Subject } from "rxjs"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { SponsoredFamily } from "./types/sponsored-family"; + +@Component({ + selector: "app-organization-member-families", + templateUrl: "organization-member-families.component.html", +}) +export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy { + tabIndex = 0; + loading = false; + + @Input() memberFamilies: SponsoredFamily[] = []; + + private _destroy = new Subject(); + + constructor(private platformUtilsService: PlatformUtilsService) {} + + async ngOnInit() { + this.loading = false; + } + + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + } + + get isSelfHosted(): boolean { + return this.platformUtilsService.isSelfHost(); + } +} diff --git a/apps/web/src/app/billing/members/organization-sponsored-families.component.html b/apps/web/src/app/billing/members/organization-sponsored-families.component.html new file mode 100644 index 00000000000..7db96deb4ab --- /dev/null +++ b/apps/web/src/app/billing/members/organization-sponsored-families.component.html @@ -0,0 +1,87 @@ + + +

+ {{ "sponsorFreeBitwardenFamilies" | i18n }} +

+
+ {{ "sponsoredFamiliesInclude" | i18n }}: +
    +
  • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
  • +
  • {{ "sponsoredFamiliesSharedCollections" | i18n }}
  • +
+
+ +

{{ "sponsoredBitwardenFamilies" | i18n }}

+ + @if (loading) { + + + {{ "loading" | i18n }} + + } + + @if (!loading && sponsoredFamilies?.length > 0) { + + + + + {{ "recipient" | i18n }} + {{ "status" | i18n }} + {{ "notes" | i18n }} + + + + + @for (o of sponsoredFamilies; let i = $index; track i) { + + + {{ o.sponsorshipEmail }} + {{ o.status }} + {{ o.sponsorshipNote }} + + + + + +
+ + +
+ + +
+ } +
+
+
+
+ } @else { +
+ Search +

{{ "noSponsoredFamilies" | i18n }}

+

{{ "noSponsoredFamiliesDescription" | i18n }}

+
+ } +
+
diff --git a/apps/web/src/app/billing/members/organization-sponsored-families.component.ts b/apps/web/src/app/billing/members/organization-sponsored-families.component.ts new file mode 100644 index 00000000000..7cc46634a38 --- /dev/null +++ b/apps/web/src/app/billing/members/organization-sponsored-families.component.ts @@ -0,0 +1,39 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Subject } from "rxjs"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { SponsoredFamily } from "./types/sponsored-family"; + +@Component({ + selector: "app-organization-sponsored-families", + templateUrl: "organization-sponsored-families.component.html", +}) +export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy { + loading = false; + tabIndex = 0; + + @Input() sponsoredFamilies: SponsoredFamily[] = []; + @Output() removeSponsorshipEvent = new EventEmitter(); + + private _destroy = new Subject(); + + constructor(private platformUtilsService: PlatformUtilsService) {} + + async ngOnInit() { + this.loading = false; + } + + get isSelfHosted(): boolean { + return this.platformUtilsService.isSelfHost(); + } + + remove(sponsorship: SponsoredFamily) { + this.removeSponsorshipEvent.emit(sponsorship); + } + + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + } +} diff --git a/apps/web/src/app/billing/members/types/sponsored-family.ts b/apps/web/src/app/billing/members/types/sponsored-family.ts new file mode 100644 index 00000000000..82d2e3948b2 --- /dev/null +++ b/apps/web/src/app/billing/members/types/sponsored-family.ts @@ -0,0 +1,5 @@ +export interface SponsoredFamily { + sponsorshipEmail?: string; + sponsorshipNote?: string; + status?: string; +} diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 5dc34b3b5b1..469ebe457d0 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -62,6 +62,9 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization- import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; import { PurgeVaultComponent } from "../vault/settings/purge-vault.component"; +import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component"; +import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component"; +import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component"; import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module"; import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "./shared.module"; @@ -128,6 +131,9 @@ import { SharedModule } from "./shared.module"; SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, + OrganizationSponsoredFamiliesComponent, + OrganizationMemberFamiliesComponent, + FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, UpdatePasswordComponent, UpdateTempPasswordComponent, @@ -175,6 +181,9 @@ import { SharedModule } from "./shared.module"; SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, + OrganizationSponsoredFamiliesComponent, + OrganizationMemberFamiliesComponent, + FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, UpdateTempPasswordComponent, UpdatePasswordComponent, diff --git a/apps/web/src/images/search.svg b/apps/web/src/images/search.svg new file mode 100644 index 00000000000..36e0ea4bd23 --- /dev/null +++ b/apps/web/src/images/search.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 85a7b8cb927..3d6cf0f23a5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6306,6 +6306,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6321,6 +6336,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7984,6 +8011,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts index 534afffd1bb..19e993487c2 100644 --- a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts @@ -6,4 +6,5 @@ export class OrganizationSponsorshipCreateRequest { sponsoredEmail: string; planSponsorshipType: PlanSponsorshipType; friendlyName: string; + notes?: string; } From 6c930deefee42365b256e558f7d3cdaa43b88aee Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:08:39 -0500 Subject: [PATCH 10/30] Migrate bootstrap styling to use tailwind while keeping the same styling (#14203) --- .../pages/breach-report.component.html | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/apps/web/src/app/tools/reports/pages/breach-report.component.html b/apps/web/src/app/tools/reports/pages/breach-report.component.html index fcfd37fef3f..d645fa39d69 100644 --- a/apps/web/src/app/tools/reports/pages/breach-report.component.html +++ b/apps/web/src/app/tools/reports/pages/breach-report.component.html @@ -7,7 +7,7 @@ {{ "username" | i18n }} - {{ "breachCheckUsernameEmail" | i18n }} + {{ "breachCheckUsernameEmail" | i18n }} @@ -21,32 +21,33 @@ {{ "breachUsernameFound" | i18n: checkedUsername : breachedAccounts.length }} -
    -
  • -
    -
    - -
    -
    -

    {{ a.title }}

    -

    -

    {{ "compromisedData" | i18n }}:

    -
      -
    • {{ d }}
    • -
    -
    -
    -
    -
    {{ "website" | i18n }}
    -
    {{ a.domain }}
    -
    {{ "affectedUsers" | i18n }}
    -
    {{ a.pwnCount | number }}
    -
    {{ "breachOccurred" | i18n }}
    -
    {{ a.breachDate | date: "mediumDate" }}
    -
    {{ "breachReported" | i18n }}
    -
    {{ a.addedDate | date: "mediumDate" }}
    -
    -
    +
      +
    • +
      + +
      +
      +

      {{ a.title }}

      +

      +

      {{ "compromisedData" | i18n }}:

      +
        +
      • {{ d }}
      • +
      +
      +
      +
      +
      {{ "website" | i18n }}
      +
      {{ a.domain }}
      +
      {{ "affectedUsers" | i18n }}
      +
      {{ a.pwnCount | number }}
      +
      {{ "breachOccurred" | i18n }}
      +
      {{ a.breachDate | date: "mediumDate" }}
      +
      {{ "breachReported" | i18n }}
      +
      {{ a.addedDate | date: "mediumDate" }}
      +
    From ede4776433a41b1d4678d3402307c4c557fda887 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 17 Apr 2025 16:25:35 +0200 Subject: [PATCH 11/30] [PM-18043] Add serialization/deserialization support (#13804) * feat: add support for serialization-PR tweaks * feat: update sdk version --- .../src/platform/ipc/background-communication-backend.ts | 6 +++++- .../src/app/platform/ipc/web-communication-provider.ts | 8 +++++++- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/platform/ipc/background-communication-backend.ts b/apps/browser/src/platform/ipc/background-communication-backend.ts index 6c5b374dd56..1ebb835fa3b 100644 --- a/apps/browser/src/platform/ipc/background-communication-backend.ts +++ b/apps/browser/src/platform/ipc/background-communication-backend.ts @@ -18,7 +18,11 @@ export class BackgroundCommunicationBackend implements CommunicationBackend { return; } - void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } }); + void this.queue.enqueue( + new IncomingMessage(message.message.payload, message.message.destination, { + Web: { id: sender.tab.id }, + }), + ); }); } diff --git a/apps/web/src/app/platform/ipc/web-communication-provider.ts b/apps/web/src/app/platform/ipc/web-communication-provider.ts index 85353ab77af..787a3c7f3a4 100644 --- a/apps/web/src/app/platform/ipc/web-communication-provider.ts +++ b/apps/web/src/app/platform/ipc/web-communication-provider.ts @@ -19,7 +19,13 @@ export class WebCommunicationProvider implements CommunicationBackend { return; } - await this.queue.enqueue({ ...message.message, source: "BrowserBackground" }); + void this.queue.enqueue( + new IncomingMessage( + message.message.payload, + message.message.destination, + "BrowserBackground", + ), + ); }); } diff --git a/package-lock.json b/package-lock.json index c101444cd8e..65f0c87721a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.133", + "@bitwarden/sdk-internal": "0.2.0-main.137", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -4700,9 +4700,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.133", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.133.tgz", - "integrity": "sha512-KzKJGf9cKlcQzfRmqkAwVGBN1kDpcRFkTMm7nrphZSrjfaWJWI1lBEJ0DhnkbMMHJXhQavGyoVk5TIn/Y8ylmw==", + "version": "0.2.0-main.137", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.137.tgz", + "integrity": "sha512-Df0pB5tOEc4WiMjskunTrqHulPzenFv8C61sqsBhHfy80xcf5kU5JyPd4asbf3e4uNS6QGXptd8imp09AuiFEA==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { diff --git a/package.json b/package.json index a39004dcc03..c78decb9827 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.133", + "@bitwarden/sdk-internal": "0.2.0-main.137", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", From e0df1ecf0c4b69ebebb34d0a07d54a1f87097918 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 17 Apr 2025 17:33:13 +0200 Subject: [PATCH 12/30] [PM-19180] Calculate sales tax correctly for sponsored plans (#14129) * [PM-19180] Sponsored family org no sales tax because they're free * [PM-19180][DRAFT] Calculate sales tax correctly for sponsored plans with additional storage --------- Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../families-for-enterprise-setup.component.ts | 2 ++ .../organizations/organization-plans.component.ts | 14 ++++++++------ .../preview-organization-invoice.request.ts | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index fdad689d982..57fe212fa65 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -43,6 +43,8 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { value.plan = PlanType.FamiliesAnnually; value.productTier = ProductTierType.Families; value.acceptingSponsorship = true; + value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise; + // eslint-disable-next-line rxjs-angular/prefer-takeuntil value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this)); } diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index fc7d6793a85..59f8dd34c37 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -34,7 +34,12 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { + PaymentMethodType, + PlanSponsorshipType, + PlanType, + ProductTierType, +} from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; 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"; @@ -83,6 +88,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { @Input() showFree = true; @Input() showCancel = false; @Input() acceptingSponsorship = false; + @Input() planSponsorshipType?: PlanSponsorshipType; @Input() currentPlan: PlanResponse; selectedFile: File; @@ -682,11 +688,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } private refreshSalesTax(): void { - if (this.formGroup.controls.plan.value == PlanType.Free) { - this.estimatedTax = 0; - return; - } - if (!this.taxComponent.validate()) { return; } @@ -696,6 +697,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { passwordManager: { additionalStorage: this.formGroup.controls.additionalStorage.value, plan: this.formGroup.controls.plan.value, + sponsoredPlan: this.planSponsorshipType, seats: this.formGroup.controls.additionalSeats.value, }, taxInformation: { diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts index 40d8db03d3b..bfeecb4eb23 100644 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -1,4 +1,4 @@ -import { PlanType } from "../../enums"; +import { PlanSponsorshipType, PlanType } from "../../enums"; export class PreviewOrganizationInvoiceRequest { organizationId?: string; @@ -21,6 +21,7 @@ export class PreviewOrganizationInvoiceRequest { class PasswordManager { plan: PlanType; + sponsoredPlan?: PlanSponsorshipType; seats: number; additionalStorage: number; From 5af12505f1f97d172d6a7c44a9557b062c406221 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:01:02 -0400 Subject: [PATCH 13/30] Switch userVisibleOnly to `false` (#14202) --- .../notifications/internal/worker-webpush-connection.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts index 74981b6782f..a1143d14d1d 100644 --- a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts @@ -134,7 +134,7 @@ class MyWebPushConnector implements WebPushConnector { private async pushManagerSubscribe(key: string) { return await this.serviceWorkerRegistration.pushManager.subscribe({ - userVisibleOnly: true, + userVisibleOnly: false, applicationServerKey: key, }); } From 7bf4bae7c6d693c8d438aaa082e28a147be85e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Fri, 18 Apr 2025 10:43:22 +0200 Subject: [PATCH 14/30] Revert event-driven .show, in order for bootstrap/hide-to-tray to work (#14181) --- apps/desktop/src/main/window.main.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 73231f1f730..1c396b09b21 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -292,11 +292,7 @@ export class WindowMain { this.win.maximize(); } - // Show it later since it might need to be maximized. - // use once event to avoid flash on unstyled content. - this.win.once("ready-to-show", () => { - this.win.show(); - }); + this.win.show(); if (template === "full-app") { // and load the index.html of the app. From d6beca569c4e80bd1657f5e0aca3a9f18ad697c2 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 18 Apr 2025 08:45:05 -0500 Subject: [PATCH 15/30] [PM-19810] Member Access Report - CSV export fix (#14313) --- .../services/member-access-report.mock.ts | 38 +++++++++++++++++++ .../member-access-report.service.spec.ts | 34 ++++++++++++++++- .../services/member-access-report.service.ts | 20 ++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts index 9ace555dd2e..b07e4946ca7 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts @@ -229,3 +229,41 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [ ], } as MemberAccessResponse, ]; + +export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [ + { + userName: "Alice Smith", + email: "asmith@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ + { + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "Alice Group 1", + itemCount: 10, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + ], + } as MemberAccessResponse, + { + userName: "Robert Brown", + email: "rbrown@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "5678", + usesKeyConnector: false, + accessDetails: [] as MemberAccessDetails[], + } as MemberAccessResponse, +]; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 7d6beca48ec..e6efac83616 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -4,7 +4,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; -import { memberAccessReportsMock } from "./member-access-report.mock"; +import { + memberAccessReportsMock, + memberAccessWithoutAccessDetailsReportsMock, +} from "./member-access-report.mock"; import { MemberAccessReportService } from "./member-access-report.service"; describe("ImportService", () => { const mockOrganizationId = "mockOrgId" as OrganizationId; @@ -112,5 +115,34 @@ describe("ImportService", () => { ]), ); }); + + it("should generate user report export items and include users with no access", async () => { + reportApiService.getMemberAccessData.mockImplementation(() => + Promise.resolve(memberAccessWithoutAccessDetailsReportsMock), + ); + const result = + await memberAccessReportService.generateUserReportExportItems(mockOrganizationId); + + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: "asmith@email.com", + name: "Alice Smith", + twoStepLogin: "memberAccessReportTwoFactorEnabledTrue", + accountRecovery: "memberAccessReportAuthenticationEnabledTrue", + group: "Alice Group 1", + totalItems: "10", + }), + expect.objectContaining({ + email: "rbrown@email.com", + name: "Robert Brown", + twoStepLogin: "memberAccessReportTwoFactorEnabledFalse", + accountRecovery: "memberAccessReportAuthenticationEnabledFalse", + group: "memberAccessReportNoGroup", + totalItems: "0", + }), + ]), + ); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index b7ff5551e2c..029dce8a404 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -65,6 +65,26 @@ export class MemberAccessReportService { } const exportItems = memberAccessReports.flatMap((report) => { + // to include users without access details + // which means a user has no groups, collections or items + if (report.accessDetails.length === 0) { + return [ + { + email: report.email, + name: report.userName, + twoStepLogin: report.twoFactorEnabled + ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") + : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), + accountRecovery: report.accountRecoveryEnabled + ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") + : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), + group: this.i18nService.t("memberAccessReportNoGroup"), + collection: this.i18nService.t("memberAccessReportNoCollection"), + collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"), + totalItems: "0", + }, + ]; + } const userDetails = report.accessDetails.map((detail) => { const collectionName = collectionNameMap.get(detail.collectionName.encryptedString); return { From 9d16435d0858d40783ff8bc5494f27314cf00a0f Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 18 Apr 2025 09:52:12 -0400 Subject: [PATCH 16/30] docs(ViewModel): Add JSDocs to view to explain proper use (#14214) --- libs/common/src/models/view/view.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/common/src/models/view/view.ts b/libs/common/src/models/view/view.ts index 1f16b3d5958..2869617dca5 100644 --- a/libs/common/src/models/view/view.ts +++ b/libs/common/src/models/view/view.ts @@ -1 +1,5 @@ +// See https://contributing.bitwarden.com/architecture/clients/data-model/#view for proper use. +// View models represent the decrypted state of a corresponding Domain model. +// They typically match the Domain model but contains a decrypted string for any EncString fields. +// Don't use this to represent arbitrary component view data as that isn't what it is for. export class View {} From e026799071e39219f600c8e1fca9fa024b4be844 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 18 Apr 2025 15:57:27 +0200 Subject: [PATCH 17/30] [PM-13128] Enable Breadcrumb Policies (#13584) * [PM-13128] Enable Breadcrumb Policies * [PM-13128] Enable Breadcrumb Policies * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * [PM-13128] wip * remove dead code * wip * wip * wip * refactor * Fix for providers * revert to functional auth guard * change prerequisite to info variant * address comment * r * r * r * tests * r * r * fix tests * feedback * fix tests * fix tests * Rename upselling to breadcrumbing * Address feedback * Fix build & tests * Make the guard callback use Observable instead of a promise * Pm 13128 suggestions (#14041) * Rename new enum value * Show the upgrade button when breadcrumbing is enabled * Show mouse pointer when cursor is hovered above badge * Do not make the dialogs overlap * Align badge middle * Gap * Badge should be a `button` instead of `span` * missing button@type --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Alex Morask --- .../guards/org-permissions.guard.spec.ts | 30 +++- .../guards/org-permissions.guard.ts | 26 ++- .../organization-layout.component.html | 2 +- .../layouts/organization-layout.component.ts | 15 ++ .../policies/policies.component.html | 15 +- .../policies/policies.component.ts | 61 +++++-- .../policies/policy-edit.component.html | 18 +++ .../policies/policy-edit.component.ts | 33 +++- .../organization-settings-routing.module.ts | 13 +- .../layouts/header/web-header.component.html | 2 +- .../src/services/jslib-services.module.ts | 1 + .../organization-billing.service.ts | 9 ++ .../organization-billing.service.spec.ts | 149 ++++++++++++++++++ .../services/organization-billing.service.ts | 33 +++- 14 files changed, 380 insertions(+), 27 deletions(-) create mode 100644 libs/common/src/billing/services/organization-billing.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts index 9afd34ca149..d628e23063f 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts @@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => { it("permits navigation if the user has permissions", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => true); + permissionsCallback.mockReturnValue(true); const actual = await TestBed.runInInjectionContext( async () => await organizationPermissionsGuard(permissionsCallback)(route, state), ); - expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId })); + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles a Promise returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(Promise.resolve(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); + expect(actual).toBe(true); + }); + + it("handles an Observable returned from the callback", async () => { + const permissionsCallback = jest.fn(); + permissionsCallback.mockReturnValue(of(true)); + + const actual = await TestBed.runInInjectionContext(() => + organizationPermissionsGuard(permissionsCallback)(route, state), + ); + + expect(permissionsCallback).toHaveBeenCalledTimes(1); expect(actual).toBe(true); }); describe("if the user does not have permissions", () => { it("and there is no Item ID, block navigation", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((_org) => false); + permissionsCallback.mockReturnValue(false); state = mock({ root: mock({ diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts index d399f9c9c05..6c9090a27b4 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts @@ -1,13 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { inject } from "@angular/core"; +import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot, } from "@angular/router"; -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs"; import { canAccessOrgAdmin, @@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components"; * proceeds as expected. */ export function organizationPermissionsGuard( - permissionsCallback?: (organization: Organization) => boolean, + permissionsCallback?: ( + organization: Organization, + ) => boolean | Promise | Observable, ): CanActivateFn { return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const router = inject(Router); @@ -51,6 +53,7 @@ export function organizationPermissionsGuard( const i18nService = inject(I18nService); const syncService = inject(SyncService); const accountService = inject(AccountService); + const environmentInjector = inject(EnvironmentInjector); // TODO: We need to fix issue once and for all. if ((await syncService.getLastSync()) == null) { @@ -78,7 +81,22 @@ export function organizationPermissionsGuard( return router.createUrlTree(["/"]); } - const hasPermissions = permissionsCallback == null || permissionsCallback(org); + if (permissionsCallback == null) { + // No additional permission checks required, allow navigation + return true; + } + + const callbackResult = runInInjectionContext(environmentInjector, () => + permissionsCallback(org), + ); + + const hasPermissions = isObservable(callbackResult) + ? await firstValueFrom(callbackResult) // handles observables + : await Promise.resolve(callbackResult); // handles promises and boolean values + + if (hasPermissions !== true && hasPermissions !== false) { + throw new Error("Permission callback did not resolve to a boolean."); + } if (!hasPermissions) { // Handle linkable ciphers for organizations the user only has view access to 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 e50c55e83d2..fec790dabcb 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 @@ -97,7 +97,7 @@ ; protected isBreadcrumbEventLogsEnabled$: Observable; protected showSponsoredFamiliesDropdown$: Observable; + protected canShowPoliciesTab$: Observable; constructor( private route: ActivatedRoute, @@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit { protected bannerService: AccountDeprovisioningBannerService, private accountService: AccountService, private freeFamiliesPolicyService: FreeFamiliesPolicyService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} async ngOnInit() { @@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit { )) ? "claimedDomains" : "domainVerification"; + + this.canShowPoliciesTab$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService + .isBreadcrumbingPoliciesEnabled$(organization) + .pipe( + map( + (isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies, + ), + ), + ), + ); } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 24021bb765f..e40b9d80e9e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,4 +1,17 @@ - + + @let organization = organization$ | async; + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 52cb4da107a..2b86d76d9b1 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, lastValueFrom } from "rxjs"; -import { first, map } from "rxjs/operators"; +import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs"; +import { first } from "rxjs/operators"; import { getOrganizationById, @@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { DialogService } from "@bitwarden/components"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component"; +import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { PolicyListService } from "../../core/policy-list.service"; import { BasePolicy } from "../policies"; +import { CollectionDialogTabType } from "../shared/components/collection-dialog"; import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component"; @@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit { loading = true; organizationId: string; policies: BasePolicy[]; - organization: Organization; + protected organization$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); + protected isBreadcrumbingEnabled$: Observable; constructor( private route: ActivatedRoute, - private organizationService: OrganizationService, private accountService: AccountService, + private organizationService: OrganizationService, private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, + private organizationBillingService: OrganizationBillingServiceAbstraction, private dialogService: DialogService, ) {} @@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit { const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - this.organization = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); + this.organization$ = this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)); this.policies = this.policyListService.getPolicies(); await this.load(); @@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit { this.orgPolicies.forEach((op) => { this.policiesEnabledMap.set(op.type, op.enabled); }); - + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); this.loading = false; } @@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); - if (result === PolicyEditDialogResult.Saved) { - await this.load(); + switch (result) { + case PolicyEditDialogResult.Saved: + await this.load(); + break; + case PolicyEditDialogResult.UpgradePlan: + await this.changePlan(await firstValueFrom(this.organization$)); + break; } } + + protected readonly CollectionDialogTabType = CollectionDialogTabType; + protected readonly All = All; + + protected async changePlan(organization: Organization) { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: organization.id, + subscription: null, + productTierType: organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + + await this.load(); + } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 671083a2318..7f33f08f888 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -1,5 +1,17 @@
    + + +
    + + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index f33460e8c16..49f4d15a100 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -9,12 +9,20 @@ import { ViewContainerRef, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Observable, map } from "rxjs"; +import { map, Observable, switchMap } from "rxjs"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -35,6 +43,7 @@ export type PolicyEditDialogData = { export enum PolicyEditDialogResult { Saved = "saved", + UpgradePlan = "upgrade-plan", } @Component({ selector: "app-policy-edit", @@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit { loading = true; enabled = false; saveDisabled$: Observable; - defaultTypes: any[]; policyComponent: BasePolicyComponent; private policyResponse: PolicyResponse; formGroup = this.formBuilder.group({ enabled: [this.enabled], }); + protected organization$: Observable; + protected isBreadcrumbingEnabled$: Observable; + constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, + private organizationService: OrganizationService, private i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, private toastService: ToastService, + private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} + get policy(): BasePolicy { return this.data.policy; } @@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit { throw e; } } + this.organization$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.organizationService.organizations$(userId)), + getOrganizationById(this.data.organizationId), + ); + this.isBreadcrumbingEnabled$ = this.organization$.pipe( + switchMap((organization) => + this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), + ), + ); } submit = async () => { @@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit { static open = (dialogService: DialogService, config: DialogConfig) => { return dialogService.open(PolicyEditComponent, config); }; + + protected upgradePlan(): void { + this.dialogRef.close(PolicyEditDialogResult.UpgradePlan); + } } diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index a644086628c..cfec0be531b 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -1,8 +1,10 @@ -import { NgModule } from "@angular/core"; +import { NgModule, inject } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; @@ -41,7 +43,14 @@ const routes: Routes = [ { path: "policies", component: PoliciesComponent, - canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)], + canActivate: [ + organizationPermissionsGuard((o: Organization) => { + const organizationBillingService = inject(OrganizationBillingServiceAbstraction); + return organizationBillingService + .isBreadcrumbingPoliciesEnabled$(o) + .pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled)); + }), + ], data: { titleId: "policies", }, diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 28d786f2d64..4b0da4ae569 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -12,7 +12,7 @@

    diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3cce9b5357e..8e2b3409593 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [ I18nServiceAbstraction, OrganizationApiServiceAbstraction, SyncService, + ConfigService, ], }), safeProvider({ diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 69309014fac..8024a120b0a 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,5 +1,8 @@ // 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"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; @@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction { organizationId: string, subscription: SubscriptionInformation, ) => Promise; + + /** + * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. + * @param organization + */ + abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable; } diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts new file mode 100644 index 00000000000..7b194dff637 --- /dev/null +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -0,0 +1,149 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +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 { KeyService } from "@bitwarden/key-management"; + +describe("BillingAccountProfileStateService", () => { + let apiService: jest.Mocked; + let billingApiService: jest.Mocked; + let keyService: jest.Mocked; + let encryptService: jest.Mocked; + let i18nService: jest.Mocked; + let organizationApiService: jest.Mocked; + let syncService: jest.Mocked; + let configService: jest.Mocked; + + let sut: OrganizationBillingService; + + beforeEach(() => { + apiService = mock(); + billingApiService = mock(); + keyService = mock(); + encryptService = mock(); + i18nService = mock(); + organizationApiService = mock(); + syncService = mock(); + configService = mock(); + + sut = new OrganizationBillingService( + apiService, + billingApiService, + keyService, + encryptService, + i18nService, + organizationApiService, + syncService, + configService, + ); + }); + + afterEach(() => { + return jest.resetAllMocks(); + }); + + describe("isBreadcrumbingPoliciesEnabled", () => { + it("returns false when feature flag is disabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when organization belongs to a provider", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: true, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("returns false when cannot edit subscription", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: false, + productTierType: ProductTierType.Teams, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it.each([ + ["Teams", ProductTierType.Teams], + ["TeamsStarter", ProductTierType.TeamsStarter], + ])("returns true when all conditions are met with %s tier", async (_, productTierType) => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: productTierType, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(true); + expect(configService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + }); + + it("returns false when product tier is not supported", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Enterprise, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("handles all conditions false correctly", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: true, + canEditSubscription: false, + productTierType: ProductTierType.Free, + } as Organization; + + const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(actual).toBe(false); + }); + + it("verifies feature flag is only called once", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + const org = { + isProviderUser: false, + canEditSubscription: true, + productTierType: ProductTierType.Teams, + } as Organization; + + await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); + expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 83efbf0a30c..6622cdcdce3 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,5 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { Observable, of, switchMap } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -20,7 +25,7 @@ import { PlanInformation, SubscriptionInformation, } from "../abstractions"; -import { PlanType } from "../enums"; +import { PlanType, ProductTierType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; @@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private i18nService: I18nService, private organizationApiService: OrganizationApiService, private syncService: SyncService, + private configService: ConfigService, ) {} async getPaymentSource(organizationId: string): Promise { @@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); await this.billingApiService.restartSubscription(organizationId, request); } + + isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable { + if (organization === null || organization === undefined) { + return of(false); + } + + return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe( + switchMap((featureFlagEnabled) => { + if (!featureFlagEnabled) { + return of(false); + } + + if (organization.isProviderUser || !organization.canEditSubscription) { + return of(false); + } + + const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter]; + const isSupportedProduct = supportedProducts.some( + (product) => product === organization.productTierType, + ); + + return of(isSupportedProduct); + }), + ); + } } From cbab354c0eaecc461c33f6d616fcc212b519bca2 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Fri, 18 Apr 2025 10:38:19 -0400 Subject: [PATCH 18/30] Hide bit-icon component from screen readers by default (#14295) * adds aria-hidden to bit-icon when no aria-label provided * add ariaLabel to logo svg usages * add ariaLabel documentation * default ariaLable value to undefined * add logo label to translations * adds i18n pipe to component * Add binding to example docs --- apps/browser/src/_locales/en/messages.json | 3 +++ .../extension-anon-layout-wrapper.component.html | 7 ++++++- .../extension-anon-layout-wrapper.component.ts | 2 ++ .../accept-family-sponsorship.component.html | 3 ++- apps/web/src/locales/en/messages.json | 3 +++ .../manage/accept-provider.component.html | 6 +++++- .../providers/setup/setup-provider.component.html | 6 +++++- .../setup/setup-business-unit.component.html | 6 +++++- .../angular/anon-layout/anon-layout.component.html | 2 +- libs/components/src/icon/icon.component.ts | 14 +++++++++----- libs/components/src/icon/icon.mdx | 10 ++++++++++ libs/components/src/icon/icon.stories.ts | 4 ++++ 12 files changed, 55 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 87b94650b51..b0f3fb5d19e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2,6 +2,9 @@ "appName": { "message": "Bitwarden" }, + "appLogoLabel": { + "message": "Bitwarden logo" + }, "extName": { "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 54cb5203a87..bd2886dacf0 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -5,7 +5,12 @@ [showBackButton]="showBackButton" [pageTitle]="''" > - + diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 51dbb6503d7..d6cccf31bb4 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -12,6 +12,7 @@ import { } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Icon, IconModule, Translation } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -36,6 +37,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { AnonLayoutComponent, CommonModule, CurrentAccountComponent, + I18nPipe, IconModule, PopOutComponent, PopupPageComponent, diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index fdbb6dbba91..ca1264829b9 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -1,6 +1,7 @@
    - + +
    - +

    - +

    - +

    - +

    NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an + > `ariaLabel` is explicitly provided to the `` component + ```html ``` + With `ariaLabel` + + ```html + + ``` + 8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client which supports multiple style modes. diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 53454567b7f..7892bdd3ec1 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -26,5 +26,9 @@ export const Default: Story = { mapping: GenericIcons, control: { type: "select" }, }, + ariaLabel: { + control: "text", + description: "the text used by a screen reader to describe the icon", + }, }, }; From 4da03821cee44184c8567f93131818340a50a5fd Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:23:52 +0200 Subject: [PATCH 19/30] move key connector components to KM team ownership (#14008) --- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 2 +- apps/browser/src/popup/app.module.ts | 2 +- apps/cli/src/auth/commands/unlock.command.ts | 2 +- .../convert-to-key-connector.command.ts | 0 apps/desktop/src/app/app-routing.module.ts | 2 +- apps/desktop/src/app/app.module.ts | 2 +- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- .../key-connector}/remove-password.component.html | 0 .../key-connector}/remove-password.component.ts | 2 +- apps/web/src/app/oss-routing.module.ts | 2 +- apps/web/src/app/shared/loose-components.module.ts | 2 +- libs/key-management-ui/src/index.ts | 1 + .../src/key-connector}/remove-password.component.ts | 0 16 files changed, 11 insertions(+), 10 deletions(-) rename apps/browser/src/{auth/popup => key-management/key-connector}/remove-password.component.html (100%) rename apps/browser/src/{auth/popup => key-management/key-connector}/remove-password.component.ts (80%) rename apps/cli/src/{commands => key-management}/convert-to-key-connector.command.ts (100%) rename apps/desktop/src/{auth => key-management/key-connector}/remove-password.component.html (100%) rename apps/desktop/src/{auth => key-management/key-connector}/remove-password.component.ts (80%) rename apps/web/src/app/{auth => key-management/key-connector}/remove-password.component.html (100%) rename apps/web/src/app/{auth => key-management/key-connector}/remove-password.component.ts (80%) rename libs/{angular/src/auth/components => key-management-ui/src/key-connector}/remove-password.component.ts (100%) diff --git a/apps/browser/src/auth/popup/remove-password.component.html b/apps/browser/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/browser/src/auth/popup/remove-password.component.html rename to apps/browser/src/key-management/key-connector/remove-password.component.html diff --git a/apps/browser/src/auth/popup/remove-password.component.ts b/apps/browser/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/browser/src/auth/popup/remove-password.component.ts rename to apps/browser/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/browser/src/auth/popup/remove-password.component.ts +++ b/apps/browser/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 21ac4c19700..45955506b91 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -55,7 +55,6 @@ import { ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperData, } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -65,6 +64,7 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 0d392afa63b..8bea41da4d6 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -25,13 +25,13 @@ import { import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { RemovePasswordComponent } from "../auth/popup/remove-password.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"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 6d524759dd6..d10c3f38ebd 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -17,7 +17,7 @@ import { MasterKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; -import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command"; +import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command"; import { Response } from "../../models/response"; import { MessageResponse } from "../../models/response/message.response"; import { CliUtils } from "../../utils"; diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/key-management/convert-to-key-connector.command.ts similarity index 100% rename from apps/cli/src/commands/convert-to-key-connector.command.ts rename to apps/cli/src/key-management/convert-to-key-connector.command.ts diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index cd5064a87e4..0c6bc730c2c 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -50,9 +50,9 @@ import { import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; 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 { VaultComponent } from "../vault/app/vault/vault.component"; import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component"; diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index fdc25ed642e..b892324a979 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -13,11 +13,11 @@ import { DecryptionFailureDialogComponent } from "@bitwarden/vault"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginModule } from "../auth/login/login.module"; -import { RemovePasswordComponent } from "../auth/remove-password.component"; 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"; import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component"; import { AddEditComponent } from "../vault/app/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/app/vault/attachments.component"; diff --git a/apps/desktop/src/auth/remove-password.component.html b/apps/desktop/src/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/desktop/src/auth/remove-password.component.html rename to apps/desktop/src/key-management/key-connector/remove-password.component.html diff --git a/apps/desktop/src/auth/remove-password.component.ts b/apps/desktop/src/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/desktop/src/auth/remove-password.component.ts rename to apps/desktop/src/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/desktop/src/auth/remove-password.component.ts +++ b/apps/desktop/src/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/auth/remove-password.component.html b/apps/web/src/app/key-management/key-connector/remove-password.component.html similarity index 100% rename from apps/web/src/app/auth/remove-password.component.html rename to apps/web/src/app/key-management/key-connector/remove-password.component.html diff --git a/apps/web/src/app/auth/remove-password.component.ts b/apps/web/src/app/key-management/key-connector/remove-password.component.ts similarity index 80% rename from apps/web/src/app/auth/remove-password.component.ts rename to apps/web/src/app/key-management/key-connector/remove-password.component.ts index 5272a3082a2..3ca9d3a5669 100644 --- a/apps/web/src/app/auth/remove-password.component.ts +++ b/apps/web/src/app/key-management/key-connector/remove-password.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; -import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component"; +import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui"; @Component({ selector: "app-remove-password", diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 0334519516a..fc8356505f5 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -58,7 +58,6 @@ 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 { RemovePasswordComponent } from "./auth/remove-password.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"; @@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; import { RouteDataProperties } from "./core"; +import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 469ebe457d0..eb63b9f798c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -15,7 +15,6 @@ 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 { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { AccountComponent } from "../auth/settings/account/account.component"; import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component"; @@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; import { SelectableAvatarComponent } from "../components/selectable-avatar.component"; +import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { FrontendLayoutComponent } from "../layouts/frontend-layout.component"; import { HeaderModule } from "../layouts/header/header.module"; import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module"; diff --git a/libs/key-management-ui/src/index.ts b/libs/key-management-ui/src/index.ts index 2f98538caad..b330e390d36 100644 --- a/libs/key-management-ui/src/index.ts +++ b/libs/key-management-ui/src/index.ts @@ -7,3 +7,4 @@ export { LockComponentService, UnlockOptions } from "./lock/services/lock-compon export { KeyRotationTrustInfoComponent } from "./key-rotation/key-rotation-trust-info.component"; export { AccountRecoveryTrustComponent } from "./trust/account-recovery-trust.component"; export { EmergencyAccessTrustComponent } from "./trust/emergency-access-trust.component"; +export { RemovePasswordComponent } from "./key-connector/remove-password.component"; diff --git a/libs/angular/src/auth/components/remove-password.component.ts b/libs/key-management-ui/src/key-connector/remove-password.component.ts similarity index 100% rename from libs/angular/src/auth/components/remove-password.component.ts rename to libs/key-management-ui/src/key-connector/remove-password.component.ts From da57b944ae77e29c5032af5f27c35a5eae2f6644 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:47:04 -0400 Subject: [PATCH 20/30] chore(deps): Remove Platform from owning Cargo.lock --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f402b15dd5..de28b210887 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,8 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev -## No ownership for Cargo.toml to allow dependency updates +## No ownership fo Cargo.lock and Cargo.toml to allow dependency updates +apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml ## Auth team files ## From a82996526279bc683a31c6c944810b37a7836fef Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 18 Apr 2025 13:05:58 -0500 Subject: [PATCH 21/30] [PM-20386] valuesChanges returns a string (#14338) --- .../vault-export-ui/src/components/export.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4e9b4175838..71599c19ae0 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 @@ -220,7 +220,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(([value]) => { + .subscribe((value) => { this.organizationId = value !== "myVault" ? value : undefined; this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip"); From f86a5c2b6ea4bf18194f3c5875c8c1e63ea3f36f Mon Sep 17 00:00:00 2001 From: Chase Nelson Date: Fri, 18 Apr 2025 14:55:23 -0400 Subject: [PATCH 22/30] [PM-19798] [PM-18807] Fix base64 encoding/decoding with special characters (#14089) * Refactor base64 encoding/decoding to use BufferLib * Add tests for base64 encoding and decoding functions --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- libs/common/src/platform/misc/utils.spec.ts | 69 +++++++++++++++++++++ libs/common/src/platform/misc/utils.ts | 4 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index 964a2a19413..818138863fb 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -706,4 +706,73 @@ describe("Utils Service", () => { }); }); }); + + describe("fromUtf8ToB64(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromUtf8ToB64(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromUtf8ToB64(asciiHelloWorld); + expect(str).toBe(b64HelloWorldString); + }); + + runInBothEnvironments("should convert various special characters", () => { + const cases = [ + { input: "»", output: "wrs=" }, + { input: "¦", output: "wqY=" }, + { input: "£", output: "wqM=" }, + { input: "é", output: "w6k=" }, + { input: "ö", output: "w7Y=" }, + { input: "»»", output: "wrvCuw==" }, + ]; + cases.forEach((c) => { + const utfStr = c.input; + const str = Utils.fromUtf8ToB64(utfStr); + expect(str).toBe(c.output); + }); + }); + }); + + describe("fromB64ToUtf8(...)", () => { + const originalIsNode = Utils.isNode; + + afterEach(() => { + Utils.isNode = originalIsNode; + }); + + runInBothEnvironments("should handle empty string", () => { + const str = Utils.fromB64ToUtf8(""); + expect(str).toBe(""); + }); + + runInBothEnvironments("should convert a normal b64 string", () => { + const str = Utils.fromB64ToUtf8(b64HelloWorldString); + expect(str).toBe(asciiHelloWorld); + }); + + runInBothEnvironments("should handle various special characters", () => { + const cases = [ + { input: "wrs=", output: "»" }, + { input: "wqY=", output: "¦" }, + { input: "wqM=", output: "£" }, + { input: "w6k=", output: "é" }, + { input: "w7Y=", output: "ö" }, + { input: "wrvCuw==", output: "»»" }, + ]; + + cases.forEach((c) => { + const b64Str = c.input; + const str = Utils.fromB64ToUtf8(b64Str); + expect(str).toBe(c.output); + }); + }); + }); }); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index ef65d2130a0..203a04851c5 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -233,7 +233,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(utfStr, "utf8").toString("base64"); } else { - return decodeURIComponent(escape(Utils.global.btoa(utfStr))); + return BufferLib.from(utfStr, "utf8").toString("base64"); } } @@ -245,7 +245,7 @@ export class Utils { if (Utils.isNode) { return Buffer.from(b64Str, "base64").toString("utf8"); } else { - return decodeURIComponent(escape(Utils.global.atob(b64Str))); + return BufferLib.from(b64Str, "base64").toString("utf8"); } } From c798e1f10d365d584eb75fda40fc93a8115e10ad Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:21:01 +0100 Subject: [PATCH 23/30] [PM-18250]Do not default account credit amount (#13719) * Set the default value to zero * Remove the 20 dollar for org --- .../web/src/app/billing/shared/add-credit-dialog.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts index bfe811f1aa7..45dab542ce8 100644 --- a/apps/web/src/app/billing/shared/add-credit-dialog.component.ts +++ b/apps/web/src/app/billing/shared/add-credit-dialog.component.ts @@ -76,7 +76,7 @@ export class AddCreditDialogComponent implements OnInit { async ngOnInit() { if (this.organizationId != null) { if (this.creditAmount == null) { - this.creditAmount = "20.00"; + this.creditAmount = "0.00"; } this.ppButtonCustomField = "organization_id:" + this.organizationId; const userId = await firstValueFrom( @@ -93,7 +93,7 @@ export class AddCreditDialogComponent implements OnInit { } } else { if (this.creditAmount == null) { - this.creditAmount = "10.00"; + this.creditAmount = "0.00"; } const [userId, email] = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), From d4dd8d096b607447a932163119d0fc3fe45f7906 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:35:42 -0400 Subject: [PATCH 24/30] chore(builds): [PM-20431] Add bit-web to build-web workflow paths * Add bit-web to build-web workflow paths. * Updated to also include bit-common --- .github/workflows/build-cli.yml | 6 ++++-- .github/workflows/build-web.yml | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e113d5c253b..e89ca59a297 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -12,12 +12,13 @@ on: - 'cf-pages' paths: - 'apps/cli/**' + - 'bitwarden_license/bit-cli/**' + - 'bitwarden_license/bit-common/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' push: branches: - 'main' @@ -25,12 +26,13 @@ on: - 'hotfix-rc-cli' paths: - 'apps/cli/**' + - 'bitwarden_license/bit-cli/**' + - 'bitwarden_license/bit-common/**' - 'libs/**' - '*' - '!*.md' - '!*.txt' - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' workflow_call: inputs: {} workflow_dispatch: diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 3da524702fe..fb429468134 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -12,6 +12,8 @@ on: - 'cf-pages' paths: - 'apps/web/**' + - 'bitwarden_license/bit-common/**' + - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' @@ -24,6 +26,8 @@ on: - 'hotfix-rc-web' paths: - 'apps/web/**' + - 'bitwarden_license/bit-common/**' + - 'bitwarden_license/bit-web/**' - 'libs/**' - '*' - '!*.md' From 5dfa0eb91a014eb3418b0f1ef56a31d55ba06e75 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 19 Apr 2025 16:10:57 +0200 Subject: [PATCH 25/30] [PM-20277] Disable fingerprint on org invite (#14285) --- .../accept-organization.service.spec.ts | 7 ------- .../accept-organization.service.ts | 11 ----------- 2 files changed, 18 deletions(-) diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index cc2d0e371ff..67dbee223d1 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -24,7 +24,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component"; import { I18nService } from "../../core/i18n.service"; import { @@ -200,11 +199,6 @@ describe("AcceptOrganizationInviteService", () => { encryptedString: "encryptedString", } as EncString); - jest.mock("../../admin-console/organizations/manage/organization-trust.component"); - OrganizationTrustComponent.open = jest.fn().mockReturnValue({ - closed: new BehaviorSubject(true), - }); - await globalState.update(() => invite); policyService.getResetPasswordPolicyOptions.mockReturnValue([ @@ -217,7 +211,6 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(OrganizationTrustComponent.open).toHaveBeenCalled(); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( { key: "userKey" }, Utils.fromB64ToArray("publicKey"), diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index 8b5db9f4872..b6a7719c548 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -31,8 +31,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component"; - import { OrganizationInvite } from "./organization-invite"; // We're storing the organization invite for 2 reasons: @@ -189,15 +187,6 @@ export class AcceptOrganizationInviteService { } const publicKey = Utils.fromB64ToArray(response.publicKey); - const dialogRef = OrganizationTrustComponent.open(this.dialogService, { - name: invite.organizationName, - orgId: invite.organizationId, - publicKey, - }); - const result = await firstValueFrom(dialogRef.closed); - if (result !== true) { - throw new Error("Organization not trusted, aborting user key rotation"); - } const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); From 8de42caf0406aac7ebaa9a3e7654cf7a76604d34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Apr 2025 18:07:20 +0200 Subject: [PATCH 26/30] [deps] KM: Update Rust crate rsa to v0.9.8 (#12298) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bernd Schoolmann --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 04bc4887ff7..0884f59e863 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -2455,9 +2455,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 01838359147..dc08c2e5a1b 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -38,7 +38,7 @@ oslog = "=0.2.0" pin-project = "=1.1.8" pkcs8 = "=0.10.2" rand = "=0.8.5" -rsa = "=0.9.6" +rsa = "=0.9.8" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" security-framework = "=3.1.0" From 86b0a6aa35c3b4bfdca4a3441ad14fd3d929c68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 21 Apr 2025 12:21:00 +0200 Subject: [PATCH 27/30] Support for logging from NAPI (#14335) * Support for log to electron console from NAPI * Fix test mock --- apps/desktop/desktop_native/Cargo.lock | 1 + apps/desktop/desktop_native/napi/Cargo.toml | 1 + apps/desktop/desktop_native/napi/index.d.ts | 10 ++++ apps/desktop/desktop_native/napi/src/lib.rs | 58 +++++++++++++++++++ .../services/electron-log.main.service.ts | 24 ++++++++ .../services/electron-log.service.spec.ts | 8 +++ 6 files changed, 102 insertions(+) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0884f59e863..6858011e0cc 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -987,6 +987,7 @@ dependencies = [ "base64", "desktop_core", "hex", + "log", "napi", "napi-build", "napi-derive", diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index d59581a5e2e..669f166e748 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -18,6 +18,7 @@ base64 = { workspace = true } hex = { workspace = true } anyhow = { workspace = true } desktop_core = { path = "../core" } +log = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 6ded4e3db93..952f2571c5d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -185,3 +185,13 @@ export declare namespace crypto { export declare namespace passkey_authenticator { export function register(): void } +export declare namespace logging { + export const enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4 + } + export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 8cbc526487e..37796ef6f59 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -807,3 +807,61 @@ pub mod passkey_authenticator { }) } } + +#[napi] +pub mod logging { + use log::{Level, Metadata, Record}; + use napi::threadsafe_function::{ + ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use std::sync::OnceLock; + struct JsLogger(OnceLock>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From for LogLevel { + fn from(level: Level) -> Self { + match level { + Level::Trace => LogLevel::Trace, + Level::Debug => LogLevel::Debug, + Level::Info => LogLevel::Info, + Level::Warn => LogLevel::Warn, + Level::Error => LogLevel::Error, + } + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) { + let _ = JS_LOGGER.0.set(js_log_fn); + let _ = log::set_logger(&JS_LOGGER); + log::set_max_level(log::LevelFilter::Debug); + } + + impl log::Log for JsLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + let Some(logger) = self.0.get() else { + return; + }; + let msg = (record.level().into(), record.args().to_string()); + let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking); + } + + fn flush(&self) {} + } +} diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index d7100b54825..947f4449271 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -7,6 +7,7 @@ import log from "electron-log/main"; import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum"; import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { logging } from "@bitwarden/desktop-napi"; import { isDev } from "../../utils"; @@ -30,6 +31,29 @@ export class ElectronLogMainService extends BaseLogService { ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { this.write(level, message, ...optionalParams); }); + + logging.initNapiLog((error, level, message) => this.writeNapiLog(level, message)); + } + + private writeNapiLog(level: logging.LogLevel, message: string) { + let levelType: LogLevelType; + + switch (level) { + case logging.LogLevel.Debug: + levelType = LogLevelType.Debug; + break; + case logging.LogLevel.Warn: + levelType = LogLevelType.Warning; + break; + case logging.LogLevel.Error: + levelType = LogLevelType.Error; + break; + default: + levelType = LogLevelType.Info; + break; + } + + this.write(levelType, "[NAPI] " + message); } write(level: LogLevelType, message?: any, ...optionalParams: any[]) { diff --git a/apps/desktop/src/platform/services/electron-log.service.spec.ts b/apps/desktop/src/platform/services/electron-log.service.spec.ts index 918508977fd..db3093e08e2 100644 --- a/apps/desktop/src/platform/services/electron-log.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-log.service.spec.ts @@ -5,6 +5,14 @@ jest.mock("electron", () => ({ ipcMain: { handle: jest.fn(), on: jest.fn() }, })); +jest.mock("@bitwarden/desktop-napi", () => { + return { + logging: { + initNapiLog: jest.fn(), + }, + }; +}); + describe("ElectronLogMainService", () => { it("sets dev based on electron method", () => { process.env.ELECTRON_IS_DEV = "1"; From 201bdf752b3ff57dba3581d13c026d55db12cdb5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 21 Apr 2025 14:14:13 +0200 Subject: [PATCH 28/30] [PM-19728] Device bulk get keys during key rotation (#14216) * Add support for device list endpoint keys during key rotation * Update libs/common/src/auth/abstractions/devices/responses/device.response.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../auth/abstractions/devices/responses/device.response.ts | 5 +++++ .../services/device-trust.service.implementation.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts index 84a2fb03c28..6b7f17f65ce 100644 --- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts @@ -15,6 +15,9 @@ export class DeviceResponse extends BaseResponse { creationDate: string; revisionDate: string; isTrusted: boolean; + encryptedUserKey: string | null; + encryptedPublicKey: string | null; + devicePendingAuthRequest: DevicePendingAuthRequest | null; constructor(response: any) { @@ -27,6 +30,8 @@ export class DeviceResponse extends BaseResponse { this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); this.isTrusted = this.getResponseProperty("IsTrusted"); + this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey"); + this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey"); this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest"); } } diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index a2211753f4e..c82efa0c571 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -209,9 +209,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { devices.data .filter((device) => device.isTrusted) .map(async (device) => { - const deviceWithKeys = await this.devicesApiService.getDeviceKeys(device.identifier); const publicKey = await this.encryptService.decryptToBytes( - deviceWithKeys.encryptedPublicKey, + new EncString(device.encryptedPublicKey), oldUserKey, ); From 83d7ea6aa3e989faef02e3885d4c40693e72a9ac Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 21 Apr 2025 09:52:53 -0400 Subject: [PATCH 29/30] [PM-20334] Remove Bindgen from Windows Plugin Authenticator (#14328) * PM-20334: Draft work removing bindgen * PM-20334: Remove comments and address clippy concerns * PM-20334: Edit wpa readme and remove .hpp header file --- apps/desktop/desktop_native/Cargo.lock | 66 ----- .../windows_plugin_authenticator/Cargo.toml | 3 - .../windows_plugin_authenticator/README.md | 20 +- .../windows_plugin_authenticator/build.rs | 27 -- .../pluginauthenticator.hpp | 231 ------------------ .../windows_plugin_authenticator/src/lib.rs | 61 +++-- .../windows_plugin_authenticator/src/pa.rs | 15 -- 7 files changed, 49 insertions(+), 374 deletions(-) delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/build.rs delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp delete mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 6858011e0cc..c786264a563 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -410,26 +410,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - [[package]] name = "bitflags" version = "2.8.0" @@ -573,15 +553,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -622,17 +593,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.5.31" @@ -1493,15 +1453,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.14" @@ -2303,16 +2254,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2491,12 +2432,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -3736,7 +3671,6 @@ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" name = "windows_plugin_authenticator" version = "0.0.0" dependencies = [ - "bindgen", "hex", "windows 0.61.1", "windows-core 0.61.0", diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index 72a8505389e..18abb86d057 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -5,9 +5,6 @@ edition = { workspace = true } license = { workspace = true } publish = { workspace = true } -[target.'cfg(target_os = "windows")'.build-dependencies] -bindgen = { workspace = true } - [target.'cfg(windows)'.dependencies] windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader" ] } windows-core = { workspace = true } diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/README.md b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md index 6dc72ceed46..4802c5d4243 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/README.md +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/README.md @@ -2,22 +2,6 @@ This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's. +This crate is very much a WIP and is not ready for internal use. + You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn). - -## Building - -To build this crate, set the following environment variables: - -- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang)) - -### Bash Example - -``` -export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' -``` - -### PowerShell Example - -``` -$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin' -``` diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs deleted file mode 100644 index 4843145bac0..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -fn main() { - #[cfg(target_os = "windows")] - windows(); -} - -#[cfg(target_os = "windows")] -fn windows() { - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); - - let bindings = bindgen::Builder::default() - .header("pluginauthenticator.hpp") - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .allowlist_type("DWORD") - .allowlist_type("PBYTE") - .allowlist_type("EXPERIMENTAL.*") - .allowlist_function(".*EXPERIMENTAL.*") - .allowlist_function("WebAuthNGetApiVersionNumber") - .generate() - .expect("Unable to generate bindings."); - - bindings - .write_to_file(format!( - "{}\\windows_plugin_authenticator_bindings.rs", - out_dir - )) - .expect("Couldn't write bindings."); -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp b/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp deleted file mode 100644 index c800266a3e6..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/pluginauthenticator.hpp +++ /dev/null @@ -1,231 +0,0 @@ -/* - Bitwarden's pluginauthenticator.hpp - - Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h - - This is a C++ header file, so the extension has been manually - changed from `.h` to `.hpp`, so bindgen will automatically - generate the correct C++ bindings. - - More Info: https://rust-lang.github.io/rust-bindgen/cpp.html -*/ - -/* this ALWAYS GENERATED file contains the definitions for the interfaces */ - -/* File created by MIDL compiler version 8.01.0628 */ -/* @@MIDL_FILE_HEADING( ) */ - -/* verify that the version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCNDR_H_VERSION__ -#define __REQUIRED_RPCNDR_H_VERSION__ 501 -#endif - -/* verify that the version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCSAL_H_VERSION__ -#define __REQUIRED_RPCSAL_H_VERSION__ 100 -#endif - -#include "rpc.h" -#include "rpcndr.h" - -#ifndef __RPCNDR_H_VERSION__ -#error this stub requires an updated version of -#endif /* __RPCNDR_H_VERSION__ */ - -#ifndef COM_NO_WINDOWS_H -#include "windows.h" -#include "ole2.h" -#endif /*COM_NO_WINDOWS_H*/ - -#ifndef __pluginauthenticator_h__ -#define __pluginauthenticator_h__ - -#if defined(_MSC_VER) && (_MSC_VER >= 1020) -#pragma once -#endif - -#ifndef DECLSPEC_XFGVIRT -#if defined(_CONTROL_FLOW_GUARD_XFG) -#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) -#else -#define DECLSPEC_XFGVIRT(base, func) -#endif -#endif - -/* Forward Declarations */ - -#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ -#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ -typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator; - -#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */ - -/* header files for imported files */ -#include "oaidl.h" -#include "webauthn.h" - -#ifdef __cplusplus -extern "C"{ -#endif - -/* interface __MIDL_itf_pluginauthenticator_0000_0000 */ -/* [local] */ - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST - { - HWND hWnd; - GUID transactionId; - DWORD cbRequestSignature; - /* [size_is] */ byte *pbRequestSignature; - DWORD cbEncodedRequest; - /* [size_is] */ byte *pbEncodedRequest; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE - { - DWORD cbEncodedResponse; - /* [size_is] */ byte *pbEncodedResponse; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST - { - GUID transactionId; - DWORD cbRequestSignature; - /* [size_is] */ byte *pbRequestSignature; - } EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; - -extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec; - -#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ -#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ - -/* interface EXPERIMENTAL_IPluginAuthenticator */ -/* [unique][version][uuid][object] */ - -EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator; - -#if defined(__cplusplus) && !defined(CINTERFACE) - - MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998") - EXPERIMENTAL_IPluginAuthenticator : public IUnknown - { - public: - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; - - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0; - - virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation( - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0; - - }; - -#else /* C style interface */ - - typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl - { - BEGIN_INTERFACE - - DECLSPEC_XFGVIRT(IUnknown, QueryInterface) - HRESULT ( STDMETHODCALLTYPE *QueryInterface )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in REFIID riid, - /* [annotation][iid_is][out] */ - _COM_Outptr_ void **ppvObject); - - DECLSPEC_XFGVIRT(IUnknown, AddRef) - ULONG ( STDMETHODCALLTYPE *AddRef )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); - - DECLSPEC_XFGVIRT(IUnknown, Release) - ULONG ( STDMETHODCALLTYPE *Release )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request, - /* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response); - - DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation) - HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )( - __RPC__in EXPERIMENTAL_IPluginAuthenticator * This, - /* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request); - - END_INTERFACE - } EXPERIMENTAL_IPluginAuthenticatorVtbl; - - interface EXPERIMENTAL_IPluginAuthenticator - { - CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl; - }; - -#ifdef COBJMACROS - - -#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \ - ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) - -#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \ - ( (This)->lpVtbl -> AddRef(This) ) - -#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \ - ( (This)->lpVtbl -> Release(This) ) - - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) ) - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) ) - -#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \ - ( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) ) - -#endif /* COBJMACROS */ - -#endif /* C style interface */ - -#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */ - -/* Additional Prototypes for ALL interfaces */ - -unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); -void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * ); - -unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * ); -unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * ); -void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * ); - -/* end of Additional Prototypes */ - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index fe2e35df2f8..21257068bd0 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -2,15 +2,6 @@ #![allow(non_snake_case)] #![allow(non_camel_case_types)] -mod pa; - -use pa::{ - DWORD, EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, - EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, - EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, PBYTE, -}; use std::ffi::c_uchar; use std::ptr; use windows::Win32::Foundation::*; @@ -23,11 +14,53 @@ const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator"; const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; const RPID: &str = "bitwarden.com"; -/// Returns the current Windows WebAuthN version. -pub fn get_version_number() -> u32 { - unsafe { pa::WebAuthNGetApiVersionNumber() } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { + pub transactionId: GUID, + pub cbRequestSignature: Dword, + pub pbRequestSignature: *mut byte, } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST { + pub hWnd: HWND, + pub transactionId: GUID, + pub cbRequestSignature: Dword, + pub pbRequestSignature: *mut byte, + pub cbEncodedRequest: Dword, + pub pbEncodedRequest: *mut byte, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { + pub cbOpSignPubKey: Dword, + pub pbOpSignPubKey: PByte, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE { + pub cbEncodedResponse: Dword, + pub pbEncodedResponse: *mut byte, +} + +type Dword = u32; +type Byte = u8; +type byte = u8; +pub type PByte = *mut Byte; + +type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST = + *const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; +pub type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST = + *const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; +pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE = + *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; +pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = + *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; + /// Handles initialization and registration for the Bitwarden desktop app as a /// plugin authenticator with Windows. /// For now, also adds the authenticator @@ -123,9 +156,9 @@ fn add_authenticator() -> std::result::Result<(), String> { pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(), }; - let plugin_signing_public_key_byte_count: DWORD = 0; + let plugin_signing_public_key_byte_count: Dword = 0; let mut plugin_signing_public_key: c_uchar = 0; - let plugin_signing_public_key_ptr: PBYTE = &mut plugin_signing_public_key; + let plugin_signing_public_key_ptr: PByte = &mut plugin_signing_public_key; let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { cbOpSignPubKey: plugin_signing_public_key_byte_count, diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs deleted file mode 100644 index 7c93399594d..00000000000 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pa.rs +++ /dev/null @@ -1,15 +0,0 @@ -/* - The 'pa' (plugin authenticator) module will contain the generated - bindgen code. - - The attributes below will suppress warnings from the generated code. -*/ - -#![cfg(target_os = "windows")] -#![allow(clippy::all)] -#![allow(warnings)] - -include!(concat!( - env!("OUT_DIR"), - "/windows_plugin_authenticator_bindings.rs" -)); From 1b6f75806d17c4f2733d15789afa6ec708d49e9a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 21 Apr 2025 14:22:41 +0000 Subject: [PATCH 30/30] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index b311b837e78..9ed3c807c11 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.3.2", + "version": "2025.4.0", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 35578fb8321..fca62568048 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.3.2", + "version": "2025.4.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 556a73ac15b..47e0cc465ef 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.3.2", + "version": "2025.4.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 04fe4290d31..1bf6a1d41a1 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.3.0", + "version": "2025.4.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ae34deee5b8..9f442da47a1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.4.1", + "version": "2025.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index a720dff7257..f6449bd9626 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index e288f5f5a79..45a6f6b90af 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.4.1", + "version": "2025.4.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 1f4ae9c29cf..e65848602e9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.4.0", + "version": "2025.4.1", "scripts": { "build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", "build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 65f0c87721a..3e16fd7ba68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,11 +191,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.3.2" + "version": "2025.4.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.3.0", + "version": "2025.4.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -231,7 +231,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.4.1", + "version": "2025.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -245,7 +245,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.4.0" + "version": "2025.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console",