From e0df1ecf0c4b69ebebb34d0a07d54a1f87097918 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 17 Apr 2025 17:33:13 +0200 Subject: [PATCH 01/11] [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 02/11] 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 03/11] 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 04/11] [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 05/11] 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 06/11] [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 07/11] 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 08/11] 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 09/11] 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 10/11] [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 11/11] [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"); } }