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:
parent
41bf1247ef
commit
d95f1163bf
@ -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)",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ window.addEventListener(
|
|||||||
|
|
||||||
const forwardCommands = [
|
const forwardCommands = [
|
||||||
"promptForLogin",
|
"promptForLogin",
|
||||||
|
"passwordReprompt",
|
||||||
"addToLockedVaultPendingNotifications",
|
"addToLockedVaultPendingNotifications",
|
||||||
"unlockCompleted",
|
"unlockCompleted",
|
||||||
"addedCipher",
|
"addedCipher",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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 };
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user