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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user