diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index bd96b388c6a..e91fba2e87a 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -312,6 +312,7 @@ jobs: cosign sign --yes ${images} - name: Scan Docker image + if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: container-scan uses: anchore/scan-action@869c549e657a088dc0441b08ce4fc0ecdac2bb65 # v5.3.0 with: @@ -320,9 +321,12 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub + if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} + sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} + ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - name: Log out of Docker run: docker logout diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index c5e189c4666..77b66ba8bf1 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -49,6 +49,8 @@ jobs: uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: cx_result.sarif + sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} + ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} quality: name: Quality scan diff --git a/apps/browser/package.json b/apps/browser/package.json index e3bccf3f0df..5a8ddd03b41 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.3.0", + "version": "2025.3.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index e02e3d8d951..d474e303336 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -59,6 +60,7 @@ describe("NotificationBackground", () => { const themeStateService = mock(); const configService = mock(); const accountService = mock(); + const organizationService = mock(); const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ id: "testId" as UserId, @@ -73,18 +75,19 @@ describe("NotificationBackground", () => { authService.activeAccountStatus$ = activeAccountStatusMock$; accountService.activeAccount$ = activeAccountSubject; notificationBackground = new NotificationBackground( + accountService, + authService, autofillService, cipherService, - authService, - policyService, - folderService, - userNotificationSettingsService, + configService, domainSettingsService, environmentService, + folderService, logService, + organizationService, + policyService, themeStateService, - configService, - accountService, + userNotificationSettingsService, ); }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 11037e7e261..50e0ee0aa75 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { 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"; @@ -63,46 +64,48 @@ export default class NotificationBackground { ExtensionCommand.AutofillIdentity, ]); private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { - unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), - bgGetFolderData: () => this.getFolderData(), - bgCloseNotificationBar: ({ message, sender }) => - this.handleCloseNotificationBarMessage(message, sender), + bgAddLogin: ({ message, sender }) => this.addLogin(message, sender), bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), - bgAddLogin: ({ message, sender }) => this.addLogin(message, sender), bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender), - bgRemoveTabFromNotificationQueue: ({ sender }) => - this.removeTabFromNotificationQueue(sender.tab), - bgSaveCipher: ({ message, sender }) => this.handleSaveCipherMessage(message, sender), - bgNeverSave: ({ sender }) => this.saveNever(sender.tab), - collectPageDetailsResponse: ({ message }) => - this.handleCollectPageDetailsResponseMessage(message), - bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab), - checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab), - bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), + bgCloseNotificationBar: ({ message, sender }) => + this.handleCloseNotificationBarMessage(message, sender), + bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), + bgGetDecryptedCiphers: () => this.getNotificationCipherData(), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetExcludedDomains: () => this.getExcludedDomains(), - bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), + bgGetFolderData: () => this.getFolderData(), + bgGetOrgData: () => this.getOrgData(), + bgNeverSave: ({ sender }) => this.saveNever(sender.tab), + bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), + bgRemoveTabFromNotificationQueue: ({ sender }) => + this.removeTabFromNotificationQueue(sender.tab), + bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), + bgSaveCipher: ({ message, sender }) => this.handleSaveCipherMessage(message, sender), + bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab), + checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab), + collectPageDetailsResponse: ({ message }) => + this.handleCollectPageDetailsResponseMessage(message), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), notificationRefreshFlagValue: () => this.getNotificationFlag(), - bgGetDecryptedCiphers: () => this.getNotificationCipherData(), - bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), + unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), }; constructor( + private accountService: AccountService, + private authService: AuthService, private autofillService: AutofillService, private cipherService: CipherService, - private authService: AuthService, - private policyService: PolicyService, - private folderService: FolderService, - private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, + private configService: ConfigService, private domainSettingsService: DomainSettingsService, private environmentService: EnvironmentService, + private folderService: FolderService, private logService: LogService, + private organizationService: OrganizationService, + private policyService: PolicyService, private themeStateService: ThemeStateService, - private configService: ConfigService, - private accountService: AccountService, + private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, ) {} init() { @@ -744,6 +747,26 @@ export default class NotificationBackground { ); } + /** + * Returns the first value found from the organization service organizations$ observable. + */ + private async getOrgData() { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + const organizations = await firstValueFrom( + this.organizationService.organizations$(activeUserId), + ); + return organizations.map((org) => { + const { id, name, productTierType } = org; + return { + id, + name, + productTierType, + }; + }); + } + /** * Handles the unlockCompleted extension message. Will close the notification bar * after an attempted autofill action, and retry the autofill action if the message diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts index 2433381dfba..66b580bde43 100644 --- a/apps/browser/src/autofill/content/components/notification/body.ts +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -16,12 +16,12 @@ const { css } = createEmotion({ }); export function NotificationBody({ - ciphers, + ciphers = [], notificationType, theme = ThemeTypes.Light, handleEditOrUpdateAction, }: { - ciphers: NotificationCipherData[]; + ciphers?: NotificationCipherData[]; customClasses?: string[]; notificationType?: NotificationType; theme: Theme; diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 1eb0a4ac5f4..8661f5957e1 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -60,23 +60,18 @@ export function NotificationButtonRow({ ) : ([] as Option[]); - const noFolderOption: Option = { - default: true, - icon: Folder, - text: "No folder", // @TODO localize - value: "0", - }; const folderOptions: Option[] = folders?.length - ? folders.reduce( + ? folders.reduce( (options, { id, name }: FolderView) => [ ...options, { icon: Folder, text: name, - value: id, + value: id === null ? "0" : id, + default: id === null, }, ], - [noFolderOption], + [], ) : []; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index f98ef795749..8d80dc9fb50 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -9,6 +9,7 @@ import { NotificationType, } from "../../../notification/abstractions/notification-bar"; import { NotificationCipherData } from "../cipher/types"; +import { FolderView, OrgView } from "../common-types"; import { themes, spacing } from "../constants/styles"; import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body"; @@ -20,20 +21,24 @@ import { export function NotificationContainer({ handleCloseNotification, + handleEditOrUpdateAction, + handleSaveAction, + ciphers, + folders, i18n, + organizations, theme = ThemeTypes.Light, type, - ciphers, - handleSaveAction, - handleEditOrUpdateAction, }: NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void; handleSaveAction: (e: Event) => void; handleEditOrUpdateAction: (e: Event) => void; } & { + ciphers?: NotificationCipherData[]; + folders?: FolderView[]; i18n: { [key: string]: string }; + organizations?: OrgView[]; type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` - ciphers: NotificationCipherData[]; }) { const headerMessage = getHeaderMessage(i18n, type); const showBody = true; @@ -42,8 +47,8 @@ export function NotificationContainer({
${NotificationHeader({ handleCloseNotification, - standalone: showBody, message: headerMessage, + standalone: showBody, theme, })} ${showBody @@ -56,9 +61,11 @@ export function NotificationContainer({ : null} ${NotificationFooter({ handleSaveAction, - theme, - notificationType: type, + folders, i18n, + notificationType: type, + organizations, + theme, })}
`; diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index ae489ea956b..bf3d562a0ef 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -880,7 +880,7 @@ async function loadNotificationBar() { const baseStyle = useComponentBar ? isNotificationFresh - ? "height: calc(276px + 25px); width: 450px; right: 0; transform:translateX(100%); opacity:0;" + ? "height: calc(276px + 50px); width: 450px; right: 0; transform:translateX(100%); opacity:0;" : "height: calc(276px + 25px); width: 450px; right: 0; transform:translateX(0%); opacity:1;" : "height: 42px; width: 100%;"; @@ -910,7 +910,7 @@ async function loadNotificationBar() { function getFrameStyle(useComponentBar: boolean): string { return ( (useComponentBar - ? "height: calc(276px + 25px); width: 450px; right: 0;" + ? "height: calc(276px + 50px); width: 450px; right: 0;" : "height: 42px; width: 100%; left: 0;") + " top: 0; padding: 0; position: fixed;" + " z-index: 2147483647; visibility: visible;" diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index cb14a86dffa..6e7427e3a38 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -1,5 +1,8 @@ import { Theme } from "@bitwarden/common/platform/enums"; +import { NotificationCipherData } from "../../../autofill/content/components/cipher/types"; +import { FolderView, OrgView } from "../../../autofill/content/components/common-types"; + const NotificationTypes = { Add: "add", Change: "change", @@ -9,21 +12,24 @@ const NotificationTypes = { type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; type NotificationBarIframeInitData = { - type?: string; // @TODO use `NotificationType` - isVaultLocked?: boolean; - theme?: Theme; - removeIndividualVault?: boolean; - importType?: string; applyRedesign?: boolean; + ciphers?: NotificationCipherData[]; + folders?: FolderView[]; + importType?: string; + isVaultLocked?: boolean; launchTimestamp?: number; + organizations?: OrgView[]; + removeIndividualVault?: boolean; + theme?: Theme; + type?: string; // @TODO use `NotificationType` }; type NotificationBarWindowMessage = { + cipherId?: string; command: string; error?: string; initData?: NotificationBarIframeInitData; username?: string; - cipherId?: string; }; type NotificationBarWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 617b1e58c14..d17c008372d 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -5,6 +5,8 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; +import { NotificationCipherData } from "../content/components/cipher/types"; +import { OrgView } from "../content/components/common-types"; import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container"; import { NotificationContainer } from "../content/components/notification/container"; import { buildSvgDomElement } from "../utils"; @@ -115,7 +117,7 @@ function setElementText(template: HTMLTemplateElement, elementId: string, text: } } -function initNotificationBar(message: NotificationBarWindowMessage) { +async function initNotificationBar(message: NotificationBarWindowMessage) { const { initData } = message; if (!initData) { return; @@ -131,7 +133,23 @@ function initNotificationBar(message: NotificationBarWindowMessage) { // Current implementations utilize a require for scss files which creates the need to remove the node. document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove()); - sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => { + await Promise.all([ + new Promise((resolve) => + sendPlatformMessage({ command: "bgGetOrgData" }, resolve), + ), + new Promise((resolve) => + sendPlatformMessage({ command: "bgGetFolderData" }, resolve), + ), + new Promise((resolve) => + sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve), + ), + ]).then(([organizations, folders, ciphers]) => { + notificationBarIframeInitData = { + ...notificationBarIframeInitData, + folders, + ciphers, + organizations, + }; // @TODO use context to avoid prop drilling return render( NotificationContainer({ @@ -142,7 +160,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { handleSaveAction, handleEditOrUpdateAction, i18n, - ciphers: cipherData, }), document.body, ); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f8f86e6a277..74fa6acdf79 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1173,18 +1173,19 @@ export default class MainBackground { () => this.generatePasswordToClipboard(), ); this.notificationBackground = new NotificationBackground( + this.accountService, + this.authService, this.autofillService, this.cipherService, - this.authService, - this.policyService, - this.folderService, - this.userNotificationSettingsService, + this.configService, this.domainSettingsService, this.environmentService, + this.folderService, this.logService, + this.organizationService, + this.policyService, this.themeStateService, - this.configService, - this.accountService, + this.userNotificationSettingsService, ); this.overlayNotificationsBackground = new OverlayNotificationsBackground( diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 5bfca440b99..4510c2f342d 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.3.0", + "version": "2025.3.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 1e2ac1812ca..fc897c1b1c3 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.3.0", + "version": "2025.3.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 67fa920d18d..37731f17fbe 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -3,14 +3,19 @@ import { BrowserApi } from "../browser/browser-api"; import BrowserClipboardService from "../services/browser-clipboard.service"; describe("OffscreenDocument", () => { - const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener"); - const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); - const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); - const consoleErrorSpy = jest.spyOn(console, "error"); + let browserClipboardServiceCopySpy: jest.SpyInstance; + let browserClipboardServiceReadSpy: jest.SpyInstance; + let browserApiMessageListenerSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("../offscreen-document/offscreen-document"); + beforeEach(async () => { + browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener"); + browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); + browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + + await import("./offscreen-document"); + }); describe("init", () => { it("sets up a `chrome.runtime.onMessage` listener", () => { @@ -47,6 +52,7 @@ describe("OffscreenDocument", () => { it("copies the message text", async () => { const text = "test"; + browserClipboardServiceCopySpy.mockResolvedValueOnce(undefined); sendMockExtensionMessage({ command: "offscreenCopyToClipboard", text }); await flushPromises(); @@ -56,6 +62,7 @@ describe("OffscreenDocument", () => { describe("handleOffscreenReadFromClipboard", () => { it("reads the value from the clipboard service", async () => { + browserClipboardServiceReadSpy.mockResolvedValueOnce(""); sendMockExtensionMessage({ command: "offscreenReadFromClipboard" }); await flushPromises(); diff --git a/apps/browser/src/platform/services/browser-clipboard.service.spec.ts b/apps/browser/src/platform/services/browser-clipboard.service.spec.ts index cf0d7c46004..1d6904c7eb0 100644 --- a/apps/browser/src/platform/services/browser-clipboard.service.spec.ts +++ b/apps/browser/src/platform/services/browser-clipboard.service.spec.ts @@ -2,9 +2,10 @@ import BrowserClipboardService from "./browser-clipboard.service"; describe("BrowserClipboardService", () => { let windowMock: any; - const consoleWarnSpy = jest.spyOn(console, "warn"); + let consoleWarnSpy: any; beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); windowMock = { navigator: { clipboard: { @@ -104,8 +105,6 @@ describe("BrowserClipboardService", () => { }); await BrowserClipboardService.read(windowMock as Window); - - expect(consoleWarnSpy).toHaveBeenCalled(); }); }); }); diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index fe049c4f1db..38166d10a08 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -185,7 +185,9 @@ describe("Browser Utils Service", () => { describe("copyToClipboard", () => { const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp"); - const clipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); + const clipboardServiceCopySpy = jest + .spyOn(BrowserClipboardService, "copy") + .mockResolvedValue(undefined); let triggerOffscreenCopyToClipboardSpy: jest.SpyInstance; beforeEach(() => { @@ -281,7 +283,9 @@ describe("Browser Utils Service", () => { describe("readFromClipboard", () => { const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp"); - const clipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); + const clipboardServiceReadSpy = jest + .spyOn(BrowserClipboardService, "read") + .mockResolvedValue(""); beforeEach(() => { getManifestVersionSpy.mockReturnValue(2); diff --git a/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html b/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html index b3bf06cfbe7..40dad4cde4b 100644 --- a/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html +++ b/apps/browser/src/tools/popup/settings/about-dialog/about-dialog.component.html @@ -42,12 +42,12 @@ -
+ -
+ diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index b7ff0718b35..f986bdfca31 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -440,6 +440,32 @@ const mapAddEditCipherInfoToInitialValues = ( initialValues.name = cipher.name; } + if (cipher.type === CipherType.Card) { + const card = cipher.card; + + if (card != null) { + if (card.cardholderName != null) { + initialValues.cardholderName = card.cardholderName; + } + + if (card.number != null) { + initialValues.number = card.number; + } + + if (card.expMonth != null) { + initialValues.expMonth = card.expMonth; + } + + if (card.expYear != null) { + initialValues.expYear = card.expYear; + } + + if (card.code != null) { + initialValues.code = card.code; + } + } + } + if (cipher.type === CipherType.Login) { const login = cipher.login; diff --git a/apps/desktop/src/platform/components/approve-ssh-request.html b/apps/desktop/src/platform/components/approve-ssh-request.html index 952e3344e9c..b7005872f25 100644 --- a/apps/desktop/src/platform/components/approve-ssh-request.html +++ b/apps/desktop/src/platform/components/approve-ssh-request.html @@ -14,13 +14,13 @@ {{params.cipherName}} {{ "sshkeyApprovalMessageSuffix" | i18n }} {{ params.action | i18n }} -
+ -
+ diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index f2995e31f12..3856f6f4e28 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -187,7 +187,7 @@ class="tw-mr-3" >
-
+
@@ -196,22 +196,25 @@ class="tw-text-xs" variant="secondary" *ngIf="u.status === userStatusType.Invited" - >{{ "invited" | i18n }} + {{ "invited" | i18n }} + {{ "needsConfirmation" | i18n }} + {{ "needsConfirmation" | i18n }} + {{ "revoked" | i18n }} + {{ "revoked" | i18n }} +
{{ u.email }} diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index 524ebbc28cf..ab93f0be3bc 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -52,7 +52,7 @@ [color]="c.avatarColor" size="small" > - + {{ c.email }} - + {{ c.email }} {{ "invited" | i18n diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index 0021d938f82..6e96b357e3e 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -13,6 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -65,6 +66,7 @@ describe("EmergencyViewDialogComponent", () => { useValue: ChangeLoginPasswordService, }, { provide: ConfigService, useValue: ConfigService }, + { provide: CipherService, useValue: mock() }, ], }, add: { @@ -79,6 +81,7 @@ describe("EmergencyViewDialogComponent", () => { useValue: mock(), }, { provide: ConfigService, useValue: mock() }, + { provide: CipherService, useValue: mock() }, ], }, }) diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index 0f31b8d4639..631ab02db7d 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -180,7 +180,7 @@ export class DeviceManagementComponent { private updateDeviceTable(devices: Array): void { this.dataSource.data = devices .map((device: DeviceView): DeviceTableData | null => { - if (device.id == undefined) { + if (!device.id) { this.validationService.showError(new Error(this.i18nService.t("deviceIdMissing"))); return null; } @@ -190,7 +190,7 @@ export class DeviceManagementComponent { return null; } - if (device.creationDate == undefined) { + if (!device.creationDate) { this.validationService.showError( new Error(this.i18nService.t("deviceCreationDateMissing")), ); diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html index 9965302d15a..7b1d859fb69 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html @@ -1,24 +1,26 @@

{{ "logInWithPasskey" | i18n }} - - - {{ "off" | i18n }} - {{ "ssoLoginIsRequired" | i18n }} - - - {{ - "on" | i18n - }} - {{ - "off" | i18n - }} + + + + {{ "off" | i18n }} - {{ "ssoLoginIsRequired" | i18n }} + + + + {{ "on" | i18n }} + + + {{ "off" | i18n }} + + - - {{ "beta" | i18n }} + {{ "beta" | i18n }} + diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html index 589704a409a..eb8e2c56527 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html @@ -49,8 +49,9 @@ appStopClick (click)="selectCipher(row)" title="{{ 'editItemWithName' | i18n: row.name }}" - >{{ row.name }} + {{ row.name }} + {{ row.name }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html index 7226c957598..7df5953c56a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html @@ -7,25 +7,27 @@ {{ "newDomain" | i18n }} + {{ ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n }} + + + + {{ data.orgDomain.domainName }} + + + {{ - ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n - }} - - {{ - data.orgDomain.domainName - }} - - {{ - ((accountDeprovisioningEnabled$ | async) - ? "domainStatusUnderVerification" - : "domainStatusUnverified" - ) | i18n - }} - {{ - ((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified") - | i18n - }} + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n + }} + + + {{ + ((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified") + | i18n + }} +
diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts index e24069a9fbe..bf834e8dd93 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts @@ -225,9 +225,10 @@ describe("NotificationsService", () => { }); it.each([ - { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, - { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, - { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, + // Temporarily rolling back notifications being connected while locked + // { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, + // { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, + // { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked }, ])( "does not re-connect when the user transitions from $initialStatus to $updatedStatus", @@ -252,7 +253,11 @@ describe("NotificationsService", () => { }, ); - it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])( + it.each([ + // Temporarily disabling notifications connecting while in a locked state + // AuthenticationStatus.Locked, + AuthenticationStatus.Unlocked, + ])( "connects when a user transitions from logged out to %s", async (newStatus: AuthenticationStatus) => { emitActiveUser(mockUser1); diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts index f0586e37ff7..fc505b018ce 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.ts +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts @@ -123,13 +123,13 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract ); } + // This method name is a lie currently as we also have an access token + // when locked, this is eventually where we want to be but it increases load + // on signalR so we are rolling back until we can move the load of browser to + // web push. private hasAccessToken$(userId: UserId) { return this.authService.authStatusFor$(userId).pipe( - map( - (authStatus) => - authStatus === AuthenticationStatus.Locked || - authStatus === AuthenticationStatus.Unlocked, - ), + map((authStatus) => authStatus === AuthenticationStatus.Unlocked), distinctUntilChanged(), ); } diff --git a/libs/importer/src/importers/msecure-csv-importer.spec.ts b/libs/importer/src/importers/msecure-csv-importer.spec.ts index 83e35802fac..3cf7cc713a8 100644 --- a/libs/importer/src/importers/msecure-csv-importer.spec.ts +++ b/libs/importer/src/importers/msecure-csv-importer.spec.ts @@ -8,6 +8,24 @@ describe("MSecureCsvImporter.parse", () => { importer = new MSecureCsvImporter(); }); + it("should correctly parse legacy formatted cards", async () => { + const mockCsvData = + `aWeirdOldStyleCard|1032,Credit Card,,Security code 1234,Card Number|12|5555 4444 3333 2222,Expiration Date|11|04/0029,Name on Card|9|Obi Wan Kenobi,Security Code|9|444,`.trim(); + const result = await importer.parse(mockCsvData); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("aWeirdOldStyleCard"); + expect(cipher.type).toBe(CipherType.Card); + expect(cipher.card.number).toBe("5555 4444 3333 2222"); + expect(cipher.card.expiration).toBe("04 / 2029"); + expect(cipher.card.code).toBe("444"); + expect(cipher.card.cardholderName).toBe("Obi Wan Kenobi"); + expect(cipher.notes).toBe("Security code 1234"); + expect(cipher.card.brand).toBe(""); + }); + it("should correctly parse credit card entries as Secret Notes", async () => { const mockCsvData = `myCreditCard|155089404,Credit Card,,,Card Number|12|41111111111111111,Expiration Date|11|05/2026,Security Code|9|123,Name on Card|0|John Doe,PIN|9|1234,Issuing Bank|0|Visa,Phone Number|4|,Billing Address|0|,`.trim(); diff --git a/libs/importer/src/importers/msecure-csv-importer.ts b/libs/importer/src/importers/msecure-csv-importer.ts index 322764fa8dc..e78c715976f 100644 --- a/libs/importer/src/importers/msecure-csv-importer.ts +++ b/libs/importer/src/importers/msecure-csv-importer.ts @@ -43,23 +43,34 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { ).split("/"); cipher.card.expMonth = month.trim(); cipher.card.expYear = year.trim(); - cipher.card.code = this.getValueOrDefault(this.splitValueRetainingLastPart(value[6])); - cipher.card.cardholderName = this.getValueOrDefault( - this.splitValueRetainingLastPart(value[7]), + const securityCodeRegex = RegExp("^Security Code\\|\\d*\\|"); + const securityCodeEntry = value.find((entry: string) => securityCodeRegex.test(entry)); + cipher.card.code = this.getValueOrDefault( + this.splitValueRetainingLastPart(securityCodeEntry), ); - cipher.card.brand = this.getValueOrDefault(this.splitValueRetainingLastPart(value[9])); - cipher.notes = - this.getValueOrDefault(value[8].split("|")[0]) + - ": " + - this.getValueOrDefault(this.splitValueRetainingLastPart(value[8]), "") + - "\n" + - this.getValueOrDefault(value[10].split("|")[0]) + - ": " + - this.getValueOrDefault(this.splitValueRetainingLastPart(value[10]), "") + - "\n" + - this.getValueOrDefault(value[11].split("|")[0]) + - ": " + - this.getValueOrDefault(this.splitValueRetainingLastPart(value[11]), ""); + + const cardNameRegex = RegExp("^Name on Card\\|\\d*\\|"); + const nameOnCardEntry = value.find((entry: string) => entry.match(cardNameRegex)); + cipher.card.cardholderName = this.getValueOrDefault( + this.splitValueRetainingLastPart(nameOnCardEntry), + ); + + cipher.card.brand = this.getValueOrDefault(this.splitValueRetainingLastPart(value[9]), ""); + + const noteRegex = RegExp("\\|\\d*\\|"); + const rawNotes = value + .slice(2) + .filter((entry: string) => !this.isNullOrWhitespace(entry) && !noteRegex.test(entry)); + const noteIndexes = [8, 10, 11]; + const indexedNotes = noteIndexes + .filter((idx) => value[idx] && noteRegex.test(value[idx])) + .map((idx) => value[idx]) + .map((val) => { + const key = val.split("|")[0]; + const value = this.getValueOrDefault(this.splitValueRetainingLastPart(val), ""); + return `${key}: ${value}`; + }); + cipher.notes = [...rawNotes, ...indexedNotes].join("\n"); } else if (value.length > 3) { cipher.type = CipherType.SecureNote; cipher.secureNote = new SecureNoteView(); @@ -95,6 +106,6 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { // like "Password|8|myPassword", we want to keep the "myPassword" but also ensure that if // the value contains any "|" it works fine private splitValueRetainingLastPart(value: string) { - return value.split("|").slice(0, 2).concat(value.split("|").slice(2).join("|")).pop(); + return value && value.split("|").slice(0, 2).concat(value.split("|").slice(2).join("|")).pop(); } } diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 3fc473c4465..8a16050804b 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -25,6 +25,11 @@ export type OptionalInitialValues = { username?: string; password?: string; name?: string; + cardholderName?: string; + number?: string; + expMonth?: string; + expYear?: string; + code?: string; }; /** diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts index 39a59192985..32baad189cf 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.spec.ts @@ -65,6 +65,8 @@ describe("CardDetailsSectionComponent", () => { cardView.cardholderName = "Ron Burgundy"; cardView.number = "4242 4242 4242 4242"; cardView.brand = "Visa"; + cardView.expMonth = ""; + cardView.code = ""; expect(patchCipherSpy).toHaveBeenCalled(); const patchFn = patchCipherSpy.mock.lastCall[0]; @@ -79,6 +81,10 @@ describe("CardDetailsSectionComponent", () => { }); const cardView = new CardView(); + cardView.cardholderName = ""; + cardView.number = ""; + cardView.expMonth = ""; + cardView.code = ""; cardView.expYear = "2022"; expect(patchCipherSpy).toHaveBeenCalled(); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index c2b3ebb59aa..cb00c7d24f5 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -97,6 +97,10 @@ export class CardDetailsSectionComponent implements OnInit { EventType = EventType; + get initialValues() { + return this.cipherFormContainer.config.initialValues; + } + constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, @@ -139,7 +143,9 @@ export class CardDetailsSectionComponent implements OnInit { const prefillCipher = this.cipherFormContainer.getInitialCipherView(); if (prefillCipher) { - this.setInitialValues(prefillCipher); + this.initFromExistingCipher(prefillCipher.card); + } else { + this.initNewCipher(); } if (this.disabled) { @@ -147,6 +153,26 @@ export class CardDetailsSectionComponent implements OnInit { } } + private initFromExistingCipher(existingCard: CardView) { + this.cardDetailsForm.patchValue({ + cardholderName: this.initialValues?.cardholderName ?? existingCard.cardholderName, + number: this.initialValues?.number ?? existingCard.number, + expMonth: this.initialValues?.expMonth ?? existingCard.expMonth, + expYear: this.initialValues?.expYear ?? existingCard.expYear, + code: this.initialValues?.code ?? existingCard.code, + }); + } + + private initNewCipher() { + this.cardDetailsForm.patchValue({ + cardholderName: this.initialValues?.cardholderName || "", + number: this.initialValues?.number || "", + expMonth: this.initialValues?.expMonth || "", + expYear: this.initialValues?.expYear || "", + code: this.initialValues?.code || "", + }); + } + /** Get the section heading based on the card brand */ getSectionHeading(): string { const { brand } = this.cardDetailsForm.value; diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 1df96656da5..57c2b4dbae4 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -15,7 +15,8 @@ import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -87,6 +88,7 @@ export class CipherViewComponent implements OnChanges, OnDestroy { private platformUtilsService: PlatformUtilsService, private changeLoginPasswordService: ChangeLoginPasswordService, private configService: ConfigService, + private cipherService: CipherService, ) {} async ngOnChanges() { @@ -152,7 +154,12 @@ export class CipherViewComponent implements OnChanges, OnDestroy { const userId = await firstValueFrom(this.activeUserId$); - if (this.cipher.edit && this.cipher.viewPassword) { + // Show Tasks for Manage and Edit permissions + // Using cipherService to see if user has access to cipher in a non-AC context to address with Edit Except Password permissions + const allCiphers = await firstValueFrom(this.cipherService.ciphers$(userId)); + const cipherServiceCipher = allCiphers[this.cipher?.id as CipherId]; + + if (cipherServiceCipher?.edit && cipherServiceCipher?.viewPassword) { await this.checkPendingChangePasswordTasks(userId); } diff --git a/package-lock.json b/package-lock.json index eaf5c0f24ed..cb51c157b09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -189,7 +189,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.3.0" + "version": "2025.3.1" }, "apps/cli": { "name": "@bitwarden/cli",