Skip to content

Commit

Permalink
Merge branch 'main' into km/pm-10741/refactor-biometrics
Browse files Browse the repository at this point in the history
  • Loading branch information
quexten committed Jan 7, 2025
2 parents 3b35ad6 + c0d3fe1 commit 6f14f8e
Show file tree
Hide file tree
Showing 85 changed files with 682 additions and 299 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ apps/web/src/app/key-management @bitwarden/team-key-management-dev
apps/browser/src/key-management @bitwarden/team-key-management-dev
apps/cli/src/key-management @bitwarden/team-key-management-dev
libs/key-management @bitwarden/team-key-management-dev
libs/common/src/key-management @bitwarden/team-key-management-dev

apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev
apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
Expand All @@ -19,6 +21,7 @@ describe("context-menu", () => {
let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let accountService: MockProxy<AccountService>;

let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>;
let createSpy: jest.SpyInstance<
Expand All @@ -34,6 +37,7 @@ describe("context-menu", () => {
i18nService = mock();
logService = mock();
billingAccountProfileStateService = mock();
accountService = mock();

removeAllSpy = jest
.spyOn(chrome.contextMenus, "removeAll")
Expand All @@ -53,8 +57,15 @@ describe("context-menu", () => {
i18nService,
logService,
billingAccountProfileStateService,
accountService,
);
autofillSettingsService.enableContextMenu$ = of(true);
accountService.activeAccount$ = of({
id: "userId" as UserId,
email: "",
emailVerified: false,
name: undefined,
});
});

afterEach(() => jest.resetAllMocks());
Expand All @@ -69,15 +80,15 @@ describe("context-menu", () => {
});

it("has menu enabled, but does not have premium", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));

const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy();
expect(createSpy).toHaveBeenCalledTimes(10);
});

it("has menu enabled and has premium", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));

const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy();
Expand Down Expand Up @@ -131,16 +142,15 @@ describe("context-menu", () => {
});

it("create entry for each cipher piece", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));

await sut.loadOptions("TEST_TITLE", "1", createCipher());

// One for autofill, copy username, copy password, and copy totp code
expect(createSpy).toHaveBeenCalledTimes(4);
});

it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));

await sut.loadOptions("TEST_TITLE", "NOOP");

Expand Down
15 changes: 10 additions & 5 deletions apps/browser/src/autofill/browser/main-context-menu-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
AUTOFILL_CARD_ID,
AUTOFILL_ID,
Expand Down Expand Up @@ -149,6 +150,7 @@ export class MainContextMenuHandler {
private i18nService: I18nService,
private logService: LogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private accountService: AccountService,
) {}

/**
Expand All @@ -168,11 +170,13 @@ export class MainContextMenuHandler {
this.initRunning = true;

try {
const account = await firstValueFrom(this.accountService.activeAccount$);
const hasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
);

for (const options of this.initContextMenuItems) {
if (
options.checkPremiumAccess &&
!(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))
) {
if (options.checkPremiumAccess && !hasPremium) {
continue;
}

Expand Down Expand Up @@ -267,8 +271,9 @@ export class MainContextMenuHandler {
await createChildItem(COPY_USERNAME_ID);
}

const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
);
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
await createChildItem(COPY_VERIFICATION_CODE_ID);
Expand Down
22 changes: 16 additions & 6 deletions apps/browser/src/autofill/services/autofill.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mock, mockReset, MockProxy } from "jest-mock-extended";
import { mock, MockProxy, mockReset } from "jest-mock-extended";
import { BehaviorSubject, of, Subject } from "rxjs";

import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
Expand Down Expand Up @@ -730,7 +730,9 @@ describe("AutofillService", () => {

it("throws an error if an autofill did not occur for any of the passed pages", async () => {
autofillOptions.tab.url = "https://a-different-url.com";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));

try {
await autofillService.doAutoFill(autofillOptions);
Expand Down Expand Up @@ -912,7 +914,9 @@ describe("AutofillService", () => {
it("returns a TOTP value", async () => {
const totpCode = "123456";
autofillOptions.cipher.login.totp = "totp";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);
jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode);

Expand All @@ -925,7 +929,9 @@ describe("AutofillService", () => {

it("does not return a TOTP value if the user does not have premium features", async () => {
autofillOptions.cipher.login.totp = "totp";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(false));
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);

const autofillResult = await autofillService.doAutoFill(autofillOptions);
Expand Down Expand Up @@ -959,7 +965,9 @@ describe("AutofillService", () => {
it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => {
autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = false;
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(false));

const autofillResult = await autofillService.doAutoFill(autofillOptions);

Expand All @@ -969,7 +977,9 @@ describe("AutofillService", () => {
it("returns a null value if the user has disabled `auto TOTP copy`", async () => {
autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = true;
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest
.spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$")
.mockImplementation(() => of(true));
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false);
jest.spyOn(totpService, "getCode");

Expand Down
3 changes: 2 additions & 1 deletion apps/browser/src/autofill/services/autofill.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,9 @@ export default class AutofillService implements AutofillServiceInterface {

let totp: string | null = null;

const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id),
);
const defaultUriMatch = await this.getDefaultUriMatchStrategy();

Expand Down
3 changes: 3 additions & 0 deletions apps/browser/src/background/main.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,8 @@ export default class MainBackground {

this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.stateProvider,
this.platformUtilsService,
this.apiService,
);

this.ssoLoginService = new SsoLoginService(this.stateProvider);
Expand Down Expand Up @@ -1229,6 +1231,7 @@ export default class MainBackground {
this.i18nService,
this.logService,
this.billingAccountProfileStateService,
this.accountService,
);

this.cipherContextMenuHandler = new CipherContextMenuHandler(
Expand Down
5 changes: 4 additions & 1 deletion apps/browser/src/background/runtime.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,11 @@ export default class RuntimeBackground {
return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification);
}
case "getUserPremiumStatus": {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const result = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
Expand Down Expand Up @@ -56,6 +57,7 @@ export class PremiumV2Component extends BasePremiumComponent {
dialogService: DialogService,
environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
) {
super(
i18nService,
Expand All @@ -66,6 +68,7 @@ export class PremiumV2Component extends BasePremiumComponent {
dialogService,
environmentService,
billingAccountProfileStateService,
accountService,
);

// Support old price string. Can be removed in future once all translations are properly updated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";

import { UnlockOptions } from "@bitwarden/auth/angular";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
Expand All @@ -11,8 +10,9 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService, BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
import { UnlockOptions } from "@bitwarden/key-management/angular";

import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";

import { ExtensionLockComponentService } from "./extension-lock-component.service";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import { inject } from "@angular/core";
import { combineLatest, defer, map, Observable } from "rxjs";

import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular";

import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors";
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service";

export class ExtensionLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
Expand Down
2 changes: 1 addition & 1 deletion apps/browser/src/popup/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
LoginComponent,
LoginSecondaryContentComponent,
LockIcon,
LockComponent,
LoginViaAuthRequestComponent,
PasswordHintComponent,
RegistrationFinishComponent,
Expand All @@ -43,6 +42,7 @@ import {
TwoFactorTimeoutIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management/angular";
import {
NewDeviceVerificationNoticePageOneComponent,
NewDeviceVerificationNoticePageTwoComponent,
Expand Down
4 changes: 2 additions & 2 deletions apps/browser/src/popup/services/services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
import {
AnonLayoutWrapperDataService,
LoginComponentService,
LockComponentService,
SsoComponentService,
LoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
Expand Down Expand Up @@ -115,6 +114,7 @@ import {
BiometricsService,
DefaultKeyService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management/angular";
import { PasswordRepromptService } from "@bitwarden/vault";

import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
Expand All @@ -126,6 +126,7 @@ import { AutofillService as AutofillServiceAbstraction } from "../../autofill/se
import AutofillService from "../../autofill/services/autofill.service";
import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service";
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
/* eslint-disable no-restricted-imports */
Expand All @@ -149,7 +150,6 @@ import { BrowserStorageServiceProvider } from "../../platform/storage/browser-st
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service";
Expand Down
12 changes: 11 additions & 1 deletion apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,17 @@ describe("SendV2Component", () => {
CurrentAccountComponent,
],
providers: [
{ provide: AccountService, useValue: mock<AccountService>() },
{
provide: AccountService,
useValue: {
activeAccount$: of({
id: "123",
email: "[email protected]",
emailVerified: true,
name: "Test User",
}),
},
},
{ provide: AuthService, useValue: mock<AuthService>() },
{ provide: AvatarService, useValue: mock<AvatarService>() },
{
Expand Down
Loading

0 comments on commit 6f14f8e

Please sign in to comment.