mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
Combine notif check/attempt, end msg pass syntax. (#15771)
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { CollectionView } from "../../content/components/common-types";
|
||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||
@@ -60,23 +57,10 @@ type LockedVaultPendingNotificationsData = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
type AtRiskPasswordNotificationsData = {
|
||||
activeUserId: UserId;
|
||||
cipher: CipherView;
|
||||
securityTask: SecurityTask;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
type AdjustNotificationBarMessageData = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
type ChangePasswordMessageData = {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type AddLoginMessageData = {
|
||||
username: string;
|
||||
password: string;
|
||||
@@ -92,10 +76,7 @@ type NotificationBackgroundExtensionMessage = {
|
||||
command: string;
|
||||
data?: Partial<LockedVaultPendingNotificationsData> &
|
||||
Partial<AdjustNotificationBarMessageData> &
|
||||
Partial<ChangePasswordMessageData> &
|
||||
Partial<UnlockVaultMessageData> &
|
||||
Partial<AtRiskPasswordNotificationsData>;
|
||||
login?: AddLoginMessageData;
|
||||
Partial<UnlockVaultMessageData>;
|
||||
folder?: string;
|
||||
edit?: boolean;
|
||||
details?: AutofillPageDetails;
|
||||
@@ -121,18 +102,6 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgOpenAtRiskPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgTriggerAddLoginNotification: ({
|
||||
message,
|
||||
sender,
|
||||
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||
bgTriggerChangedPasswordNotification: ({
|
||||
message,
|
||||
sender,
|
||||
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||
bgTriggerAtRiskPasswordNotification: ({
|
||||
message,
|
||||
sender,
|
||||
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
|
||||
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
bgOpenAddEditVaultItemPopout: ({
|
||||
@@ -162,7 +131,6 @@ export {
|
||||
NotificationQueueMessageItem,
|
||||
LockedVaultPendingNotificationsData,
|
||||
AdjustNotificationBarMessageData,
|
||||
ChangePasswordMessageData,
|
||||
UnlockVaultMessageData,
|
||||
AddLoginMessageData,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
export type NotificationTypeData = {
|
||||
@@ -8,6 +11,12 @@ export type NotificationTypeData = {
|
||||
launchTimestamp?: number;
|
||||
};
|
||||
|
||||
export type LoginSecurityTaskInfo = {
|
||||
securityTask: SecurityTask;
|
||||
cipher: CipherView;
|
||||
uri: ModifyLoginCipherFormData["uri"];
|
||||
};
|
||||
|
||||
export type WebsiteOriginsWithFields = Map<chrome.tabs.Tab["id"], Set<string>>;
|
||||
|
||||
export type ActiveFormSubmissionRequests = Set<chrome.webRequest.ResourceRequest["requestId"]>;
|
||||
|
||||
@@ -3,17 +3,18 @@ import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
LockedVaultPendingNotificationsData,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
} from "./abstractions/notification.background";
|
||||
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
|
||||
import NotificationBackground from "./notification.background";
|
||||
|
||||
jest.mock("rxjs", () => {
|
||||
@@ -58,13 +60,21 @@ describe("NotificationBackground", () => {
|
||||
const collectionService = mock<CollectionService>();
|
||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
const policyService = mock<DefaultPolicyService>();
|
||||
const policyAppliesToUser$ = new BehaviorSubject<boolean>(true);
|
||||
const policyService = mock<PolicyService>({
|
||||
policyAppliesToUser$: jest.fn().mockReturnValue(policyAppliesToUser$),
|
||||
});
|
||||
const folderService = mock<FolderService>();
|
||||
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
||||
const enableChangedPasswordPromptMock$ = new BehaviorSubject(true);
|
||||
const userNotificationSettingsService = mock<UserNotificationSettingsServiceAbstraction>();
|
||||
userNotificationSettingsService.enableChangedPasswordPrompt$ = enableChangedPasswordPromptMock$;
|
||||
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
const logService = mock<LogService>();
|
||||
const selectedThemeMock$ = new BehaviorSubject(ThemeTypes.Light);
|
||||
const themeStateService = mock<ThemeStateService>();
|
||||
themeStateService.selectedTheme$ = selectedThemeMock$;
|
||||
const configService = mock<ConfigService>();
|
||||
const accountService = mock<AccountService>();
|
||||
const organizationService = mock<OrganizationService>();
|
||||
@@ -164,7 +174,7 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification bar extension message handlers", () => {
|
||||
describe("notification bar extension message handlers and triggers", () => {
|
||||
beforeEach(() => {
|
||||
notificationBackground.init();
|
||||
});
|
||||
@@ -283,7 +293,12 @@ describe("NotificationBackground", () => {
|
||||
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||
|
||||
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
||||
username: "test",
|
||||
password: "password",
|
||||
uri: "https://example.com",
|
||||
newPassword: null,
|
||||
};
|
||||
beforeEach(() => {
|
||||
tab = createChromeTabMock();
|
||||
sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
@@ -304,43 +319,34 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the user is logged out", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
|
||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the login data does not contain a valid url", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "" },
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
|
||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the user with a locked vault has disabled the login notification", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
|
||||
@@ -349,16 +355,12 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the user with an unlocked vault has disabled the login notification", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
@@ -367,10 +369,7 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
|
||||
@@ -378,8 +377,7 @@ describe("NotificationBackground", () => {
|
||||
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
@@ -389,18 +387,14 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to change the password for an existing login if the password has not changed", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
@@ -409,48 +403,55 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("adds the login to the queue if the user has a locked account", async () => {
|
||||
const login = { username: "test", password: "password", url: "https://example.com" };
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login,
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true);
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
|
||||
"example.com",
|
||||
{
|
||||
url: data.uri,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
},
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds the login to the queue if the user has an unlocked account and the login is new", async () => {
|
||||
const login = {
|
||||
username: undefined,
|
||||
password: "password",
|
||||
url: "https://example.com",
|
||||
} as any;
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login,
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
username: null,
|
||||
};
|
||||
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab);
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
|
||||
"example.com",
|
||||
{
|
||||
url: data.uri,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
},
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
|
||||
const login = { username: "tEsT", password: "password", url: "https://example.com" };
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login,
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
username: "tEsT",
|
||||
};
|
||||
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
|
||||
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
|
||||
@@ -461,13 +462,12 @@ describe("NotificationBackground", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
login.password,
|
||||
data.password,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
@@ -478,6 +478,12 @@ describe("NotificationBackground", () => {
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
||||
username: null,
|
||||
uri: null,
|
||||
password: "currentPassword",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
tab = createChromeTabMock();
|
||||
@@ -490,69 +496,51 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: { newPassword: "newPassword", currentPassword: "currentPassword", url: "" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
|
||||
sendMockExtensionMessage(message);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
null,
|
||||
"example.com",
|
||||
message.data?.newPassword,
|
||||
data?.newPassword,
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(getAllDecryptedForUrlSpy).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 message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@@ -560,21 +548,16 @@ describe("NotificationBackground", () => {
|
||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
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 () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@@ -584,24 +567,20 @@ describe("NotificationBackground", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
message.data?.newPassword,
|
||||
data?.newPassword,
|
||||
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 () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@@ -609,20 +588,17 @@ describe("NotificationBackground", () => {
|
||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
password: null,
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@@ -632,13 +608,12 @@ describe("NotificationBackground", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
message.data?.newPassword,
|
||||
data?.newPassword,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
|
||||
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task";
|
||||
|
||||
// FIXME (PM-22628): Popup imports are forbidden in background
|
||||
@@ -68,14 +68,17 @@ import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
ChangePasswordMessageData,
|
||||
AddLoginMessageData,
|
||||
NotificationQueueMessageItem,
|
||||
LockedVaultPendingNotificationsData,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
NotificationBackgroundExtensionMessageHandlers,
|
||||
} from "./abstractions/notification.background";
|
||||
import { NotificationTypeData } from "./abstractions/overlay-notifications.background";
|
||||
import {
|
||||
LoginSecurityTaskInfo,
|
||||
ModifyLoginCipherFormData,
|
||||
NotificationTypeData,
|
||||
} from "./abstractions/overlay-notifications.background";
|
||||
import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background";
|
||||
|
||||
export default class NotificationBackground {
|
||||
@@ -91,12 +94,6 @@ export default class NotificationBackground {
|
||||
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
||||
bgAdjustNotificationBar: ({ message, sender }) =>
|
||||
this.handleAdjustNotificationBarMessage(message, sender),
|
||||
bgTriggerAddLoginNotification: ({ message, sender }) =>
|
||||
this.triggerAddLoginNotification(message, sender),
|
||||
bgTriggerChangedPasswordNotification: ({ message, sender }) =>
|
||||
this.triggerChangedPasswordNotification(message, sender),
|
||||
bgTriggerAtRiskPasswordNotification: ({ message, sender }) =>
|
||||
this.triggerAtRiskPasswordNotification(message, sender),
|
||||
bgCloseNotificationBar: ({ message, sender }) =>
|
||||
this.handleCloseNotificationBarMessage(message, sender),
|
||||
bgOpenAtRiskPasswords: ({ message, sender }) =>
|
||||
@@ -286,6 +283,62 @@ export default class NotificationBackground {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
* @param activeUserId - The currently logged in user ID
|
||||
*/
|
||||
private async getSecurityTaskAndCipherForLoginData(
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
activeUserId: UserId,
|
||||
): Promise<LoginSecurityTaskInfo | null> {
|
||||
const tasks: SecurityTask[] = await this.getSecurityTasks(activeUserId);
|
||||
if (!tasks?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||
modifyLoginData.uri,
|
||||
activeUserId,
|
||||
);
|
||||
if (!urlCiphers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const securityTaskForLogin = urlCiphers.reduce(
|
||||
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
|
||||
if (
|
||||
// exit early if info was found already
|
||||
taskInfo ||
|
||||
// exit early if the cipher was deleted
|
||||
cipher.deletedDate ||
|
||||
// exit early if the entered login info doesn't match an existing cipher
|
||||
modifyLoginData.username !== cipher.login.username ||
|
||||
modifyLoginData.password !== cipher.login.password
|
||||
) {
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
// Find the first security task for the cipherId belonging to the entered login
|
||||
const cipherSecurityTask = tasks.find(
|
||||
({ cipherId, status }) =>
|
||||
cipher.id === cipherId && // match security task cipher id to url cipher id
|
||||
status === SecurityTaskStatus.Pending, // security task has not been completed
|
||||
);
|
||||
|
||||
if (cipherSecurityTask) {
|
||||
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
|
||||
}
|
||||
|
||||
return taskInfo;
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
return securityTaskForLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active user server config from the config service.
|
||||
*/
|
||||
@@ -302,6 +355,10 @@ export default class NotificationBackground {
|
||||
return flagValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current authentication status of the user.
|
||||
* @returns Promise<AuthenticationStatus> - The current authentication status of the user.
|
||||
*/
|
||||
private async getAuthStatus() {
|
||||
return await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
}
|
||||
@@ -400,11 +457,32 @@ export default class NotificationBackground {
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
async triggerAtRiskPasswordNotification(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const { activeUserId, securityTask, cipher } = message.data;
|
||||
const domain = Utils.getDomain(sender.tab.url);
|
||||
const flag = await this.getNotificationFlag();
|
||||
if (!flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (!activeUserId) {
|
||||
return false;
|
||||
}
|
||||
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
|
||||
data,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
if (!loginSecurityTaskInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { securityTask, cipher } = loginSecurityTaskInfo;
|
||||
const domain = Utils.getDomain(tab.url);
|
||||
const passwordChangeUri =
|
||||
await new TemporaryNotificationChangeLoginService().getChangePasswordUrl(cipher);
|
||||
|
||||
@@ -418,7 +496,7 @@ export default class NotificationBackground {
|
||||
.pipe(getOrganizationById(securityTask.organizationId)),
|
||||
);
|
||||
|
||||
this.removeTabFromNotificationQueue(sender.tab);
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const queueMessage: NotificationQueueMessageItem = {
|
||||
domain,
|
||||
@@ -426,12 +504,12 @@ export default class NotificationBackground {
|
||||
type: NotificationQueueMessageType.AtRiskPassword,
|
||||
passwordChangeUri,
|
||||
organizationName: organization.name,
|
||||
tab: sender.tab,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
|
||||
};
|
||||
this.notificationQueue.push(queueMessage);
|
||||
await this.checkNotificationQueue(sender.tab);
|
||||
await this.checkNotificationQueue(tab);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -444,17 +522,22 @@ export default class NotificationBackground {
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
async triggerAddLoginNotification(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const login = {
|
||||
url: data.uri,
|
||||
username: data.username,
|
||||
password: data.password || data.newPassword,
|
||||
};
|
||||
|
||||
const authStatus = await this.getAuthStatus();
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const loginInfo = message.login;
|
||||
const normalizedUsername = loginInfo.username ? loginInfo.username.toLowerCase() : "";
|
||||
const loginDomain = Utils.getDomain(loginInfo.url);
|
||||
const normalizedUsername = login.username ? login.username.toLowerCase() : "";
|
||||
const loginDomain = Utils.getDomain(login.url);
|
||||
if (loginDomain == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -463,7 +546,7 @@ export default class NotificationBackground {
|
||||
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
if (addLoginIsEnabled) {
|
||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
|
||||
await this.pushAddLoginToQueue(loginDomain, login, tab, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -476,12 +559,12 @@ export default class NotificationBackground {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(login.url, activeUserId);
|
||||
const usernameMatches = ciphers.filter(
|
||||
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
|
||||
);
|
||||
if (addLoginIsEnabled && usernameMatches.length === 0) {
|
||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
|
||||
await this.pushAddLoginToQueue(loginDomain, login, tab);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -490,14 +573,9 @@ export default class NotificationBackground {
|
||||
if (
|
||||
changePasswordIsEnabled &&
|
||||
usernameMatches.length === 1 &&
|
||||
usernameMatches[0].login.password !== loginInfo.password
|
||||
usernameMatches[0].login.password !== login.password
|
||||
) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
usernameMatches[0].id,
|
||||
loginDomain,
|
||||
loginInfo.password,
|
||||
sender.tab,
|
||||
);
|
||||
await this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, login.password, tab);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -535,23 +613,22 @@ export default class NotificationBackground {
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
async triggerChangedPasswordNotification(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
const changeData = message.data as ChangePasswordMessageData;
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const changeData = {
|
||||
url: data.uri,
|
||||
currentPassword: data.password,
|
||||
newPassword: data.newPassword,
|
||||
};
|
||||
|
||||
const loginDomain = Utils.getDomain(changeData.url);
|
||||
if (loginDomain == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
null,
|
||||
loginDomain,
|
||||
changeData.newPassword,
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -575,7 +652,7 @@ export default class NotificationBackground {
|
||||
id = ciphers[0].id;
|
||||
}
|
||||
if (id != null) {
|
||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, sender.tab);
|
||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, Subject, switchMap, timer } from "rxjs";
|
||||
import { Subject, switchMap, timer } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
|
||||
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
|
||||
|
||||
import {
|
||||
@@ -25,12 +23,6 @@ import {
|
||||
} from "./abstractions/overlay-notifications.background";
|
||||
import NotificationBackground from "./notification.background";
|
||||
|
||||
type LoginSecurityTaskInfo = {
|
||||
securityTask: SecurityTask;
|
||||
cipher: CipherView;
|
||||
uri: ModifyLoginCipherFormData["uri"];
|
||||
};
|
||||
|
||||
export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface {
|
||||
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
|
||||
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
|
||||
@@ -274,8 +266,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
|
||||
return (
|
||||
!modifyLoginData ||
|
||||
!this.shouldAttemptAddLoginNotification(modifyLoginData) ||
|
||||
!this.shouldAttemptChangedPasswordNotification(modifyLoginData)
|
||||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Add) ||
|
||||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Change)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -381,7 +373,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
return;
|
||||
}
|
||||
|
||||
await this.triggerNotificationInit(requestId, modifyLoginData, tab);
|
||||
await this.processNotifications(requestId, modifyLoginData, tab);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -401,171 +393,86 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
const handleWebNavigationOnCompleted = async () => {
|
||||
chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted);
|
||||
const tab = await BrowserApi.getTab(tabId);
|
||||
await this.triggerNotificationInit(requestId, modifyLoginData, tab);
|
||||
await this.processNotifications(requestId, modifyLoginData, tab);
|
||||
};
|
||||
chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the add login or change password notification based on the modified login form data
|
||||
* and the tab details. This will trigger the notification to be displayed to the user.
|
||||
* This method attempts to trigger the add login, change password, or at-risk password notifications
|
||||
* based on the modified login data and the tab details.
|
||||
*
|
||||
* @param requestId - The details of the web response
|
||||
* @param modifyLoginData - The modified login form data
|
||||
* @param tab - The tab details
|
||||
*/
|
||||
private triggerNotificationInit = async (
|
||||
private processNotifications = async (
|
||||
requestId: chrome.webRequest.ResourceRequest["requestId"],
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
config: { skippable: NotificationType[] } = { skippable: [] },
|
||||
) => {
|
||||
let result: string;
|
||||
if (this.shouldAttemptChangedPasswordNotification(modifyLoginData)) {
|
||||
// These notifications are temporarily setup as "messages" to the notification background.
|
||||
// This will be structured differently in a future refactor.
|
||||
const success = await this.notificationBackground.triggerChangedPasswordNotification(
|
||||
{
|
||||
command: "bgChangedPassword",
|
||||
data: {
|
||||
url: modifyLoginData.uri,
|
||||
currentPassword: modifyLoginData.password,
|
||||
newPassword: modifyLoginData.newPassword,
|
||||
},
|
||||
},
|
||||
{ tab },
|
||||
);
|
||||
if (!success) {
|
||||
result = "Unqualified changedPassword notification attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.shouldAttemptAddLoginNotification(modifyLoginData)) {
|
||||
const success = await this.notificationBackground.triggerAddLoginNotification(
|
||||
{
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: {
|
||||
url: modifyLoginData.uri,
|
||||
username: modifyLoginData.username,
|
||||
password: modifyLoginData.password || modifyLoginData.newPassword,
|
||||
},
|
||||
},
|
||||
{ tab },
|
||||
);
|
||||
if (!success) {
|
||||
result = "Unqualified addLogin notification attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
const shouldGetTasks =
|
||||
(await this.notificationBackground.getNotificationFlag()) && !modifyLoginData.newPassword;
|
||||
|
||||
if (shouldGetTasks) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (activeUserId) {
|
||||
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
|
||||
modifyLoginData,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
if (loginSecurityTaskInfo) {
|
||||
await this.notificationBackground.triggerAtRiskPasswordNotification(
|
||||
{
|
||||
command: "bgTriggerAtRiskPasswordNotification",
|
||||
data: {
|
||||
activeUserId,
|
||||
cipher: loginSecurityTaskInfo.cipher,
|
||||
securityTask: loginSecurityTaskInfo.securityTask,
|
||||
},
|
||||
},
|
||||
{ tab },
|
||||
);
|
||||
} else {
|
||||
result = "Unqualified atRiskPassword notification attempt.";
|
||||
}
|
||||
}
|
||||
}
|
||||
this.clearCompletedWebRequest(requestId, tab);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the change password notification should be triggered.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
*/
|
||||
private shouldAttemptChangedPasswordNotification = (
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
) => {
|
||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the add login notification should be triggered.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
*/
|
||||
private shouldAttemptAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
|
||||
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
|
||||
};
|
||||
|
||||
/**
|
||||
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
* @param activeUserId - The currently logged in user ID
|
||||
*/
|
||||
private async getSecurityTaskAndCipherForLoginData(
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
activeUserId: UserId,
|
||||
): Promise<LoginSecurityTaskInfo | null> {
|
||||
const tasks: SecurityTask[] = await this.notificationBackground.getSecurityTasks(activeUserId);
|
||||
if (!tasks?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||
modifyLoginData.uri,
|
||||
activeUserId,
|
||||
);
|
||||
if (!urlCiphers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const securityTaskForLogin = urlCiphers.reduce(
|
||||
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
|
||||
if (
|
||||
// exit early if info was found already
|
||||
taskInfo ||
|
||||
// exit early if the cipher was deleted
|
||||
cipher.deletedDate ||
|
||||
// exit early if the entered login info doesn't match an existing cipher
|
||||
modifyLoginData.username !== cipher.login.username ||
|
||||
modifyLoginData.password !== cipher.login.password
|
||||
) {
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
// Find the first security task for the cipherId belonging to the entered login
|
||||
const cipherSecurityTask = tasks.find(
|
||||
({ cipherId, status }) =>
|
||||
cipher.id === cipherId && // match security task cipher id to url cipher id
|
||||
status === SecurityTaskStatus.Pending, // security task has not been completed
|
||||
);
|
||||
|
||||
if (cipherSecurityTask) {
|
||||
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
|
||||
}
|
||||
|
||||
return taskInfo;
|
||||
const notificationCandidates = [
|
||||
{
|
||||
type: NotificationTypes.Change,
|
||||
trigger: this.notificationBackground.triggerChangedPasswordNotification,
|
||||
},
|
||||
null,
|
||||
{
|
||||
type: NotificationTypes.Add,
|
||||
trigger: this.notificationBackground.triggerAddLoginNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationTypes.AtRiskPassword,
|
||||
trigger: this.notificationBackground.triggerAtRiskPasswordNotification,
|
||||
},
|
||||
].filter(
|
||||
(candidate) =>
|
||||
this.shouldAttemptNotification(modifyLoginData, candidate.type) ||
|
||||
config.skippable.includes(candidate.type),
|
||||
);
|
||||
|
||||
return securityTaskForLogin;
|
||||
}
|
||||
const results: string[] = [];
|
||||
for (const { trigger, type } of notificationCandidates) {
|
||||
const success = await trigger.bind(this.notificationBackground)(modifyLoginData, tab);
|
||||
if (success) {
|
||||
results.push(`Success: ${type}`);
|
||||
break;
|
||||
} else {
|
||||
results.push(`Unqualified ${type} notification attempt.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.clearCompletedWebRequest(requestId, tab);
|
||||
return results.join(" ");
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the add login notification should be attempted based on the modified login form data.
|
||||
* @param modifyLoginData modified login form data
|
||||
* @param notificationType The type of notification to be triggered
|
||||
* @returns true if the notification should be attempted, false otherwise
|
||||
*/
|
||||
private shouldAttemptNotification = (
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
notificationType: NotificationType,
|
||||
): boolean => {
|
||||
switch (notificationType) {
|
||||
case NotificationTypes.Change:
|
||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||
case NotificationTypes.Add:
|
||||
return (
|
||||
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
|
||||
);
|
||||
case NotificationTypes.AtRiskPassword:
|
||||
return !modifyLoginData.newPassword;
|
||||
case NotificationTypes.Unlock:
|
||||
// Unlock notifications are handled separately and do not require form data
|
||||
return false;
|
||||
default:
|
||||
this.logService.error(`Unknown notification type: ${notificationType}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the completed web request and removes the modified login form data for the tab.
|
||||
|
||||
Reference in New Issue
Block a user