1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 02:23:25 +00:00

Merge branch 'auth/pm-19877/notification-processing' into auth/pm-23620/auth-request-answering-service

This commit is contained in:
Patrick Pimentel
2025-08-11 10:54:23 -04:00
86 changed files with 869 additions and 584 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2025.7.0",
"version": "2025.7.1",
"scripts": {
"build": "npm run build:chrome",
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",

View File

@@ -5573,5 +5573,11 @@
"wasmNotSupported": {
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
"description": "'WebAssembly' is a technical term and should not be translated."
},
"showMore": {
"message": "Show more"
},
"showLess": {
"message": "Show less"
}
}

View File

@@ -1,5 +1,6 @@
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CollectionView } from "../../content/components/common-types";
@@ -17,7 +18,7 @@ interface NotificationQueueMessage {
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
type: "change";
cipherId: string;
cipherId: CipherView["id"];
newPassword: string;
}

View File

@@ -28,19 +28,12 @@ export type ModifyLoginCipherFormData = {
newPassword: string;
};
export type ModifyLoginCipherFormDataForTab = Map<
chrome.tabs.Tab["id"],
{ uri: string; username: string; password: string; newPassword: string }
>;
export type ModifyLoginCipherFormDataForTab = Map<chrome.tabs.Tab["id"], ModifyLoginCipherFormData>;
export type OverlayNotificationsExtensionMessage = {
command: string;
uri?: string;
username?: string;
password?: string;
newPassword?: string;
details?: AutofillPageDetails;
};
} & ModifyLoginCipherFormData;
type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage };
type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender };

View File

@@ -1,12 +1,9 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import AutofillField from "../models/autofill-field";
@@ -27,9 +24,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
describe("OverlayNotificationsBackground", () => {
let logService: MockProxy<LogService>;
let notificationBackground: NotificationBackground;
let taskService: TaskService;
let accountService: AccountService;
let cipherService: CipherService;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let overlayNotificationsBackground: OverlayNotificationsBackground;
@@ -38,9 +32,6 @@ describe("OverlayNotificationsBackground", () => {
jest.useFakeTimers();
logService = mock<LogService>();
notificationBackground = mock<NotificationBackground>();
taskService = mock<TaskService>();
accountService = mock<AccountService>();
cipherService = mock<CipherService>();
getEnableChangedPasswordPromptSpy = jest
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
.mockResolvedValue(true);
@@ -50,9 +41,6 @@ describe("OverlayNotificationsBackground", () => {
overlayNotificationsBackground = new OverlayNotificationsBackground(
logService,
notificationBackground,
taskService,
accountService,
cipherService,
);
await overlayNotificationsBackground.init();
});

View File

@@ -2,11 +2,8 @@
// @ts-strict-ignore
import { Subject, switchMap, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
@@ -31,6 +28,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
generatedPasswordFilled: ({ message, sender }) =>
this.storeModifiedLoginFormData(message, sender),
formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender),
collectPageDetailsResponse: ({ message, sender }) =>
this.handleCollectPageDetailsResponse(message, sender),
@@ -39,9 +38,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
constructor(
private logService: LogService,
private notificationBackground: NotificationBackground,
private taskService: TaskService,
private accountService: AccountService,
private cipherService: CipherService,
) {}
/**
@@ -442,7 +438,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
}
}
this.clearCompletedWebRequest(requestId, tab);
this.clearCompletedWebRequest(requestId, tab.id);
return results.join(" ");
};
@@ -482,11 +478,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
*/
private clearCompletedWebRequest = (
requestId: chrome.webRequest.ResourceRequest["requestId"],
tab: chrome.tabs.Tab,
tabId: chrome.tabs.Tab["id"],
) => {
this.activeFormSubmissionRequests.delete(requestId);
this.modifyLoginCipherFormData.delete(tab.id);
this.websiteOriginsWithFields.delete(tab.id);
this.modifyLoginCipherFormData.delete(tabId);
this.websiteOriginsWithFields.delete(tabId);
this.setupWebRequestsListeners();
};

View File

@@ -49,7 +49,6 @@ import {
MAX_SUB_FRAME_DEPTH,
RedirectFocusDirection,
} from "../enums/autofill-overlay.enum";
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
import { AutofillService } from "../services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
import {
@@ -71,6 +70,7 @@ import {
triggerWebRequestOnCompletedEvent,
} from "../spec/testing-utils";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import {
FocusedFieldData,
InlineMenuPosition,
@@ -2076,7 +2076,7 @@ describe("OverlayBackground", () => {
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
let focusedFieldData: FocusedFieldData;
let formData: InlineMenuFormFieldData;
let formData: ModifyLoginCipherFormData;
beforeEach(async () => {
await initOverlayElementPorts();
@@ -3651,6 +3651,18 @@ describe("OverlayBackground", () => {
});
});
it("sends a message to the tab to store modify login change when a password is generated", async () => {
jest.useFakeTimers();
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
jest.advanceTimersByTime(400);
await flushPromises();
expect(tabsSendMessageSpy.mock.lastCall[1].command).toBe("generatedPasswordModifyLogin");
});
it("filters the page details to only include the new password fields before filling", async () => {
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
@@ -3663,31 +3675,6 @@ describe("OverlayBackground", () => {
allowTotpAutofill: false,
});
});
it("opens the inline menu for fields that fill a generated password", async () => {
jest.useFakeTimers();
const formData = {
uri: "https://example.com",
username: "username",
password: "password",
newPassword: "newPassword",
};
tabsSendMessageSpy.mockImplementation((_tab, message) => {
if (message.command === "getInlineMenuFormFieldData") {
return Promise.resolve(formData);
}
return Promise.resolve();
});
const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu");
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
jest.advanceTimersByTime(400);
await flushPromises();
expect(openInlineMenuSpy).toHaveBeenCalled();
});
});
});

View File

@@ -69,7 +69,6 @@ import {
MAX_SUB_FRAME_DEPTH,
} from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field";
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
import { AutofillService, PageDetail } from "../services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service";
import {
@@ -82,6 +81,7 @@ import {
} from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import {
BuildCipherDataParams,
CloseInlineMenuMessage,
@@ -1813,7 +1813,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Triggers a fill of the generated password into the current tab. Will trigger
* a focus of the last focused field after filling the password.
* a focus of the last focused field after filling the password.
*
* @param port - The port of the sender
*/
@@ -1857,10 +1857,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
});
globalThis.setTimeout(async () => {
if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) {
await this.openInlineMenu(port.sender, true);
}
}, 300);
await BrowserApi.tabSendMessage(
port.sender.tab,
{
command: "generatedPasswordModifyLogin",
},
{
frameId: this.focusedFieldData.frameId || 0,
},
);
}, 150);
}
/**
@@ -1891,7 +1897,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
*
* @param tab - The tab to get the form field data from
*/
private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise<InlineMenuFormFieldData> {
private async getInlineMenuFormFieldData(
tab: chrome.tabs.Tab,
): Promise<ModifyLoginCipherFormData> {
return await BrowserApi.tabSendMessage(
tab,
{

View File

@@ -35,7 +35,7 @@ class AutofillInit implements AutofillInitInterface {
* @param domElementVisibilityService - Used to check if an element is viewable.
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
* @param autofillInlineMenuContentService - The inline menu content service, potentially undefined.
* @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined.
* @param overlayNotificationsContentService - The overlay server notifications content service, potentially undefined.
*/
constructor(
domQueryService: DomQueryService,

View File

@@ -1,3 +1,4 @@
import { ModifyLoginCipherFormData } from "../../background/abstractions/overlay-notifications.background";
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
import AutofillField from "../../models/autofill-field";
@@ -8,13 +9,6 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
subFrameDepth: number;
};
export type InlineMenuFormFieldData = {
uri: string;
username: string;
password: string;
newPassword: string;
};
export type AutofillOverlayContentExtensionMessageHandlers = {
[key: string]: CallableFunction;
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
@@ -32,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
destroyAutofillInlineMenuListeners: () => void;
getInlineMenuFormFieldData: ({
message,
}: AutofillExtensionMessageParam) => Promise<InlineMenuFormFieldData>;
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
};
export interface AutofillOverlayContentService {

View File

@@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
import AutofillInit from "../content/autofill-init";
import {
AutofillOverlayElement,
@@ -1750,6 +1751,29 @@ describe("AutofillOverlayContentService", () => {
});
describe("extension onMessage handlers", () => {
describe("generatedPasswordModifyLogin", () => {
it("relays a message regarding password generation to store modified login data", async () => {
const formFieldData: ModifyLoginCipherFormData = {
newPassword: "newPassword",
password: "password",
uri: "http://localhost/",
username: "username",
};
jest
.spyOn(autofillOverlayContentService as any, "getFormFieldData")
.mockResolvedValue(formFieldData);
sendMockExtensionMessage({
command: "generatedPasswordModifyLogin",
});
await flushPromises();
const resolvedValue = await sendExtensionMessageSpy.mock.calls[0][1];
expect(resolvedValue).toEqual(formFieldData);
});
});
describe("addNewVaultItemFromOverlay message handler", () => {
it("skips sending the message if the overlay list is not visible", async () => {
jest

View File

@@ -12,6 +12,7 @@ import {
} from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
import {
FocusedFieldData,
NewCardCipherData,
@@ -48,7 +49,6 @@ import {
import {
AutofillOverlayContentExtensionMessageHandlers,
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
InlineMenuFormFieldData,
SubFrameDataFromWindowMessage,
} from "./abstractions/autofill-overlay-content.service";
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
@@ -95,6 +95,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
destroyAutofillInlineMenuListeners: () => this.destroy(),
getInlineMenuFormFieldData: ({ message }) =>
this.handleGetInlineMenuFormFieldDataMessage(message),
generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(),
};
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
@@ -235,6 +236,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
});
}
/**
* On password generation, send form field data i.e. modified login data
*/
sendGeneratedPasswordModifyLogin = async () => {
await this.sendExtensionMessage("generatedPasswordFilled", this.getFormFieldData());
};
/**
* Formats any found user filled fields for a login cipher and sends a message
* to the background script to add a new cipher.
@@ -637,7 +645,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
/**
* Returns the form field data used for add login and change password notifications.
*/
private getFormFieldData = (): InlineMenuFormFieldData => {
private getFormFieldData = (): ModifyLoginCipherFormData => {
return {
uri: globalThis.document.URL,
username: this.userFilledFields["username"]?.value || "",

View File

@@ -7,7 +7,7 @@ import {
VaultTimeoutSettingsService,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
const IdleInterval = 60 * 5; // 5 minutes

View File

@@ -121,7 +121,7 @@ import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { Account } from "@bitwarden/common/platform/models/domain/account";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
import {
DefaultServerNotificationsService,
@@ -129,9 +129,7 @@ import {
UnsupportedWebPushConnectionService,
WebPushNotificationsApiService,
WorkerWebPushConnectionService,
} from "@bitwarden/common/platform/notifications/internal";
import { SystemNotificationsService } from "@bitwarden/common/platform/notifications/system-notifications.service";
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/notifications/unsupported-system-notifications.service";
} from "@bitwarden/common/platform/server-notifications/internal";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
@@ -171,6 +169,8 @@ import { WindowStorageService } from "@bitwarden/common/platform/storage/window-
import { SyncService } from "@bitwarden/common/platform/sync";
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/";
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service";
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -1294,9 +1294,6 @@ export default class MainBackground {
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.notificationBackground,
this.taskService,
this.accountService,
this.cipherService,
);
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
@@ -1411,6 +1408,7 @@ export default class MainBackground {
this.badgeService = new BadgeService(
this.stateProvider,
new DefaultBadgeBrowserApi(this.platformUtilsService),
this.logService,
);
this.authStatusBadgeUpdaterService = new AuthStatusBadgeUpdaterService(
this.badgeService,

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "Bitwarden",
"version": "2025.7.0",
"version": "2025.7.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "Bitwarden",
"version": "2025.7.0",
"version": "2025.7.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@@ -1,7 +1,10 @@
import { map, Observable } from "rxjs";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../browser/browser-api";
import { fromChromeEvent } from "../browser/from-chrome-event";
import { BadgeIcon, IconPaths } from "./icon";
@@ -13,6 +16,8 @@ export interface RawBadgeState {
}
export interface BadgeBrowserApi {
activeTab$: Observable<chrome.tabs.TabActiveInfo | undefined>;
setState(state: RawBadgeState, tabId?: number): Promise<void>;
getTabs(): Promise<number[]>;
}
@@ -21,6 +26,10 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi {
private badgeAction = BrowserApi.getBrowserAction();
private sidebarAction = BrowserApi.getSidebarAction(self);
activeTab$ = fromChromeEvent(chrome.tabs.onActivated).pipe(
map(([tabActiveInfo]) => tabActiveInfo),
);
constructor(private platformUtilsService: PlatformUtilsService) {}
async setState(state: RawBadgeState, tabId?: number): Promise<void> {

View File

@@ -1,5 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { Subscription } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec";
import { RawBadgeState } from "./badge-browser-api";
@@ -13,6 +15,7 @@ import { MockBadgeBrowserApi } from "./test/mock-badge-browser-api";
describe("BadgeService", () => {
let badgeApi: MockBadgeBrowserApi;
let stateProvider: FakeStateProvider;
let logService!: MockProxy<LogService>;
let badgeService!: BadgeService;
let badgeServiceSubscription: Subscription;
@@ -20,8 +23,9 @@ describe("BadgeService", () => {
beforeEach(() => {
badgeApi = new MockBadgeBrowserApi();
stateProvider = new FakeStateProvider(new FakeAccountService({}));
logService = mock<LogService>();
badgeService = new BadgeService(stateProvider, badgeApi);
badgeService = new BadgeService(stateProvider, badgeApi, logService);
});
afterEach(() => {
@@ -34,14 +38,10 @@ describe("BadgeService", () => {
describe("given a single tab is open", () => {
beforeEach(() => {
badgeApi.tabs = [1];
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
// This relies on the state provider to auto-emit
it("sets default values on startup", async () => {
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
});
it("sets provided state when no other state has been set", async () => {
const state: BadgeState = {
text: "text",
@@ -52,7 +52,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(state);
expect(badgeApi.specificStates[tabId]).toEqual(state);
});
@@ -63,7 +62,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@@ -82,7 +80,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@@ -105,7 +102,6 @@ describe("BadgeService", () => {
backgroundColor: "#aaa",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@@ -126,7 +122,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@@ -147,7 +142,6 @@ describe("BadgeService", () => {
await badgeService.clearState("state-3");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@@ -167,7 +161,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: DefaultBadgeState.icon,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@@ -190,26 +183,20 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Unlocked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
});
describe("given multiple tabs are open", () => {
const tabId = 1;
const tabIds = [1, 2, 3];
beforeEach(() => {
badgeApi.tabs = tabIds;
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
it("sets default values for each tab on startup", async () => {
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
for (const tabId of tabIds) {
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
}
});
it("sets state for each tab when no other state has been set", async () => {
const state: BadgeState = {
text: "text",
@@ -220,11 +207,10 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(state);
expect(badgeApi.specificStates).toEqual({
1: state,
2: state,
3: state,
2: undefined,
3: undefined,
});
});
});
@@ -236,6 +222,7 @@ describe("BadgeService", () => {
beforeEach(() => {
badgeApi.tabs = [tabId];
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
@@ -249,7 +236,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(state);
});
@@ -260,7 +246,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@@ -279,11 +264,6 @@ describe("BadgeService", () => {
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual({
...DefaultBadgeState,
text: "text",
icon: BadgeIcon.Locked,
});
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@@ -316,7 +296,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@@ -354,7 +333,6 @@ describe("BadgeService", () => {
backgroundColor: "#aaa",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@@ -377,11 +355,6 @@ describe("BadgeService", () => {
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual({
text: "override",
backgroundColor: "#aaa",
icon: DefaultBadgeState.icon,
});
expect(badgeApi.specificStates[tabId]).toEqual({
text: "override",
backgroundColor: "#aaa",
@@ -411,7 +384,6 @@ describe("BadgeService", () => {
await badgeService.clearState("state-2");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@@ -451,7 +423,6 @@ describe("BadgeService", () => {
await badgeService.clearState("state-3");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@@ -476,7 +447,6 @@ describe("BadgeService", () => {
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@@ -513,7 +483,6 @@ describe("BadgeService", () => {
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@@ -523,14 +492,16 @@ describe("BadgeService", () => {
});
describe("given multiple tabs are open", () => {
const tabId = 1;
const tabIds = [1, 2, 3];
beforeEach(() => {
badgeApi.tabs = tabIds;
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
it("sets tab-specific state for provided tab and general state for the others", async () => {
it("sets tab-specific state for provided tab", async () => {
const generalState: BadgeState = {
text: "general-text",
backgroundColor: "general-color",
@@ -550,11 +521,10 @@ describe("BadgeService", () => {
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(generalState);
expect(badgeApi.specificStates).toEqual({
[tabIds[0]]: { ...specificState, backgroundColor: "general-color" },
[tabIds[1]]: generalState,
[tabIds[2]]: generalState,
[tabIds[1]]: undefined,
[tabIds[2]]: undefined,
});
});
});

View File

@@ -1,15 +1,15 @@
import {
defer,
combineLatest,
concatMap,
distinctUntilChanged,
filter,
map,
mergeMap,
pairwise,
startWith,
Subscription,
switchMap,
} from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
BADGE_MEMORY,
GlobalState,
@@ -39,6 +39,7 @@ export class BadgeService {
constructor(
private stateProvider: StateProvider,
private badgeApi: BadgeBrowserApi,
private logService: LogService,
) {
this.states = this.stateProvider.getGlobal(BADGE_STATES);
}
@@ -48,52 +49,47 @@ export class BadgeService {
* Without this the service will not be able to update the badge state.
*/
startListening(): Subscription {
const initialSetup$ = defer(async () => {
const openTabs = await this.badgeApi.getTabs();
await this.badgeApi.setState(DefaultBadgeState);
for (const tabId of openTabs) {
await this.badgeApi.setState(DefaultBadgeState, tabId);
}
});
return initialSetup$
.pipe(
switchMap(() => this.states.state$),
return combineLatest({
states: this.states.state$.pipe(
startWith({}),
distinctUntilChanged(),
map((states) => new Set(states ? Object.values(states) : [])),
pairwise(),
map(([previous, current]) => {
const [removed, added] = difference(previous, current);
return { states: current, removed, added };
return { all: current, removed, added };
}),
filter(({ removed, added }) => removed.size > 0 || added.size > 0),
mergeMap(async ({ states, removed, added }) => {
const changed = [...removed, ...added];
const changedTabIds = new Set(
changed.map((s) => s.tabId).filter((tabId) => tabId !== undefined),
);
const onlyTabSpecificStatesChanged = changed.every((s) => s.tabId != undefined);
if (onlyTabSpecificStatesChanged) {
// If only tab-specific states changed then we only need to update those specific tabs.
for (const tabId of changedTabIds) {
const newState = this.calculateState(states, tabId);
await this.badgeApi.setState(newState, tabId);
}
),
activeTab: this.badgeApi.activeTab$.pipe(startWith(undefined)),
})
.pipe(
concatMap(async ({ states, activeTab }) => {
const changed = [...states.removed, ...states.added];
// If the active tab wasn't changed, we don't need to update the badge.
if (!changed.some((s) => s.tabId === activeTab?.tabId || s.tabId === undefined)) {
return;
}
// If there are any general states that changed then we need to update all tabs.
const openTabs = await this.badgeApi.getTabs();
const generalState = this.calculateState(states);
await this.badgeApi.setState(generalState);
for (const tabId of openTabs) {
const newState = this.calculateState(states, tabId);
await this.badgeApi.setState(newState, tabId);
try {
const state = this.calculateState(states.all, activeTab?.tabId);
await this.badgeApi.setState(state, activeTab?.tabId);
} catch (error) {
// This usually happens when the user opens a popout because of how the browser treats it
// as a tab in the same window but then won't let you set the badge state for it.
this.logService.warning("Failed to set badge state", error);
}
}),
)
.subscribe();
.subscribe({
error: (err: unknown) => {
this.logService.error(
"Fatal error in badge service observable, badge will fail to update",
err,
);
},
});
}
/**

View File

@@ -1,10 +1,22 @@
import { BehaviorSubject } from "rxjs";
import { BadgeBrowserApi, RawBadgeState } from "../badge-browser-api";
export class MockBadgeBrowserApi implements BadgeBrowserApi {
private _activeTab$ = new BehaviorSubject<chrome.tabs.TabActiveInfo | undefined>(undefined);
activeTab$ = this._activeTab$.asObservable();
specificStates: Record<number, RawBadgeState> = {};
generalState?: RawBadgeState;
tabs: number[] = [];
setActiveTab(tabId: number) {
this._activeTab$.next({
tabId,
windowId: 1,
});
}
setState(state: RawBadgeState, tabId?: number): Promise<void> {
if (tabId !== undefined) {
this.specificStates[tabId] = state;

View File

@@ -2,10 +2,10 @@ import { Observable, Subscription } from "rxjs";
import { NotificationResponse } from "@bitwarden/common/models/response/notification.response";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { UserId } from "@bitwarden/common/types/guid";
// Eventually if we want to support listening to notifications from browser foreground we
// Eventually if we want to support listening to server notifications from browser foreground we
// will only ever create a single SignalR connection, likely messaging to the background to reuse its connection.
export class ForegroundServerNotificationsService implements ServerNotificationsService {
notifications$: Observable<readonly [NotificationResponse, UserId]>;

View File

@@ -1,7 +1,5 @@
import { map, merge, Observable } from "rxjs";
import { v4 as uuidv4 } from "uuid";
import { DeviceType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
@@ -10,7 +8,7 @@ import {
SystemNotificationCreateInfo,
SystemNotificationEvent,
SystemNotificationsService,
} from "@bitwarden/common/platform/notifications/system-notifications.service";
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { fromChromeEvent } from "../browser/from-chrome-event";
@@ -37,60 +35,24 @@ export class BrowserSystemNotificationService implements SystemNotificationsServ
);
}
async create(createInfo: SystemNotificationCreateInfo): Promise<string | undefined> {
try {
const notificationId = createInfo.id || uuidv4();
const deviceType = this.platformUtilsService.getDevice();
const notificationOptions: chrome.notifications.NotificationOptions<true> = {
iconUrl: "https://avatars.githubusercontent.com/u/15990069?s=200",
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
return new Promise<string>((resolve) => {
const options: chrome.notifications.NotificationOptions<true> = {
iconUrl: chrome.runtime.getURL("images/icon128.png"),
message: createInfo.body,
type: "basic",
title: createInfo.title,
buttons: createInfo.buttons.map((value) => {
return { title: value.title };
}),
buttons: createInfo.buttons.map((value) => ({ title: value.title })),
};
switch (deviceType) {
case DeviceType.FirefoxExtension:
// Firefox does not support buttons in notifications
delete notificationOptions.buttons;
break;
default:
break;
if (createInfo.id != null) {
chrome.notifications.create(createInfo.id, options, (notificationId) =>
resolve(notificationId),
);
} else {
chrome.notifications.create(options, (notificationId) => resolve(notificationId));
}
chrome.notifications.create(notificationId, notificationOptions);
// eslint-disable-next-line no-restricted-syntax
chrome.notifications.onButtonClicked.addListener(
(notificationId: string, buttonIndex: number) => {
this.notificationClicked$.subscribe({
next: () => ({
id: notificationId,
buttonIdentifier: buttonIndex,
}),
});
},
);
// eslint-disable-next-line no-restricted-syntax
chrome.notifications.onClicked.addListener((notificationId: string) => {
this.notificationClicked$.subscribe({
next: () => ({
id: notificationId,
buttonIdentifier: ButtonLocation.NotificationButton,
}),
});
});
return notificationId;
} catch (e) {
this.logService.error(
`Failed to create notification on ${this.platformUtilsService.getDevice()} with error: ${e}`,
);
}
});
}
async clear(clearInfo: SystemNotificationClearInfo): Promise<undefined> {

View File

@@ -96,9 +96,8 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { flagEnabled } from "@bitwarden/common/platform/misc/flags";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { SystemNotificationsService } from "@bitwarden/common/platform/notifications/system-notifications.service";
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
@@ -114,6 +113,7 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";

View File

@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */ = {isa = PBXBuildFile; fileRef = 03100CAE291891F4008E14EF /* encrypt-worker.js */; };
55BC93932CB4268A008CA4C6 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 55BC93922CB4268A008CA4C6 /* assets */; };
55E0374D2577FA6B00979016 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E0374C2577FA6B00979016 /* AppDelegate.swift */; };
55E037502577FA6B00979016 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55E0374E2577FA6B00979016 /* Main.storyboard */; };
@@ -53,7 +52,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
03100CAE291891F4008E14EF /* encrypt-worker.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "encrypt-worker.js"; path = "../../../build/encrypt-worker.js"; sourceTree = "<group>"; };
5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; };
55BC93922CB4268A008CA4C6 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/assets; sourceTree = "<group>"; };
55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -155,7 +153,6 @@
isa = PBXGroup;
children = (
55BC93922CB4268A008CA4C6 /* assets */,
03100CAE291891F4008E14EF /* encrypt-worker.js */,
55E037702577FA6F00979016 /* popup */,
55E037712577FA6F00979016 /* background.js */,
55E037722577FA6F00979016 /* images */,
@@ -272,7 +269,6 @@
55E037802577FA6F00979016 /* background.html in Resources */,
55E0377A2577FA6F00979016 /* background.js in Resources */,
55E037792577FA6F00979016 /* popup in Resources */,
03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */,
55BC93932CB4268A008CA4C6 /* assets in Resources */,
55E0377C2577FA6F00979016 /* notification in Resources */,
55E0377E2577FA6F00979016 /* vendor.js in Resources */,

View File

@@ -1 +0,0 @@
v20

View File

@@ -90,9 +90,5 @@
"semver": "7.7.2",
"tldts": "7.0.1",
"zxcvbn": "4.4.2"
},
"engines": {
"node": "~20",
"npm": "~10"
}
}

View File

@@ -61,7 +61,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";

View File

@@ -13,7 +13,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync";

View File

@@ -3620,7 +3620,7 @@
},
"uriMatchDefaultStrategyHint": {
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
},
"regExAdvancedOptionWarning": {
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
@@ -4066,6 +4066,12 @@
}
}
},
"showMore": {
"message": "Show more"
},
"showLess": {
"message": "Show less"
},
"enableAutotype": {
"message": "Enable Autotype"
},

View File

@@ -21,7 +21,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";

View File

@@ -9,6 +9,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -86,26 +87,29 @@ describe("WebLoginComponentService", () => {
});
describe("getOrgPoliciesFromOrgInvite", () => {
const mockEmail = "test@example.com";
const orgInvite: OrganizationInvite = {
organizationId: "org-id",
token: "token",
email: mockEmail,
organizationUserId: "org-user-id",
initOrganization: false,
orgSsoIdentifier: "sso-id",
orgUserHasExistingUser: false,
organizationName: "org-name",
};
it("returns undefined if organization invite is null", async () => {
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.getOrgPoliciesFromOrgInvite();
const result = await service.getOrgPoliciesFromOrgInvite(mockEmail);
expect(result).toBeUndefined();
});
it("logs an error if getPoliciesByToken throws an error", async () => {
const error = new Error("Test error");
organizationInviteService.getOrganizationInvite.mockResolvedValue({
organizationId: "org-id",
token: "token",
email: "email",
organizationUserId: "org-user-id",
initOrganization: false,
orgSsoIdentifier: "sso-id",
orgUserHasExistingUser: false,
organizationName: "org-name",
});
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
policyApiService.getPoliciesByToken.mockRejectedValue(error);
await service.getOrgPoliciesFromOrgInvite();
await service.getOrgPoliciesFromOrgInvite(mockEmail);
expect(logService.error).toHaveBeenCalledWith(error);
});
@@ -120,16 +124,7 @@ describe("WebLoginComponentService", () => {
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
resetPasswordPolicyOptions.autoEnrollEnabled = autoEnrollEnabled;
organizationInviteService.getOrganizationInvite.mockResolvedValue({
organizationId: "org-id",
token: "token",
email: "email",
organizationUserId: "org-user-id",
initOrganization: false,
orgSsoIdentifier: "sso-id",
orgUserHasExistingUser: false,
organizationName: "org-name",
});
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
policyApiService.getPoliciesByToken.mockResolvedValue(policies);
internalPolicyService.getResetPasswordPolicyOptions.mockReturnValue([
@@ -141,7 +136,7 @@ describe("WebLoginComponentService", () => {
masterPasswordPolicyOptions,
);
const result = await service.getOrgPoliciesFromOrgInvite();
const result = await service.getOrgPoliciesFromOrgInvite(mockEmail);
expect(result).toEqual({
policies: policies,
@@ -151,5 +146,40 @@ describe("WebLoginComponentService", () => {
});
},
);
describe("given the orgInvite email does not match the provided email", () => {
const mockMismatchedEmail = "mismatched@example.com";
it("should clear the login redirect URL and organization invite", async () => {
// Arrange
organizationInviteService.getOrganizationInvite.mockResolvedValue({
...orgInvite,
email: mockMismatchedEmail,
});
// Act
await service.getOrgPoliciesFromOrgInvite(mockEmail);
// Assert
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1);
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1);
});
it("should log an error and return undefined", async () => {
// Arrange
organizationInviteService.getOrganizationInvite.mockResolvedValue({
...orgInvite,
email: mockMismatchedEmail,
});
// Act
const result = await service.getOrgPoliciesFromOrgInvite(mockEmail);
// Assert
expect(logService.error).toHaveBeenCalledWith(
`WebLoginComponentService.getOrgPoliciesFromOrgInvite: Email mismatch. Expected: ${mockMismatchedEmail}, Received: ${mockEmail}`,
);
expect(result).toBeUndefined();
});
});
});
});

View File

@@ -66,10 +66,27 @@ export class WebLoginComponentService
return;
}
async getOrgPoliciesFromOrgInvite(): Promise<PasswordPolicies | undefined> {
async getOrgPoliciesFromOrgInvite(email: string): Promise<PasswordPolicies | undefined> {
const orgInvite = await this.organizationInviteService.getOrganizationInvite();
if (orgInvite != null) {
/**
* Check if the email on the org invite matches the email submitted in the login form. This is
* important because say userA at "userA@mail.com" clicks an emailed org invite link, but then
* on the login page form they change the email to "userB@mail.com". We don't want to apply the org
* invite in state to userB. Therefore we clear the login redirect url as well as the org invite,
* allowing userB to login as normal.
*/
if (orgInvite.email !== email.toLowerCase()) {
await this.routerService.getAndClearLoginRedirectUrl();
await this.organizationInviteService.clearOrganizationInvitation();
this.logService.error(
`WebLoginComponentService.getOrgPoliciesFromOrgInvite: Email mismatch. Expected: ${orgInvite.email}, Received: ${email}`,
);
return undefined;
}
let policies: Policy[];
try {

View File

@@ -2,11 +2,14 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -60,6 +63,11 @@ describe("EmergencyViewDialogComponent", () => {
{ provide: AccountService, useValue: accountService },
{ provide: TaskService, useValue: mock<TaskService>() },
{ provide: LogService, useValue: mock<LogService>() },
{
provide: EnvironmentService,
useValue: { environment$: of({ getIconsUrl: () => "https://icons.example.com" }) },
},
{ provide: DomainSettingsService, useValue: { showFavicons$: of(true) } },
],
})
.overrideComponent(EmergencyViewDialogComponent, {

View File

@@ -84,7 +84,7 @@ import { IpcService } from "@bitwarden/common/platform/ipc";
import {
UnsupportedWebPushConnectionService,
WebPushConnectionService,
} from "@bitwarden/common/platform/notifications/internal";
} from "@bitwarden/common/platform/server-notifications/internal";
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";

View File

@@ -13,7 +13,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { IpcService } from "@bitwarden/common/platform/ipc";
import { ServerNotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";

View File

@@ -5,7 +5,7 @@ import { SupportStatus } from "@bitwarden/common/platform/misc/support-status";
import {
WebPushConnector,
WorkerWebPushConnectionService,
} from "@bitwarden/common/platform/notifications/internal";
} from "@bitwarden/common/platform/server-notifications/internal";
import { UserId } from "@bitwarden/common/types/guid";
export class PermissionsWebPushConnectionService extends WorkerWebPushConnectionService {

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Router } from "@angular/router";
import { firstValueFrom, ReplaySubject } from "rxjs";
import { firstValueFrom, Observable, ReplaySubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
@@ -16,6 +16,7 @@ import {
SelfHostedEnvironment,
} from "@bitwarden/common/platform/services/default-environment.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/user-core";
export type WebRegionConfig = RegionConfig & {
key: Region | string; // strings are used for custom environments
@@ -27,6 +28,8 @@ export type WebRegionConfig = RegionConfig & {
* Web specific environment service. Ensures that the urls are set from the window location.
*/
export class WebEnvironmentService extends DefaultEnvironmentService {
private _environmentSubject: ReplaySubject<Environment>;
constructor(
private win: Window,
stateProvider: StateProvider,
@@ -60,7 +63,9 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
// Override the environment observable with a replay subject
const subject = new ReplaySubject<Environment>(1);
subject.next(environment);
this._environmentSubject = subject;
this.environment$ = subject.asObservable();
this.globalEnvironment$ = subject.asObservable();
}
// Web setting env means navigating to a new location
@@ -100,6 +105,12 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
// This return shouldn't matter as we are about to leave the current window
return chosenRegionConfig.urls;
}
getEnvironment$(userId: UserId): Observable<Environment> {
// Web does not support account switching, and even if it did, you'd be required to be the environment of where the application
// is running.
return this._environmentSubject.asObservable();
}
}
export class WebCloudEnvironment extends CloudEnvironment {

View File

@@ -565,7 +565,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
this.refreshing = false;
// Explicitly mark for check to ensure the view is updated
// Some sources are not always emitted within the Angular zone (e.g. ciphers updated via WS notifications)
// Some sources are not always emitted within the Angular zone (e.g. ciphers updated via WS server notifications)
this.changeDetectorRef.markForCheck();
},
);

View File

@@ -11002,5 +11002,11 @@
},
"providersubCanceledmessage": {
"message" : "To resubscribe, contact Bitwarden Customer Support."
},
"showMore": {
"message": "Show more"
},
"showLess": {
"message": "Show less"
}
}
}