1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 10:33:57 +00:00

[PM-11442] Emergency Cipher Viewing (#12054)

* force viewOnly to be true for emergency access

* add input to hide password history, applicable when the view is used from emergency view

* add extension refresh version of the emergency view dialog

* allow emergency access to view password history

- `ViewPasswordHistoryService` accepts a cipher id or CipherView. When a CipherView is included, the history component no longer has to fetch the cipher.

* remove unused comments

* Add fixme comment for removing non-extension refresh code

* refactor password history component to accept a full cipher view

- Remove the option to pass in only an id
This commit is contained in:
Nick Krantz
2024-12-19 09:42:37 -06:00
committed by GitHub
parent 1d04a0a399
commit 0f3803ac91
18 changed files with 337 additions and 83 deletions

View File

@@ -1,8 +1,8 @@
<popup-page>
<popup-page [loading]="!cipher">
<popup-header slot="header" pageTitle="{{ 'passwordHistory' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<vault-password-history-view *ngIf="cipherId" [cipherId]="cipherId" />
<vault-password-history-view *ngIf="cipher" [cipher]="cipher" />
</popup-page>

View File

@@ -1,27 +1,40 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended";
import { Subject } from "rxjs";
import { BehaviorSubject, Subject } from "rxjs";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
import { PasswordHistoryV2Component } from "./vault-password-history-v2.component";
describe("PasswordHistoryV2Component", () => {
let component: PasswordHistoryV2Component;
let fixture: ComponentFixture<PasswordHistoryV2Component>;
const params$ = new Subject();
const mockCipherView = {
id: "111-222-333",
name: "cipher one",
} as CipherView;
const mockCipher = {
decrypt: jest.fn().mockResolvedValue(mockCipherView),
} as unknown as Cipher;
const back = jest.fn().mockResolvedValue(undefined);
const getCipher = jest.fn().mockResolvedValue(mockCipher);
beforeEach(async () => {
back.mockClear();
getCipher.mockClear();
await TestBed.configureTestingModule({
imports: [PasswordHistoryV2Component],
@@ -29,8 +42,13 @@ describe("PasswordHistoryV2Component", () => {
{ provide: WINDOW, useValue: window },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: AccountService, useValue: mock<AccountService>() },
{ provide: CipherService, useValue: mock<CipherService>({ get: getCipher }) },
{
provide: AccountService,
useValue: mock<AccountService>({
activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account),
}),
},
{ provide: PopupRouterCacheService, useValue: { back } },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
@@ -38,19 +56,21 @@ describe("PasswordHistoryV2Component", () => {
}).compileComponents();
fixture = TestBed.createComponent(PasswordHistoryV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("sets the cipherId from the params", () => {
params$.next({ cipherId: "444-33-33-1111" });
it("loads the cipher from params the cipherId from the params", fakeAsync(() => {
params$.next({ cipherId: mockCipherView.id });
expect(component["cipherId"]).toBe("444-33-33-1111");
});
tick(100);
expect(getCipher).toHaveBeenCalledWith(mockCipherView.id);
}));
it("navigates back when a cipherId is not in the params", () => {
params$.next({});
expect(back).toHaveBeenCalledTimes(1);
expect(getCipher).not.toHaveBeenCalled();
});
});

View File

@@ -3,10 +3,14 @@
import { NgIf } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { firstValueFrom } from "rxjs";
import { first, map } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherId } from "@bitwarden/common/types/guid";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
@@ -28,18 +32,20 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
],
})
export class PasswordHistoryV2Component implements OnInit {
protected cipherId: CipherId;
protected cipher: CipherView;
constructor(
private browserRouterHistory: PopupRouterCacheService,
private route: ActivatedRoute,
private cipherService: CipherService,
private accountService: AccountService,
) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.pipe(first()).subscribe((params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
void this.loadCipher(params.cipherId);
} else {
this.close();
}
@@ -49,4 +55,22 @@ export class PasswordHistoryV2Component implements OnInit {
close() {
void this.browserRouterHistory.back();
}
/** Load the cipher based on the given Id */
private async loadCipher(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
const activeAccount = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
);
if (!activeAccount?.id) {
throw new Error("Active account is not available.");
}
const activeUserId = activeAccount.id as UserId;
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}
}

View File

@@ -2,6 +2,8 @@ import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service";
describe("BrowserViewPasswordHistoryService", () => {
@@ -19,9 +21,9 @@ describe("BrowserViewPasswordHistoryService", () => {
describe("viewPasswordHistory", () => {
it("navigates to the password history screen", async () => {
await service.viewPasswordHistory("test");
await service.viewPasswordHistory({ id: "cipher-id" } as CipherView);
expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], {
queryParams: { cipherId: "test" },
queryParams: { cipherId: "cipher-id" },
});
});
});

View File

@@ -4,6 +4,7 @@ import { inject } from "@angular/core";
import { Router } from "@angular/router";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
/**
* This class handles the premium upgrade process for the browser extension.
@@ -14,7 +15,9 @@ export class BrowserViewPasswordHistoryService implements ViewPasswordHistorySer
/**
* Navigates to the password history screen.
*/
async viewPasswordHistory(cipherId: string) {
await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } });
async viewPasswordHistory(cipher: CipherView) {
await this.router.navigate(["/cipher-password-history"], {
queryParams: { cipherId: cipher.id },
});
}
}