1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

Allow notification trigger attempts with tracking of last error.

This commit is contained in:
Miles Blackwood
2025-05-20 18:05:40 -04:00
parent c1ea9ba8c1
commit 46789392c2
4 changed files with 88 additions and 57 deletions

View File

@@ -119,14 +119,17 @@ type NotificationBackgroundExtensionMessageHandlers = {
sender,
}: BackgroundOnMessageHandlerParams) => Promise<CollectionView[]>;
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenAtRisksPasswordNotification: ({
bgTriggerAtRiskPasswordNotification: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<true | never>;
bgTriggerChangedPasswordNotification: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<true | never>;
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
bgOpenAddEditVaultItemPopout: ({

View File

@@ -85,12 +85,13 @@ export default class NotificationBackground {
ExtensionCommand.AutofillIdentity,
]);
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
bgAddLogin: ({ message, sender }) => this.addLogin(message, sender),
bgAddLogin: ({ message, sender }) => this.triggerAddLoginNotification(message, sender),
bgAdjustNotificationBar: ({ message, sender }) =>
this.handleAdjustNotificationBarMessage(message, sender),
bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender),
bgOpenAtRisksPasswordNotification: ({ message, sender }) =>
this.openAtRisksPasswordNotification(message, sender),
bgTriggerChangedPasswordNotification: ({ message, sender }) =>
this.triggerChangedPasswordNotification(message, sender),
bgTriggerAtRiskPasswordNotification: ({ message, sender }) =>
this.triggerAtRiskPasswordNotification(message, sender),
bgCloseNotificationBar: ({ message, sender }) =>
this.handleCloseNotificationBarMessage(message, sender),
bgOpenAtRisksPasswords: ({ message, sender }) =>
@@ -393,7 +394,7 @@ export default class NotificationBackground {
* @param message - The extension message
* @param sender - The contextual sender of the message
*/
async openAtRisksPasswordNotification(
async triggerAtRiskPasswordNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
@@ -435,20 +436,20 @@ export default class NotificationBackground {
* @param message - The message to add to the queue
* @param sender - The contextual sender of the message
*/
async addLogin(
async triggerAddLoginNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
): Promise<true | never> {
const authStatus = await this.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
return;
throw Error("Auth status is logged out.");
}
const loginInfo = message.login;
const normalizedUsername = loginInfo.username ? loginInfo.username.toLowerCase() : "";
const loginDomain = Utils.getDomain(loginInfo.url);
if (loginDomain == null) {
return;
throw Error("Login domain is not present.");
}
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
@@ -458,14 +459,14 @@ export default class NotificationBackground {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
}
return;
return true;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (activeUserId == null) {
return;
throw Error("No active user.");
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
@@ -474,7 +475,7 @@ export default class NotificationBackground {
);
if (addLoginIsEnabled && usernameMatches.length === 0) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
return;
return true;
}
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
@@ -490,7 +491,9 @@ export default class NotificationBackground {
loginInfo.password,
sender.tab,
);
return true;
}
throw Error("No change password notification pushed to queue.");
}
private async pushAddLoginToQueue(
@@ -524,14 +527,14 @@ export default class NotificationBackground {
* @param message - The message to add to the queue
* @param sender - The contextual sender of the message
*/
async changedPassword(
async triggerChangedPasswordNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
): Promise<true | never> {
const changeData = message.data as ChangePasswordMessageData;
const loginDomain = Utils.getDomain(changeData.url);
if (loginDomain == null) {
return;
throw new Error("No login domain present.");
}
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
@@ -542,7 +545,7 @@ export default class NotificationBackground {
sender.tab,
true,
);
return;
return true;
}
let id: string = null;
@@ -550,7 +553,7 @@ export default class NotificationBackground {
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (activeUserId == null) {
return;
throw new Error("No active user.");
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId);
@@ -566,7 +569,9 @@ export default class NotificationBackground {
}
if (id != null) {
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, sender.tab);
return true;
}
throw Error("No change password notification queued.");
}
/**
@@ -639,6 +644,7 @@ export default class NotificationBackground {
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
return true;
}
private async pushUnlockVaultToQueue(loginDomain: string, tab: chrome.tabs.Tab) {

View File

@@ -335,14 +335,22 @@ describe("OverlayNotificationsBackground", () => {
const pageDetails = mock<AutofillPageDetails>({ fields: [mock<AutofillField>()] });
let notificationChangedPasswordSpy: jest.SpyInstance;
let notificationAddLoginSpy: jest.SpyInstance;
let notificationAtRiskPasswordSpy: jest.SpyInstance;
beforeEach(async () => {
sender = mock<chrome.runtime.MessageSender>({
tab: { id: 1 },
url: "https://example.com",
});
notificationChangedPasswordSpy = jest.spyOn(notificationBackground, "changedPassword");
notificationAddLoginSpy = jest.spyOn(notificationBackground, "addLogin");
notificationChangedPasswordSpy = jest.spyOn(
notificationBackground,
"triggerChangedPasswordNotification",
);
notificationAddLoginSpy = jest.spyOn(notificationBackground, "triggerAddLoginNotification");
notificationAtRiskPasswordSpy = jest.spyOn(
notificationBackground,
"triggerAtRiskPasswordNotification",
);
sendMockExtensionMessage(
{ command: "collectPageDetailsResponse", details: pageDetails },
@@ -403,6 +411,7 @@ describe("OverlayNotificationsBackground", () => {
expect(notificationChangedPasswordSpy).not.toHaveBeenCalled();
expect(notificationAddLoginSpy).not.toHaveBeenCalled();
expect(notificationAtRiskPasswordSpy).not.toHaveBeenCalled();
});
it("ignores requests for tabs that do not contain stored login data", async () => {
@@ -427,6 +436,7 @@ describe("OverlayNotificationsBackground", () => {
expect(notificationChangedPasswordSpy).not.toHaveBeenCalled();
expect(notificationAddLoginSpy).not.toHaveBeenCalled();
expect(notificationAtRiskPasswordSpy).not.toHaveBeenCalled();
});
it("clears the notification fallback timeout if the request is completed with an invalid status code", async () => {

View File

@@ -268,8 +268,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
return (
!modifyLoginData ||
!this.shouldTriggerAddLoginNotification(modifyLoginData) ||
!this.shouldTriggerChangePasswordNotification(modifyLoginData)
!this.shouldAttemptAddLoginNotification(modifyLoginData) ||
!this.shouldAttemptChangePasswordNotification(modifyLoginData)
);
};
@@ -401,7 +401,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
};
/**
* Initializes the add login or change password notification based on the modified login form data
* Initializes the add login, change, or at risk password notification based on the modified login form data
* and the tab details. This will trigger the notification to be displayed to the user.
*
* @param requestId - The details of the web response
@@ -413,38 +413,48 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
modifyLoginData: ModifyLoginCipherFormData,
tab: chrome.tabs.Tab,
) => {
if (this.shouldTriggerChangePasswordNotification(modifyLoginData)) {
let error: Error;
if (this.shouldAttemptChangePasswordNotification(modifyLoginData)) {
// These notifications are temporarily setup as "messages" to the notification background.
// This will be structured differently in a future refactor.
await this.notificationBackground.changedPassword(
{
command: "bgChangedPassword",
data: {
url: modifyLoginData.uri,
currentPassword: modifyLoginData.password,
newPassword: modifyLoginData.newPassword,
try {
await this.notificationBackground.triggerChangedPasswordNotification(
{
command: "bgTriggerChangedPasswordNotification",
data: {
url: modifyLoginData.uri,
currentPassword: modifyLoginData.password,
newPassword: modifyLoginData.newPassword,
},
},
},
{ tab },
);
this.clearCompletedWebRequest(requestId, tab);
return;
{ tab },
);
this.clearCompletedWebRequest(requestId, tab);
return;
} catch (e) {
error = e;
}
}
if (this.shouldTriggerAddLoginNotification(modifyLoginData)) {
await this.notificationBackground.addLogin(
{
command: "bgAddLogin",
login: {
url: modifyLoginData.uri,
username: modifyLoginData.username,
password: modifyLoginData.password || modifyLoginData.newPassword,
if (this.shouldAttemptAddLoginNotification(modifyLoginData)) {
try {
await this.notificationBackground.triggerAddLoginNotification(
{
command: "bgAddLogin",
login: {
url: modifyLoginData.uri,
username: modifyLoginData.username,
password: modifyLoginData.password || modifyLoginData.newPassword,
},
},
},
{ tab },
);
this.clearCompletedWebRequest(requestId, tab);
return;
{ tab },
);
this.clearCompletedWebRequest(requestId, tab);
return;
} catch (e) {
error = e;
}
}
const shouldGetTasks: boolean = await this.notificationBackground.getNotificationFlag();
@@ -460,9 +470,9 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
const shouldTriggerAtRiskPasswordNotification: boolean = typeof securityTask !== "undefined";
if (shouldTriggerAtRiskPasswordNotification) {
await this.notificationBackground.openAtRisksPasswordNotification(
await this.notificationBackground.triggerAtRiskPasswordNotification(
{
command: "bgOpenAtRisksPasswordNotification",
command: "bgTriggerAtRiskPasswordNotification",
data: {
activeUserId,
cipher,
@@ -472,27 +482,29 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
{ tab },
);
this.clearCompletedWebRequest(requestId, tab);
return;
}
}
return error;
};
/**
* Determines if the change password notification should be triggered.
* Determines if the change password notification should be attempted.
*
* @param modifyLoginData - The modified login form data
*/
private shouldTriggerChangePasswordNotification = (
private shouldAttemptChangePasswordNotification = (
modifyLoginData: ModifyLoginCipherFormData,
) => {
return modifyLoginData?.newPassword && !modifyLoginData.username;
};
/**
* Determines if the add login notification should be triggered.
* Determines if the add login notification should be attempted.
*
* @param modifyLoginData - The modified login form data
*/
private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
private shouldAttemptAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
};