mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
Streamlines change pass notification logic. (#16435)
* Streamlines change pass notification logic. * Includes test cases for all change password behavior. * Allows multiple cipher views to be passed to notification. * Removes assumptions about matching passwords. * Includes current password match for now. * [WIP] Fixes exact login match ignore for change. Partially updates save/update methods for ciphers. * Removes password matching. * Preserves nullable cipherId, set only while cipher action handled * Updates comment.
This commit is contained in:
@@ -35,7 +35,7 @@ interface NotificationQueueMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChangePasswordNotificationData = {
|
type ChangePasswordNotificationData = {
|
||||||
cipherId: CipherView["id"];
|
cipherIds: CipherView["id"][];
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ describe("NotificationBackground", () => {
|
|||||||
let tab: chrome.tabs.Tab;
|
let tab: chrome.tabs.Tab;
|
||||||
let sender: chrome.runtime.MessageSender;
|
let sender: chrome.runtime.MessageSender;
|
||||||
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
|
||||||
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
||||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||||
@@ -306,10 +305,7 @@ describe("NotificationBackground", () => {
|
|||||||
notificationBackground as any,
|
notificationBackground as any,
|
||||||
"getEnableAddedLoginPrompt",
|
"getEnableAddedLoginPrompt",
|
||||||
);
|
);
|
||||||
getEnableChangedPasswordPromptSpy = jest.spyOn(
|
|
||||||
notificationBackground as any,
|
|
||||||
"getEnableChangedPasswordPrompt",
|
|
||||||
);
|
|
||||||
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
|
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
|
||||||
pushChangePasswordToQueueSpy = jest.spyOn(
|
pushChangePasswordToQueueSpy = jest.spyOn(
|
||||||
notificationBackground as any,
|
notificationBackground as any,
|
||||||
@@ -368,24 +364,6 @@ describe("NotificationBackground", () => {
|
|||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
|
|
||||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
|
||||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
|
||||||
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
|
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
|
||||||
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
|
||||||
|
|
||||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
|
||||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
|
||||||
expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled();
|
|
||||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips attempting to change the password for an existing login if the password has not changed", async () => {
|
it("skips attempting to change the password for an existing login if the password has not changed", async () => {
|
||||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
@@ -445,37 +423,12 @@ describe("NotificationBackground", () => {
|
|||||||
sender.tab,
|
sender.tab,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
|
|
||||||
const data: ModifyLoginCipherFormData = {
|
|
||||||
...mockModifyLoginCipherFormData,
|
|
||||||
username: "tEsT",
|
|
||||||
};
|
|
||||||
|
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
|
||||||
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
|
|
||||||
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
|
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
|
||||||
mock<CipherView>({
|
|
||||||
id: "cipher-id",
|
|
||||||
login: { username: "test", password: "oldPassword" },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
|
||||||
|
|
||||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
|
||||||
"cipher-id",
|
|
||||||
"example.com",
|
|
||||||
data.password,
|
|
||||||
sender.tab,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("bgTriggerChangedPasswordNotification message handler", () => {
|
describe("bgTriggerChangedPasswordNotification message handler", () => {
|
||||||
let tab: chrome.tabs.Tab;
|
let tab: chrome.tabs.Tab;
|
||||||
let sender: chrome.runtime.MessageSender;
|
let sender: chrome.runtime.MessageSender;
|
||||||
|
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||||
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
||||||
@@ -488,6 +441,11 @@ describe("NotificationBackground", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tab = createChromeTabMock();
|
tab = createChromeTabMock();
|
||||||
sender = mock<chrome.runtime.MessageSender>({ tab });
|
sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||||
|
getEnableChangedPasswordPromptSpy = jest.spyOn(
|
||||||
|
notificationBackground as any,
|
||||||
|
"getEnableChangedPasswordPrompt",
|
||||||
|
);
|
||||||
|
|
||||||
pushChangePasswordToQueueSpy = jest.spyOn(
|
pushChangePasswordToQueueSpy = jest.spyOn(
|
||||||
notificationBackground as any,
|
notificationBackground as any,
|
||||||
"pushChangePasswordToQueue",
|
"pushChangePasswordToQueue",
|
||||||
@@ -495,6 +453,40 @@ describe("NotificationBackground", () => {
|
|||||||
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
|
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
getEnableChangedPasswordPromptSpy.mockRestore();
|
||||||
|
pushChangePasswordToQueueSpy.mockRestore();
|
||||||
|
getAllDecryptedForUrlSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
|
||||||
|
const data: ModifyLoginCipherFormData = {
|
||||||
|
...mockModifyLoginCipherFormData,
|
||||||
|
};
|
||||||
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
|
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
|
||||||
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
|
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips attempting to add the change password message to the queue if the user is logged out", async () => {
|
||||||
|
const data: ModifyLoginCipherFormData = {
|
||||||
|
...mockModifyLoginCipherFormData,
|
||||||
|
uri: "https://example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
|
||||||
|
|
||||||
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
|
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
|
||||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||||
|
|
||||||
@@ -503,7 +495,92 @@ describe("NotificationBackground", () => {
|
|||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
|
it("only only includes ciphers in notification data matching a username if username was present in the modify form data", async () => {
|
||||||
|
const data: ModifyLoginCipherFormData = {
|
||||||
|
...mockModifyLoginCipherFormData,
|
||||||
|
uri: "https://example.com",
|
||||||
|
username: "userName",
|
||||||
|
};
|
||||||
|
|
||||||
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-1",
|
||||||
|
login: { username: "test", password: "currentPassword" },
|
||||||
|
}),
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-2",
|
||||||
|
login: { username: "username", password: "currentPassword" },
|
||||||
|
}),
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-3",
|
||||||
|
login: { username: "uSeRnAmE", password: "currentPassword" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
|
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||||
|
["cipher-id-2", "cipher-id-3"],
|
||||||
|
"example.com",
|
||||||
|
data?.newPassword,
|
||||||
|
sender.tab,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds a change password message to the queue with current password, if there is a current password, but no new password", async () => {
|
||||||
|
const data: ModifyLoginCipherFormData = {
|
||||||
|
...mockModifyLoginCipherFormData,
|
||||||
|
uri: "https://example.com",
|
||||||
|
password: "newPasswordUpdatedElsewhere",
|
||||||
|
newPassword: null,
|
||||||
|
};
|
||||||
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-1",
|
||||||
|
login: { password: "currentPassword" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
|
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||||
|
["cipher-id-1"],
|
||||||
|
"example.com",
|
||||||
|
data?.password,
|
||||||
|
sender.tab,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds a change password message to the queue with new password, if new password is provided", async () => {
|
||||||
|
const data: ModifyLoginCipherFormData = {
|
||||||
|
...mockModifyLoginCipherFormData,
|
||||||
|
uri: "https://example.com",
|
||||||
|
password: "password2",
|
||||||
|
newPassword: "password3",
|
||||||
|
};
|
||||||
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-1",
|
||||||
|
login: { password: "password1" },
|
||||||
|
}),
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-4",
|
||||||
|
login: { password: "password4" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
|
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||||
|
["cipher-id-1", "cipher-id-4"],
|
||||||
|
"example.com",
|
||||||
|
data?.newPassword,
|
||||||
|
sender.tab,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds a change password message to the queue if the user has a locked account", async () => {
|
||||||
const data: ModifyLoginCipherFormData = {
|
const data: ModifyLoginCipherFormData = {
|
||||||
...mockModifyLoginCipherFormData,
|
...mockModifyLoginCipherFormData,
|
||||||
uri: "https://example.com",
|
uri: "https://example.com",
|
||||||
@@ -522,10 +599,12 @@ describe("NotificationBackground", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips adding a change password message to the queue if the multiple ciphers exist for the passed URL and the current password is not found within the list of ciphers", async () => {
|
it("doesn't add a password if there is no current or new password", async () => {
|
||||||
const data: ModifyLoginCipherFormData = {
|
const data: ModifyLoginCipherFormData = {
|
||||||
...mockModifyLoginCipherFormData,
|
...mockModifyLoginCipherFormData,
|
||||||
uri: "https://example.com",
|
uri: "https://example.com",
|
||||||
|
password: null,
|
||||||
|
newPassword: null,
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
@@ -537,23 +616,6 @@ describe("NotificationBackground", () => {
|
|||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
|
|
||||||
const data: ModifyLoginCipherFormData = {
|
|
||||||
...mockModifyLoginCipherFormData,
|
|
||||||
uri: "https://example.com",
|
|
||||||
};
|
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
|
||||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
|
||||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
|
||||||
|
|
||||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds a change password message to the queue if a single cipher matches the passed current password", async () => {
|
it("adds a change password message to the queue if a single cipher matches the passed current password", async () => {
|
||||||
const data: ModifyLoginCipherFormData = {
|
const data: ModifyLoginCipherFormData = {
|
||||||
...mockModifyLoginCipherFormData,
|
...mockModifyLoginCipherFormData,
|
||||||
@@ -570,28 +632,39 @@ describe("NotificationBackground", () => {
|
|||||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||||
"cipher-id",
|
["cipher-id"],
|
||||||
"example.com",
|
"example.com",
|
||||||
data?.newPassword,
|
data?.newPassword,
|
||||||
sender.tab,
|
sender.tab,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips adding a change password message if no current password is passed in the message and more than one cipher is found for a url", async () => {
|
it("adds a change password message with all matching ciphers if no current password is passed and more than one cipher is found for a url", async () => {
|
||||||
const data: ModifyLoginCipherFormData = {
|
const data: ModifyLoginCipherFormData = {
|
||||||
...mockModifyLoginCipherFormData,
|
...mockModifyLoginCipherFormData,
|
||||||
uri: "https://example.com",
|
uri: "https://example.com",
|
||||||
|
password: null,
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
mock<CipherView>({
|
||||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
id: "cipher-id-1",
|
||||||
|
login: { username: "test", password: "password" },
|
||||||
|
}),
|
||||||
|
mock<CipherView>({
|
||||||
|
id: "cipher-id-2",
|
||||||
|
login: { username: "test2", password: "password" },
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
["cipher-id-1", "cipher-id-2"],
|
||||||
|
"example.com",
|
||||||
|
data?.newPassword,
|
||||||
|
sender.tab,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => {
|
it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => {
|
||||||
@@ -611,7 +684,7 @@ describe("NotificationBackground", () => {
|
|||||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||||
|
|
||||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||||
"cipher-id",
|
["cipher-id"],
|
||||||
"example.com",
|
"example.com",
|
||||||
data?.newPassword,
|
data?.newPassword,
|
||||||
sender.tab,
|
sender.tab,
|
||||||
|
|||||||
@@ -213,14 +213,26 @@ export default class NotificationBackground {
|
|||||||
let cipherView: CipherView;
|
let cipherView: CipherView;
|
||||||
if (cipherQueueMessage.type === NotificationType.ChangePassword) {
|
if (cipherQueueMessage.type === NotificationType.ChangePassword) {
|
||||||
const {
|
const {
|
||||||
data: { cipherId },
|
data: { cipherIds },
|
||||||
} = cipherQueueMessage;
|
} = cipherQueueMessage;
|
||||||
cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
const cipherViews = await this.cipherService.getAllDecrypted(activeUserId);
|
||||||
|
return cipherViews
|
||||||
|
.filter((cipher) => cipherIds.includes(cipher.id))
|
||||||
|
.map((cipherView) => {
|
||||||
|
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||||
|
return this.convertToNotificationCipherData(
|
||||||
|
cipherView,
|
||||||
|
iconsServerUrl,
|
||||||
|
showFavicons,
|
||||||
|
organizationType,
|
||||||
|
);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationType = getOrganizationType(cipherView.organizationId);
|
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
this.convertToNotificationCipherData(
|
this.convertToNotificationCipherData(
|
||||||
cipherView,
|
cipherView,
|
||||||
@@ -555,16 +567,6 @@ export default class NotificationBackground {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
|
|
||||||
|
|
||||||
if (
|
|
||||||
changePasswordIsEnabled &&
|
|
||||||
usernameMatches.length === 1 &&
|
|
||||||
usernameMatches[0].login.password !== login.password
|
|
||||||
) {
|
|
||||||
await this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, login.password, tab);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,45 +605,92 @@ export default class NotificationBackground {
|
|||||||
data: ModifyLoginCipherFormData,
|
data: ModifyLoginCipherFormData,
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const changeData = {
|
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
|
||||||
url: data.uri,
|
if (!changePasswordIsEnabled) {
|
||||||
currentPassword: data.password,
|
|
||||||
newPassword: data.newPassword,
|
|
||||||
};
|
|
||||||
|
|
||||||
const loginDomain = Utils.getDomain(changeData.url);
|
|
||||||
if (loginDomain == null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const authStatus = await this.getAuthStatus();
|
||||||
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let id: string = null;
|
|
||||||
const activeUserId = await firstValueFrom(
|
const activeUserId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||||
);
|
);
|
||||||
if (activeUserId == null) {
|
if (activeUserId === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const loginDomain = Utils.getDomain(data.uri);
|
||||||
|
if (loginDomain === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId);
|
const username: string | null = data.username || null;
|
||||||
if (changeData.currentPassword != null) {
|
const currentPassword = data.password || null;
|
||||||
const passwordMatches = ciphers.filter(
|
const newPassword = data.newPassword || null;
|
||||||
(c) => c.login.password === changeData.currentPassword,
|
|
||||||
);
|
if (authStatus === AuthenticationStatus.Locked && newPassword !== null) {
|
||||||
if (passwordMatches.length === 1) {
|
await this.pushChangePasswordToQueue(null, loginDomain, newPassword, tab, true);
|
||||||
id = passwordMatches[0].id;
|
|
||||||
}
|
|
||||||
} else if (ciphers.length === 1) {
|
|
||||||
id = ciphers[0].id;
|
|
||||||
}
|
|
||||||
if (id != null) {
|
|
||||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ciphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||||
|
data.uri,
|
||||||
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalizedUsername: string = username ? username.toLowerCase() : "";
|
||||||
|
|
||||||
|
const shouldMatchUsername = typeof username === "string" && username.length > 0;
|
||||||
|
|
||||||
|
if (shouldMatchUsername) {
|
||||||
|
// Presence of a username should filter ciphers further.
|
||||||
|
ciphers = ciphers.filter(
|
||||||
|
(cipher) =>
|
||||||
|
cipher.login.username !== null &&
|
||||||
|
cipher.login.username.toLowerCase() === normalizedUsername,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ciphers.length === 1) {
|
||||||
|
const [cipher] = ciphers;
|
||||||
|
if (
|
||||||
|
username !== null &&
|
||||||
|
newPassword === null &&
|
||||||
|
cipher.login.username === normalizedUsername &&
|
||||||
|
cipher.login.password === currentPassword
|
||||||
|
) {
|
||||||
|
// Assumed to be a login
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPassword && !newPassword) {
|
||||||
|
// Only use current password for change if no new password present.
|
||||||
|
if (ciphers.length > 0) {
|
||||||
|
await this.pushChangePasswordToQueue(
|
||||||
|
ciphers.map((cipher) => cipher.id),
|
||||||
|
loginDomain,
|
||||||
|
currentPassword,
|
||||||
|
tab,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword) {
|
||||||
|
// Otherwise include all known ciphers.
|
||||||
|
if (ciphers.length > 0) {
|
||||||
|
await this.pushChangePasswordToQueue(
|
||||||
|
ciphers.map((cipher) => cipher.id),
|
||||||
|
loginDomain,
|
||||||
|
newPassword,
|
||||||
|
tab,
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,7 +715,7 @@ export default class NotificationBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pushChangePasswordToQueue(
|
private async pushChangePasswordToQueue(
|
||||||
cipherId: string,
|
cipherIds: CipherView["id"][],
|
||||||
loginDomain: string,
|
loginDomain: string,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
@@ -677,7 +726,7 @@ export default class NotificationBackground {
|
|||||||
const launchTimestamp = new Date().getTime();
|
const launchTimestamp = new Date().getTime();
|
||||||
const message: AddChangePasswordNotificationQueueMessage = {
|
const message: AddChangePasswordNotificationQueueMessage = {
|
||||||
type: NotificationType.ChangePassword,
|
type: NotificationType.ChangePassword,
|
||||||
data: { cipherId: cipherId, newPassword: newPassword },
|
data: { cipherIds: cipherIds, newPassword: newPassword },
|
||||||
domain: loginDomain,
|
domain: loginDomain,
|
||||||
tab: tab,
|
tab: tab,
|
||||||
launchTimestamp,
|
launchTimestamp,
|
||||||
@@ -716,12 +765,12 @@ export default class NotificationBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder);
|
await this.saveOrUpdateCredentials(sender.tab, message.cipherId, message.edit, message.folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) {
|
async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) {
|
||||||
if (message.success) {
|
if (message.success) {
|
||||||
await this.saveOrUpdateCredentials(message.tab, false, undefined, true);
|
await this.saveOrUpdateCredentials(message.tab, message.cipherId, false, undefined, true);
|
||||||
} else {
|
} else {
|
||||||
await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", {
|
await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", {
|
||||||
error: "Password reprompt failed",
|
error: "Password reprompt failed",
|
||||||
@@ -740,6 +789,7 @@ export default class NotificationBackground {
|
|||||||
*/
|
*/
|
||||||
private async saveOrUpdateCredentials(
|
private async saveOrUpdateCredentials(
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
|
cipherId: CipherView["id"],
|
||||||
edit: boolean,
|
edit: boolean,
|
||||||
folderId?: string,
|
folderId?: string,
|
||||||
skipReprompt: boolean = false,
|
skipReprompt: boolean = false,
|
||||||
@@ -764,7 +814,7 @@ export default class NotificationBackground {
|
|||||||
|
|
||||||
if (queueMessage.type === NotificationType.ChangePassword) {
|
if (queueMessage.type === NotificationType.ChangePassword) {
|
||||||
const {
|
const {
|
||||||
data: { cipherId, newPassword },
|
data: { newPassword },
|
||||||
} = queueMessage;
|
} = queueMessage;
|
||||||
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||||
|
|
||||||
|
|||||||
@@ -455,12 +455,12 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
switch (notificationType) {
|
switch (notificationType) {
|
||||||
case NotificationTypes.Change:
|
|
||||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
|
||||||
case NotificationTypes.Add:
|
case NotificationTypes.Add:
|
||||||
return (
|
return (
|
||||||
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
|
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
|
||||||
);
|
);
|
||||||
|
case NotificationTypes.Change:
|
||||||
|
return !!(modifyLoginData.password || modifyLoginData.newPassword);
|
||||||
case NotificationTypes.AtRiskPassword:
|
case NotificationTypes.AtRiskPassword:
|
||||||
return !modifyLoginData.newPassword;
|
return !modifyLoginData.newPassword;
|
||||||
case NotificationTypes.Unlock:
|
case NotificationTypes.Unlock:
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { BadgeButton } from "../../../content/components/buttons/badge-button";
|
|||||||
import { EditButton } from "../../../content/components/buttons/edit-button";
|
import { EditButton } from "../../../content/components/buttons/edit-button";
|
||||||
import { NotificationTypes } from "../../../notification/abstractions/notification-bar";
|
import { NotificationTypes } from "../../../notification/abstractions/notification-bar";
|
||||||
import { I18n } from "../common-types";
|
import { I18n } from "../common-types";
|
||||||
|
import { selectedCipher as selectedCipherSignal } from "../signals/selected-cipher";
|
||||||
|
|
||||||
export type CipherActionProps = {
|
export type CipherActionProps = {
|
||||||
|
cipherId: string;
|
||||||
handleAction?: (e: Event) => void;
|
handleAction?: (e: Event) => void;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
itemName: string;
|
itemName: string;
|
||||||
@@ -15,6 +17,7 @@ export type CipherActionProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function CipherAction({
|
export function CipherAction({
|
||||||
|
cipherId,
|
||||||
handleAction = () => {
|
handleAction = () => {
|
||||||
/* no-op */
|
/* no-op */
|
||||||
},
|
},
|
||||||
@@ -24,9 +27,17 @@ export function CipherAction({
|
|||||||
theme,
|
theme,
|
||||||
username,
|
username,
|
||||||
}: CipherActionProps) {
|
}: CipherActionProps) {
|
||||||
|
const selectCipherHandleAction = (e: Event) => {
|
||||||
|
selectedCipherSignal.set(cipherId);
|
||||||
|
try {
|
||||||
|
handleAction(e);
|
||||||
|
} finally {
|
||||||
|
selectedCipherSignal.set(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
return notificationType === NotificationTypes.Change
|
return notificationType === NotificationTypes.Change
|
||||||
? BadgeButton({
|
? BadgeButton({
|
||||||
buttonAction: handleAction,
|
buttonAction: selectCipherHandleAction,
|
||||||
buttonText: i18n.notificationUpdate,
|
buttonText: i18n.notificationUpdate,
|
||||||
itemName,
|
itemName,
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function CipherItem({
|
|||||||
if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) {
|
if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) {
|
||||||
cipherActionButton = html`<div>
|
cipherActionButton = html`<div>
|
||||||
${CipherAction({
|
${CipherAction({
|
||||||
|
cipherId: cipher.id,
|
||||||
handleAction,
|
handleAction,
|
||||||
i18n,
|
i18n,
|
||||||
itemName: name,
|
itemName: name,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { signal } from "@lit-labs/signals";
|
||||||
|
|
||||||
|
export const selectedCipher = signal<string | null>(null);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { render } from "lit";
|
import { render } from "lit";
|
||||||
|
|
||||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
import { NotificationCipherData } from "../content/components/cipher/types";
|
import { NotificationCipherData } from "../content/components/cipher/types";
|
||||||
@@ -8,6 +9,7 @@ import { CollectionView, I18n, OrgView } from "../content/components/common-type
|
|||||||
import { AtRiskNotification } from "../content/components/notification/at-risk-password/container";
|
import { AtRiskNotification } from "../content/components/notification/at-risk-password/container";
|
||||||
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
|
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
|
||||||
import { NotificationContainer } from "../content/components/notification/container";
|
import { NotificationContainer } from "../content/components/notification/container";
|
||||||
|
import { selectedCipher as selectedCipherSignal } from "../content/components/signals/selected-cipher";
|
||||||
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
|
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
|
||||||
import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault";
|
import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault";
|
||||||
|
|
||||||
@@ -180,9 +182,9 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
const i18n = getI18n();
|
const i18n = getI18n();
|
||||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||||
|
|
||||||
const resolvedType = resolveNotificationType(notificationBarIframeInitData);
|
const notificationType = resolveNotificationType(notificationBarIframeInitData);
|
||||||
const headerMessage = getNotificationHeaderMessage(i18n, resolvedType);
|
const headerMessage = getNotificationHeaderMessage(i18n, notificationType);
|
||||||
const notificationTestId = getNotificationTestId(resolvedType);
|
const notificationTestId = getNotificationTestId(notificationType);
|
||||||
appendHeaderMessageToTitle(headerMessage);
|
appendHeaderMessageToTitle(headerMessage);
|
||||||
|
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
@@ -191,7 +193,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
const notificationConfig = {
|
const notificationConfig = {
|
||||||
...notificationBarIframeInitData,
|
...notificationBarIframeInitData,
|
||||||
headerMessage,
|
headerMessage,
|
||||||
type: resolvedType,
|
type: notificationType,
|
||||||
notificationTestId,
|
notificationTestId,
|
||||||
theme: resolvedTheme,
|
theme: resolvedTheme,
|
||||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||||
@@ -201,7 +203,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAction = () => {
|
const handleSaveAction = () => {
|
||||||
sendSaveCipherMessage(true);
|
// cipher ID is null while vault is locked.
|
||||||
|
sendSaveCipherMessage(null, true);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
NotificationContainer({
|
NotificationContainer({
|
||||||
@@ -262,7 +265,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
NotificationContainer({
|
NotificationContainer({
|
||||||
...notificationBarIframeInitData,
|
...notificationBarIframeInitData,
|
||||||
headerMessage,
|
headerMessage,
|
||||||
type: resolvedType,
|
type: notificationType,
|
||||||
theme: resolvedTheme,
|
theme: resolvedTheme,
|
||||||
notificationTestId,
|
notificationTestId,
|
||||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||||
@@ -276,9 +279,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleEditOrUpdateAction(e: Event) {
|
function handleEditOrUpdateAction(e: Event) {
|
||||||
const notificationType = initData?.type;
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false);
|
sendSaveCipherMessage(selectedCipherSignal.get(), notificationType === NotificationTypes.Add);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +293,7 @@ function handleCloseNotification(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSaveAction(e: Event) {
|
function handleSaveAction(e: Event) {
|
||||||
|
const selectedCipher = selectedCipherSignal.get();
|
||||||
const selectedVault = selectedVaultSignal.get();
|
const selectedVault = selectedVaultSignal.get();
|
||||||
const selectedFolder = selectedFolderSignal.get();
|
const selectedFolder = selectedFolderSignal.get();
|
||||||
|
|
||||||
@@ -304,16 +307,16 @@ function handleSaveAction(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
sendSaveCipherMessage(selectedCipher, removeIndividualVault(), selectedFolder);
|
||||||
sendSaveCipherMessage(removeIndividualVault(), selectedFolder);
|
|
||||||
if (removeIndividualVault()) {
|
if (removeIndividualVault()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendSaveCipherMessage(edit: boolean, folder?: string) {
|
function sendSaveCipherMessage(cipherId: CipherView["id"] | null, edit: boolean, folder?: string) {
|
||||||
sendPlatformMessage({
|
sendPlatformMessage({
|
||||||
command: "bgSaveCipher",
|
command: "bgSaveCipher",
|
||||||
|
cipherId,
|
||||||
folder,
|
folder,
|
||||||
edit,
|
edit,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user