1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +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(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(1); expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith( expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
"Test Cipher (Test Username)", "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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; 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 { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -177,11 +176,7 @@ export class CipherContextMenuHandler {
} }
private async updateForCipher(url: string, cipher: CipherView) { private async updateForCipher(url: string, cipher: CipherView) {
if ( if (cipher == null || cipher.type !== CipherType.Login) {
cipher == null ||
cipher.type !== CipherType.Login ||
cipher.reprompt !== CipherRepromptType.None
) {
return; return;
} }

View File

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

View File

@ -28,6 +28,7 @@ window.addEventListener(
const forwardCommands = [ const forwardCommands = [
"promptForLogin", "promptForLogin",
"passwordReprompt",
"addToLockedVaultPendingNotifications", "addToLockedVaultPendingNotifications",
"unlockCompleted", "unlockCompleted",
"addedCipher", "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; return null;
} }

View File

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

View File

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

View File

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

View File

@ -11,20 +11,48 @@ class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
height: 800, height: 800,
}; };
async openLoginPrompt(senderWindowId: number) { async openUnlockPrompt(senderWindowId: number) {
await this.closeLoginPrompt(); await this.closeUnlockPrompt();
await this.openPopoutWindow( await this.openSingleActionPopout(
senderWindowId, senderWindowId,
"popup/index.html?uilocation=popout", "popup/index.html?uilocation=popout",
"loginPrompt" "unlockPrompt"
); );
} }
async closeLoginPrompt() { async closeUnlockPrompt() {
await this.closeSingleActionPopout("loginPrompt"); 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, senderWindowId: number,
popupWindowURL: string, popupWindowURL: string,
singleActionPopoutKey: string singleActionPopoutKey: string

View File

@ -555,7 +555,11 @@
class="box-content-row" class="box-content-row"
appStopClick appStopClick
(click)="fillCipher()" (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="row-main text-primary">
<div class="icon text-primary" aria-hidden="true"> <div class="icon text-primary" aria-hidden="true">

View File

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