1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-21 16:18:28 +01:00

[PM-1345] Bugfix - Items with "Re-prompt Masterpassword" fail silently (#5621)

* upon action outside of the extenstion requiring password reprompt, open new tab with reprompt

* allow popup view component to load with default action and send context menu actions on reprompt ciphers to password reprompt

* open password reprompt in new window instead of new tab

* update test and linting

* Apply suggestions from code review

Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>

* add support for getTab in Manifest V2

* remove unneeded loadAction check

* allow auto-fill button in popout window

* add LoadAction type

* update code to use new BrowserPopoutWindowService

* access queryParams with subscribe

* do not dismiss window if no loadAction was specified

* rehide autofill option for non-single-action popout windows

---------

Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>
This commit is contained in:
Jonathan Prusik 2023-08-15 17:27:59 -04:00 committed by GitHub
parent 41bf1247ef
commit d95f1163bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 191 additions and 31 deletions

View File

@ -96,7 +96,7 @@ describe("CipherContextMenuHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(1);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
"Test Cipher (Test Username)",

View File

@ -4,7 +4,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -177,11 +176,7 @@ export class CipherContextMenuHandler {
}
private async updateForCipher(url: string, cipher: CipherView) {
if (
cipher == null ||
cipher.type !== CipherType.Login ||
cipher.reprompt !== CipherRepromptType.None
) {
if (cipher == null || cipher.type !== CipherType.Login) {
return;
}

View File

@ -203,17 +203,45 @@ export class ContextMenuClickedHandler {
if (tab == null) {
return;
}
if (cipher.reprompt !== CipherRepromptType.None) {
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
cipherId: cipher.id,
action: AUTOFILL_ID,
});
} else {
await this.autofillAction(tab, cipher);
}
break;
case COPY_USERNAME_ID:
this.copyToClipboard({ text: cipher.login.username, tab: tab });
break;
case COPY_PASSWORD_ID:
if (cipher.reprompt !== CipherRepromptType.None) {
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
cipherId: cipher.id,
action: COPY_PASSWORD_ID,
});
} else {
this.copyToClipboard({ text: cipher.login.password, tab: tab });
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
}
break;
case COPY_VERIFICATIONCODE_ID:
this.copyToClipboard({ text: await this.totpService.getCode(cipher.login.totp), tab: tab });
if (cipher.reprompt !== CipherRepromptType.None) {
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
cipherId: cipher.id,
action: COPY_VERIFICATIONCODE_ID,
});
} else {
this.copyToClipboard({
text: await this.totpService.getCode(cipher.login.totp),
tab: tab,
});
}
break;
}
}

View File

@ -28,6 +28,7 @@ window.addEventListener(
const forwardCommands = [
"promptForLogin",
"passwordReprompt",
"addToLockedVaultPendingNotifications",
"unlockCompleted",
"addedCipher",

View File

@ -234,7 +234,16 @@ export default class AutofillService implements AutofillServiceInterface {
}
}
if (cipher == null || cipher.reprompt !== CipherRepromptType.None) {
if (cipher == null) {
return null;
}
if (cipher.reprompt !== CipherRepromptType.None) {
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
cipherId: cipher.id,
action: "autofill",
});
return null;
}

View File

@ -61,6 +61,8 @@ export default class RuntimeBackground {
}
async processMessage(msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) {
const cipherId = msg.data?.cipherId;
switch (msg.command) {
case "loggedIn":
case "unlocked": {
@ -68,7 +70,7 @@ export default class RuntimeBackground {
if (this.lockedVaultPendingNotifications?.length > 0) {
item = this.lockedVaultPendingNotifications.pop();
await this.browserPopoutWindowService.closeLoginPrompt();
await this.browserPopoutWindowService.closeUnlockPrompt();
}
await this.main.refreshBadge();
@ -108,13 +110,22 @@ export default class RuntimeBackground {
break;
case "promptForLogin":
case "bgReopenPromptForLogin":
await this.browserPopoutWindowService.openLoginPrompt(sender.tab?.windowId);
await this.browserPopoutWindowService.openUnlockPrompt(sender.tab?.windowId);
break;
case "passwordReprompt":
if (cipherId) {
await this.browserPopoutWindowService.openPasswordRepromptPrompt(sender.tab?.windowId, {
cipherId: cipherId,
senderTabId: sender.tab.id,
action: msg.data?.action,
});
}
break;
case "openAddEditCipher": {
const addEditCipherUrl =
msg.data?.cipherId == null
cipherId == null
? "popup/index.html#/edit-cipher"
: "popup/index.html#/edit-cipher?cipherId=" + msg.data.cipherId;
: "popup/index.html#/edit-cipher?cipherId=" + cipherId;
BrowserApi.openBitwardenExtensionTab(addEditCipherUrl, true);
break;

View File

@ -42,13 +42,22 @@ export class BrowserApi {
});
}
static async getTab(tabId: number) {
if (tabId == null) {
static async getTab(tabId: number): Promise<chrome.tabs.Tab> | null {
if (!tabId) {
return null;
}
if (BrowserApi.manifestVersion === 3) {
return await chrome.tabs.get(tabId);
}
return new Promise((resolve) =>
chrome.tabs.get(tabId, (tab) => {
resolve(tab);
})
);
}
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
return await BrowserApi.tabsQueryFirst({
active: true,

View File

@ -1,6 +1,15 @@
interface BrowserPopoutWindowService {
openLoginPrompt(senderWindowId: number): Promise<void>;
closeLoginPrompt(): Promise<void>;
openUnlockPrompt(senderWindowId: number): Promise<void>;
closeUnlockPrompt(): Promise<void>;
openPasswordRepromptPrompt(
senderWindowId: number,
promptData: {
action: string;
cipherId: string;
senderTabId: number;
}
): Promise<void>;
closePasswordRepromptPrompt(): Promise<void>;
}
export { BrowserPopoutWindowService };

View File

@ -11,20 +11,48 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
height: 800,
};
async openLoginPrompt(senderWindowId: number) {
await this.closeLoginPrompt();
await this.openPopoutWindow(
async openUnlockPrompt(senderWindowId: number) {
await this.closeUnlockPrompt();
await this.openSingleActionPopout(
senderWindowId,
"popup/index.html?uilocation=popout",
"loginPrompt"
"unlockPrompt"
);
}
async closeLoginPrompt() {
await this.closeSingleActionPopout("loginPrompt");
async closeUnlockPrompt() {
await this.closeSingleActionPopout("unlockPrompt");
}
private async openPopoutWindow(
async openPasswordRepromptPrompt(
senderWindowId: number,
{
cipherId,
senderTabId,
action,
}: {
cipherId: string;
senderTabId: number;
action: string;
}
) {
await this.closePasswordRepromptPrompt();
const promptWindowPath =
"popup/index.html#/view-cipher" +
"?uilocation=popout" +
`&cipherId=${cipherId}` +
`&senderTabId=${senderTabId}` +
`&action=${action}`;
await this.openSingleActionPopout(senderWindowId, promptWindowPath, "passwordReprompt");
}
async closePasswordRepromptPrompt() {
await this.closeSingleActionPopout("passwordReprompt");
}
private async openSingleActionPopout(
senderWindowId: number,
popupWindowURL: string,
singleActionPopoutKey: string

View File

@ -555,7 +555,11 @@
class="box-content-row"
appStopClick
(click)="fillCipher()"
*ngIf="cipher.type !== cipherType.SecureNote && !cipher.isDeleted && !inPopout"
*ngIf="
cipher.type !== cipherType.SecureNote &&
!cipher.isDeleted &&
(!this.inPopout || this.loadAction)
"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">

View File

@ -1,6 +1,7 @@
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { first } from "rxjs/operators";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
@ -31,6 +32,17 @@ import { PopupUtilsService } from "../../../../popup/services/popup-utils.servic
const BroadcasterSubscriptionId = "ChildViewComponent";
export const AUTOFILL_ID = "autofill";
export const COPY_USERNAME_ID = "copy-username";
export const COPY_PASSWORD_ID = "copy-password";
export const COPY_VERIFICATIONCODE_ID = "copy-totp";
type LoadAction =
| typeof AUTOFILL_ID
| typeof COPY_USERNAME_ID
| typeof COPY_PASSWORD_ID
| typeof COPY_VERIFICATIONCODE_ID;
@Component({
selector: "app-vault-view",
templateUrl: "view.component.html",
@ -39,10 +51,15 @@ export class ViewComponent extends BaseViewComponent {
showAttachments = true;
pageDetails: any[] = [];
tab: any;
senderTabId?: number;
loadAction?: LoadAction;
uilocation?: "popout" | "popup" | "sidebar" | "tab";
loadPageDetailsTimeout: number;
inPopout = false;
cipherType = CipherType;
private destroy$ = new Subject<void>();
constructor(
cipherService: CipherService,
folderService: FolderService,
@ -93,7 +110,14 @@ export class ViewComponent extends BaseViewComponent {
}
ngOnInit() {
this.inPopout = this.popupUtilsService.inPopout(window);
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => {
this.loadAction = value?.action;
this.senderTabId = parseInt(value?.senderTabId, 10) || undefined;
this.uilocation = value?.uilocation;
});
this.inPopout = this.uilocation === "popout" || this.popupUtilsService.inPopout(window);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
@ -134,6 +158,8 @@ export class ViewComponent extends BaseViewComponent {
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
@ -141,6 +167,27 @@ export class ViewComponent extends BaseViewComponent {
async load() {
await super.load();
await this.loadPageDetails();
switch (this.loadAction) {
case AUTOFILL_ID:
this.fillCipher();
return;
case COPY_USERNAME_ID:
await this.copy(this.cipher.login.username, "username", "Username");
break;
case COPY_PASSWORD_ID:
await this.copy(this.cipher.login.password, "password", "Password");
break;
case COPY_VERIFICATIONCODE_ID:
await this.copy(this.cipher.login.totp, "verificationCodeTotp", "TOTP");
break;
default:
break;
}
if (this.inPopout && this.loadAction) {
this.close();
}
}
async edit() {
@ -191,6 +238,10 @@ export class ViewComponent extends BaseViewComponent {
const didAutofill = await this.doAutofill();
if (didAutofill) {
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
if (this.inPopout) {
this.close();
}
}
}
@ -255,15 +306,30 @@ export class ViewComponent extends BaseViewComponent {
}
close() {
if (this.senderTabId) {
BrowserApi.focusTab(this.senderTabId);
}
if (this.inPopout) {
window.close();
return;
}
this.location.back();
}
private async loadPageDetails() {
this.pageDetails = [];
this.tab = await BrowserApi.getTabFromCurrentWindow();
if (this.tab == null) {
if (this.senderTabId) {
this.tab = await BrowserApi.getTab(this.senderTabId);
}
if (!this.tab) {
return;
}
BrowserApi.tabSendMessage(this.tab, {
command: "collectPageDetails",
tab: this.tab,