diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index db110319d20..067c1d16fc8 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -1,6 +1,10 @@ +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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"; @@ -32,10 +36,17 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { type: "unlock"; } +interface AtRiskPasswordQueueMessage extends NotificationQueueMessage { + type: "at-risk-password"; + organization: Organization; + cipher: CipherView; +} + type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordQueueMessage - | AddUnlockVaultQueueMessage; + | AddUnlockVaultQueueMessage + | AtRiskPasswordQueueMessage; type LockedVaultPendingNotificationsData = { commandToRetry: { @@ -50,6 +61,13 @@ type LockedVaultPendingNotificationsData = { target: string; }; +type AtRiskPasswordNotificationsData = { + activeUserId: UserId; + cipher: CipherView; + securityTask: SecurityTask; + uri: string; +}; + type AdjustNotificationBarMessageData = { height: number; }; @@ -76,7 +94,8 @@ type NotificationBackgroundExtensionMessage = { data?: Partial & Partial & Partial & - Partial; + Partial & + Partial; login?: AddLoginMessageData; folder?: string; edit?: boolean; @@ -101,6 +120,10 @@ type NotificationBackgroundExtensionMessageHandlers = { sender, }: BackgroundOnMessageHandlerParams) => Promise; bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; + bgOpenAtRisksPasswordNotification: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 339b033809d..11d682e2f20 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -3,7 +3,10 @@ import { firstValueFrom, switchMap, map, of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -84,6 +87,8 @@ export default class NotificationBackground { bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender), + bgOpenAtRisksPasswordNotification: ({ message, sender }) => + this.openAtRisksPasswordNotification(message, sender), bgCloseNotificationBar: ({ message, sender }) => this.handleCloseNotificationBarMessage(message, sender), bgOpenAtRisksPasswords: ({ message, sender }) => @@ -372,6 +377,44 @@ export default class NotificationBackground { } } + /** + * Sends a message to trigger the at risk password notification + * + * @param message - The extension message + * @param sender - The contextual sender of the message + */ + async openAtRisksPasswordNotification( + message: NotificationBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + const { activeUserId, cipher, securityTask, uri } = message.data; + + const domain = Utils.getDomain(uri); + const addLoginIsEnabled = await this.getEnableAddedLoginPrompt(); + const wasVaultLocked = AuthenticationStatus.Locked && addLoginIsEnabled; + + const organization = await firstValueFrom( + this.organizationService + .organizations$(activeUserId) + .pipe(getOrganizationById(securityTask.organizationId)), + ); + + this.removeTabFromNotificationQueue(sender.tab); + const launchTimestamp = new Date().getTime(); + const queueMessage: NotificationQueueMessageItem = { + domain, + wasVaultLocked, + type: NotificationQueueMessageType.AtRiskPassword, + organization: organization, + tab: sender.tab, + cipher, + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), + }; + this.notificationQueue.push(queueMessage); + await this.checkNotificationQueue(sender.tab); + } + /** * Adds a login message to the notification queue, prompting the user to save * the login if it does not already exist in the vault. If the cipher exists diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index 5c85ce132d7..fd30bca676e 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -1,9 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Subject, switchMap, timer } from "rxjs"; +import { firstValueFrom, 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, TaskService } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils"; @@ -35,6 +41,9 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg constructor( private logService: LogService, private notificationBackground: NotificationBackground, + private taskService: TaskService, + private accountService: AccountService, + private cipherService: CipherService, ) {} /** @@ -436,6 +445,29 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg ); this.clearCompletedWebRequest(requestId, tab); } + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + const { cipher, securityTask } = await this.getSecurityTaskAndCipherForLoginData( + modifyLoginData, + activeUserId, + ); + const shouldTriggerAtRiskPasswordNotification: boolean = typeof securityTask !== "undefined"; + + if (shouldTriggerAtRiskPasswordNotification) { + await this.notificationBackground.openAtRisksPasswordNotification( + { + command: "bgOpenAtRisksPasswordNotification", + data: { + activeUserId, + cipher, + securityTask, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + } }; /** @@ -458,6 +490,40 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword); }; + /** + * Determines if the at-risk password notification should be triggered. + * + * @param modifyLoginData - The modified login form data + * @param activeUserId - The currently logged in user ID + */ + private async getSecurityTaskAndCipherForLoginData( + modifyLoginData: ModifyLoginCipherFormData, + activeUserId: UserId, + ) { + const shouldGetTasks: boolean = await this.notificationBackground.getNotificationFlag(); + if (!shouldGetTasks) { + return; + } + + const tasks: SecurityTask[] = await this.notificationBackground.getSecurityTasks(activeUserId); + const ciphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl( + modifyLoginData.uri, + activeUserId, + ); + + const cipherIds: CipherView["id"][] = ciphers.map((c) => c.id); + + const securityTask = + tasks.length > 0 && tasks.find((task) => cipherIds.indexOf(task.cipherId) > -1); + + const cipher = ciphers.find((cipher) => cipher.id === securityTask.cipherId); + + return { securityTask, cipher, uri: modifyLoginData.uri }; + // see at-risk-password-component launchChangePassword + // DefaultChangeLoginPasswordService + // this can be implemented as a provider in the view. + } + /** * Clears the completed web request and removes the modified login form data for the tab. * diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts index cc0fa359303..ea59da541f7 100644 --- a/apps/browser/src/autofill/content/components/notification/body.ts +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -3,7 +3,10 @@ import { html } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; -import { NotificationType } from "../../../notification/abstractions/notification-bar"; +import { + NotificationType, + NotificationTypes, +} from "../../../notification/abstractions/notification-bar"; import { CipherItem } from "../cipher"; import { NotificationCipherData } from "../cipher/types"; import { scrollbarStyles, spacing, themes, typography } from "../constants/styles"; @@ -17,12 +20,14 @@ const { css } = createEmotion({ export function NotificationBody({ ciphers = [], + passwordChangeUri, i18n, notificationType, theme = ThemeTypes.Light, handleEditOrUpdateAction, }: { ciphers?: NotificationCipherData[]; + passwordChangeUri: string; customClasses?: string[]; i18n: { [key: string]: string }; notificationType?: NotificationType; @@ -32,22 +37,32 @@ export function NotificationBody({ // @TODO get client vendor from context const isSafari = false; - return html` -
- ${ciphers.map((cipher) => - ItemRow({ - theme, - children: CipherItem({ - cipher, - i18n, - notificationType, - theme, - handleAction: handleEditOrUpdateAction, - }), - }), - )} -
- `; + switch (notificationType) { + case NotificationTypes.AtRiskPassword: + return html` +
+ ${passwordChangeUri ? i18n.atRiskChangePrompt : i18n.atRiskNavigatePrompt} + ${passwordChangeUri && i18n.changePassword} +
+ `; + default: + return html` +
+ ${ciphers.map((cipher) => + ItemRow({ + theme, + children: CipherItem({ + cipher, + i18n, + notificationType, + theme, + handleAction: handleEditOrUpdateAction, + }), + }), + )} +
+ `; + } } const notificationBodyStyles = ({ isSafari, theme }: { isSafari: boolean; theme: Theme }) => css` diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index b21a05696c1..bd4c52a226c 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -107,6 +107,8 @@ function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationTy return i18n.updateLogin; case NotificationTypes.Unlock: return ""; + case NotificationTypes.AtRiskPassword: + return i18n.atRiskPassword; default: return undefined; } diff --git a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts index 5a7b8fa990b..1fe6246f8b8 100644 --- a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts +++ b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts @@ -2,6 +2,7 @@ const NotificationQueueMessageType = { AddLogin: "add", ChangePassword: "change", UnlockVault: "unlock", + AtRiskPassword: "at-risk-password", } as const; type NotificationQueueMessageTypes = diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index e5fe474e58b..713566cc449 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -11,7 +11,7 @@ const NotificationTypes = { Add: "add", Change: "change", Unlock: "unlock", - SecurityTaskNotice: "security-task-notice", + AtRiskPassword: "at-risk-password", } as const; type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; @@ -32,6 +32,7 @@ type NotificationBarIframeInitData = { removeIndividualVault?: boolean; theme?: Theme; type?: NotificationType; // @TODO use `NotificationType` + passwordChangeUri?: string; }; type NotificationBarWindowMessage = { diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 3751341e64a..b5ce5f9e61c 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -55,6 +55,10 @@ function applyNotificationBarStyle() { function getI18n() { return { appName: chrome.i18n.getMessage("appName"), + atRiskPassword: chrome.i18n.getMessage("atRiskPassword"), + atRiskChangePrompt: chrome.i18n.getMessage("atRiskChangePrompt"), + atRiskNavigatePrompt: chrome.i18n.getMessage("atRiskNavigatePrompt"), + changePassword: chrome.i18n.getMessage("changePassword"), close: chrome.i18n.getMessage("close"), collection: chrome.i18n.getMessage("collection"), folder: chrome.i18n.getMessage("folder"), diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index da47542ee6b..4f2df8deb0b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1223,6 +1223,9 @@ export default class MainBackground { this.overlayNotificationsBackground = new OverlayNotificationsBackground( this.logService, this.notificationBackground, + this.taskService, + this.accountService, + this.cipherService, ); this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(