1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Generate password triggers notifications (#15874)

This commit is contained in:
Miles Blackwood
2025-08-07 13:12:05 -04:00
committed by GitHub
parent 46046ca1fa
commit f7169e909f
10 changed files with 75 additions and 79 deletions

View File

@@ -1,5 +1,6 @@
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CollectionView } from "../../content/components/common-types";
@@ -17,7 +18,7 @@ interface NotificationQueueMessage {
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
type: "change";
cipherId: string;
cipherId: CipherView["id"];
newPassword: string;
}

View File

@@ -28,19 +28,12 @@ export type ModifyLoginCipherFormData = {
newPassword: string;
};
export type ModifyLoginCipherFormDataForTab = Map<
chrome.tabs.Tab["id"],
{ uri: string; username: string; password: string; newPassword: string }
>;
export type ModifyLoginCipherFormDataForTab = Map<chrome.tabs.Tab["id"], ModifyLoginCipherFormData>;
export type OverlayNotificationsExtensionMessage = {
command: string;
uri?: string;
username?: string;
password?: string;
newPassword?: string;
details?: AutofillPageDetails;
};
} & ModifyLoginCipherFormData;
type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage };
type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender };

View File

@@ -1,12 +1,9 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import AutofillField from "../models/autofill-field";
@@ -27,9 +24,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
describe("OverlayNotificationsBackground", () => {
let logService: MockProxy<LogService>;
let notificationBackground: NotificationBackground;
let taskService: TaskService;
let accountService: AccountService;
let cipherService: CipherService;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let overlayNotificationsBackground: OverlayNotificationsBackground;
@@ -38,9 +32,6 @@ describe("OverlayNotificationsBackground", () => {
jest.useFakeTimers();
logService = mock<LogService>();
notificationBackground = mock<NotificationBackground>();
taskService = mock<TaskService>();
accountService = mock<AccountService>();
cipherService = mock<CipherService>();
getEnableChangedPasswordPromptSpy = jest
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
.mockResolvedValue(true);
@@ -50,9 +41,6 @@ describe("OverlayNotificationsBackground", () => {
overlayNotificationsBackground = new OverlayNotificationsBackground(
logService,
notificationBackground,
taskService,
accountService,
cipherService,
);
await overlayNotificationsBackground.init();
});

View File

@@ -2,11 +2,8 @@
// @ts-strict-ignore
import { Subject, switchMap, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
@@ -31,6 +28,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
generatedPasswordFilled: ({ message, sender }) =>
this.storeModifiedLoginFormData(message, sender),
formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender),
collectPageDetailsResponse: ({ message, sender }) =>
this.handleCollectPageDetailsResponse(message, sender),
@@ -39,9 +38,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
constructor(
private logService: LogService,
private notificationBackground: NotificationBackground,
private taskService: TaskService,
private accountService: AccountService,
private cipherService: CipherService,
) {}
/**
@@ -442,7 +438,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
}
}
this.clearCompletedWebRequest(requestId, tab);
this.clearCompletedWebRequest(requestId, tab.id);
return results.join(" ");
};
@@ -482,11 +478,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
*/
private clearCompletedWebRequest = (
requestId: chrome.webRequest.ResourceRequest["requestId"],
tab: chrome.tabs.Tab,
tabId: chrome.tabs.Tab["id"],
) => {
this.activeFormSubmissionRequests.delete(requestId);
this.modifyLoginCipherFormData.delete(tab.id);
this.websiteOriginsWithFields.delete(tab.id);
this.modifyLoginCipherFormData.delete(tabId);
this.websiteOriginsWithFields.delete(tabId);
this.setupWebRequestsListeners();
};

View File

@@ -49,7 +49,6 @@ import {
MAX_SUB_FRAME_DEPTH,
RedirectFocusDirection,
} from "../enums/autofill-overlay.enum";
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
import { AutofillService } from "../services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
import {
@@ -71,6 +70,7 @@ import {
triggerWebRequestOnCompletedEvent,
} from "../spec/testing-utils";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import {
FocusedFieldData,
InlineMenuPosition,
@@ -2076,7 +2076,7 @@ describe("OverlayBackground", () => {
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
let focusedFieldData: FocusedFieldData;
let formData: InlineMenuFormFieldData;
let formData: ModifyLoginCipherFormData;
beforeEach(async () => {
await initOverlayElementPorts();
@@ -3651,6 +3651,18 @@ describe("OverlayBackground", () => {
});
});
it("sends a message to the tab to store modify login change when a password is generated", async () => {
jest.useFakeTimers();
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
jest.advanceTimersByTime(400);
await flushPromises();
expect(tabsSendMessageSpy.mock.lastCall[1].command).toBe("generatedPasswordModifyLogin");
});
it("filters the page details to only include the new password fields before filling", async () => {
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
@@ -3663,31 +3675,6 @@ describe("OverlayBackground", () => {
allowTotpAutofill: false,
});
});
it("opens the inline menu for fields that fill a generated password", async () => {
jest.useFakeTimers();
const formData = {
uri: "https://example.com",
username: "username",
password: "password",
newPassword: "newPassword",
};
tabsSendMessageSpy.mockImplementation((_tab, message) => {
if (message.command === "getInlineMenuFormFieldData") {
return Promise.resolve(formData);
}
return Promise.resolve();
});
const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu");
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
jest.advanceTimersByTime(400);
await flushPromises();
expect(openInlineMenuSpy).toHaveBeenCalled();
});
});
});

View File

@@ -69,7 +69,6 @@ import {
MAX_SUB_FRAME_DEPTH,
} from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field";
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
import { AutofillService, PageDetail } from "../services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service";
import {
@@ -82,6 +81,7 @@ import {
} from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import {
BuildCipherDataParams,
CloseInlineMenuMessage,
@@ -1813,7 +1813,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Triggers a fill of the generated password into the current tab. Will trigger
* a focus of the last focused field after filling the password.
* a focus of the last focused field after filling the password.
*
* @param port - The port of the sender
*/
@@ -1857,10 +1857,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
});
globalThis.setTimeout(async () => {
if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) {
await this.openInlineMenu(port.sender, true);
}
}, 300);
await BrowserApi.tabSendMessage(
port.sender.tab,
{
command: "generatedPasswordModifyLogin",
},
{
frameId: this.focusedFieldData.frameId || 0,
},
);
}, 150);
}
/**
@@ -1891,7 +1897,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
*
* @param tab - The tab to get the form field data from
*/
private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise<InlineMenuFormFieldData> {
private async getInlineMenuFormFieldData(
tab: chrome.tabs.Tab,
): Promise<ModifyLoginCipherFormData> {
return await BrowserApi.tabSendMessage(
tab,
{

View File

@@ -1,3 +1,4 @@
import { ModifyLoginCipherFormData } from "../../background/abstractions/overlay-notifications.background";
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
import AutofillField from "../../models/autofill-field";
@@ -8,13 +9,6 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
subFrameDepth: number;
};
export type InlineMenuFormFieldData = {
uri: string;
username: string;
password: string;
newPassword: string;
};
export type AutofillOverlayContentExtensionMessageHandlers = {
[key: string]: CallableFunction;
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
@@ -32,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
destroyAutofillInlineMenuListeners: () => void;
getInlineMenuFormFieldData: ({
message,
}: AutofillExtensionMessageParam) => Promise<InlineMenuFormFieldData>;
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
};
export interface AutofillOverlayContentService {

View File

@@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
import AutofillInit from "../content/autofill-init";
import {
AutofillOverlayElement,
@@ -1750,6 +1751,29 @@ describe("AutofillOverlayContentService", () => {
});
describe("extension onMessage handlers", () => {
describe("generatedPasswordModifyLogin", () => {
it("relays a message regarding password generation to store modified login data", async () => {
const formFieldData: ModifyLoginCipherFormData = {
newPassword: "newPassword",
password: "password",
uri: "http://localhost/",
username: "username",
};
jest
.spyOn(autofillOverlayContentService as any, "getFormFieldData")
.mockResolvedValue(formFieldData);
sendMockExtensionMessage({
command: "generatedPasswordModifyLogin",
});
await flushPromises();
const resolvedValue = await sendExtensionMessageSpy.mock.calls[0][1];
expect(resolvedValue).toEqual(formFieldData);
});
});
describe("addNewVaultItemFromOverlay message handler", () => {
it("skips sending the message if the overlay list is not visible", async () => {
jest

View File

@@ -12,6 +12,7 @@ import {
} from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
import {
FocusedFieldData,
NewCardCipherData,
@@ -48,7 +49,6 @@ import {
import {
AutofillOverlayContentExtensionMessageHandlers,
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
InlineMenuFormFieldData,
SubFrameDataFromWindowMessage,
} from "./abstractions/autofill-overlay-content.service";
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
@@ -95,6 +95,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
destroyAutofillInlineMenuListeners: () => this.destroy(),
getInlineMenuFormFieldData: ({ message }) =>
this.handleGetInlineMenuFormFieldDataMessage(message),
generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(),
};
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
@@ -235,6 +236,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
});
}
/**
* On password generation, send form field data i.e. modified login data
*/
sendGeneratedPasswordModifyLogin = async () => {
await this.sendExtensionMessage("generatedPasswordFilled", this.getFormFieldData());
};
/**
* Formats any found user filled fields for a login cipher and sends a message
* to the background script to add a new cipher.
@@ -637,7 +645,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
/**
* Returns the form field data used for add login and change password notifications.
*/
private getFormFieldData = (): InlineMenuFormFieldData => {
private getFormFieldData = (): ModifyLoginCipherFormData => {
return {
uri: globalThis.document.URL,
username: this.userFilledFields["username"]?.value || "",

View File

@@ -1258,9 +1258,6 @@ export default class MainBackground {
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.notificationBackground,
this.taskService,
this.accountService,
this.cipherService,
);
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(