1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

Merge branch 'main' into autofill/pm-6546-blurring-of-autofilled-elements-causes-problems-in-blur-event-listeners

This commit is contained in:
Cesar Gonzalez
2024-03-05 13:47:11 -06:00
committed by GitHub
90 changed files with 2144 additions and 869 deletions

View File

@@ -109,6 +109,8 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>;
checkNotificationQueue: ({ sender }: BackgroundSenderParam) => Promise<void>;
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
getWebVaultUrlForNotification: () => string;
};

View File

@@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -45,6 +46,7 @@ describe("NotificationBackground", () => {
const policyService = mock<PolicyService>();
const folderService = mock<FolderService>();
const stateService = mock<BrowserStateService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>();
@@ -56,6 +58,7 @@ describe("NotificationBackground", () => {
policyService,
folderService,
stateService,
userNotificationSettingsService,
environmentService,
logService,
);
@@ -235,8 +238,8 @@ describe("NotificationBackground", () => {
let tab: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender;
let getAuthStatusSpy: jest.SpyInstance;
let getDisableAddLoginNotificationSpy: jest.SpyInstance;
let getDisableChangedPasswordNotificationSpy: jest.SpyInstance;
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let pushAddLoginToQueueSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
@@ -245,13 +248,13 @@ describe("NotificationBackground", () => {
tab = createChromeTabMock();
sender = mock<chrome.runtime.MessageSender>({ tab });
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
getDisableAddLoginNotificationSpy = jest.spyOn(
stateService,
"getDisableAddLoginNotification",
getEnableAddedLoginPromptSpy = jest.spyOn(
notificationBackground as any,
"getEnableAddedLoginPrompt",
);
getDisableChangedPasswordNotificationSpy = jest.spyOn(
stateService,
"getDisableChangedPasswordNotification",
getEnableChangedPasswordPromptSpy = jest.spyOn(
notificationBackground as any,
"getEnableChangedPasswordPrompt",
);
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
pushChangePasswordToQueueSpy = jest.spyOn(
@@ -272,7 +275,7 @@ describe("NotificationBackground", () => {
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).not.toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
});
@@ -287,7 +290,7 @@ describe("NotificationBackground", () => {
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).not.toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
});
@@ -297,13 +300,13 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(true);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
@@ -315,14 +318,14 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(true);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
@@ -334,8 +337,8 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
getDisableChangedPasswordNotificationSpy.mockReturnValueOnce(true);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
]);
@@ -344,9 +347,9 @@ describe("NotificationBackground", () => {
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(getDisableChangedPasswordNotificationSpy).toHaveBeenCalled();
expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
@@ -357,7 +360,7 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
]);
@@ -366,7 +369,7 @@ describe("NotificationBackground", () => {
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
@@ -376,7 +379,7 @@ describe("NotificationBackground", () => {
const login = { username: "test", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
@@ -393,7 +396,7 @@ describe("NotificationBackground", () => {
} as any;
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
]);
@@ -409,7 +412,8 @@ describe("NotificationBackground", () => {
const login = { username: "tEsT", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false);
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id",

View File

@@ -4,6 +4,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -57,6 +58,8 @@ export default class NotificationBackground {
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
};
@@ -67,6 +70,7 @@ export default class NotificationBackground {
private policyService: PolicyService,
private folderService: FolderService,
private stateService: BrowserStateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private environmentService: EnvironmentService,
private logService: LogService,
) {}
@@ -81,6 +85,20 @@ export default class NotificationBackground {
this.cleanupNotificationQueue();
}
/**
* Gets the enableChangedPasswordPrompt setting from the user notification settings service.
*/
async getEnableChangedPasswordPrompt(): Promise<boolean> {
return await firstValueFrom(this.userNotificationSettingsService.enableChangedPasswordPrompt$);
}
/**
* Gets the enableAddedLoginPrompt setting from the user notification settings service.
*/
async getEnableAddedLoginPrompt(): Promise<boolean> {
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
}
/**
* Checks the notification queue for any messages that need to be sent to the
* specified tab. If no tab is specified, the current tab will be used.
@@ -194,9 +212,10 @@ export default class NotificationBackground {
return;
}
const disabledAddLogin = await this.stateService.getDisableAddLoginNotification();
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
if (authStatus === AuthenticationStatus.Locked) {
if (!disabledAddLogin) {
if (addLoginIsEnabled) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
}
@@ -207,14 +226,15 @@ export default class NotificationBackground {
const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
);
if (!disabledAddLogin && usernameMatches.length === 0) {
if (addLoginIsEnabled && usernameMatches.length === 0) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
return;
}
const disabledChangePassword = await this.stateService.getDisableChangedPasswordNotification();
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
if (
!disabledChangePassword &&
changePasswordIsEnabled &&
usernameMatches.length === 1 &&
usernameMatches[0].login.password !== loginInfo.password
) {

View File

@@ -0,0 +1,25 @@
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
export type UserNotificationSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
export function userNotificationSettingsServiceFactory(
cache: { userNotificationSettingsService?: UserNotificationSettingsService } & CachedServices,
opts: UserNotificationSettingsServiceInitOptions,
): Promise<UserNotificationSettingsService> {
return factory(
cache,
"userNotificationSettingsService",
opts,
async () => new UserNotificationSettingsService(await stateProviderFactory(cache, opts)),
);
}

View File

@@ -7,7 +7,11 @@ import { WatchedForm } from "../models/watched-form";
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
import { FormData } from "../services/abstractions/autofill.service";
import { GlobalSettings, UserSettings } from "../types";
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils";
import {
getFromLocalStorage,
sendExtensionMessage,
setupExtensionDisconnectAction,
} from "../utils";
interface HTMLElementWithFormOpId extends HTMLElement {
formOpId: string;
@@ -86,12 +90,11 @@ async function loadNotificationBar() {
]);
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
// These are preferences for whether to show the notification bar based on the user's settings
// and they are set in the Settings > Options page in the browser extension.
let disabledAddLoginNotification = false;
let disabledChangedPasswordNotification = false;
const enableChangedPasswordPrompt = await sendExtensionMessage(
"bgGetEnableChangedPasswordPrompt",
);
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
let showNotificationBar = true;
// Look up the active user id from storage
const activeUserIdKey = "activeUserId";
const globalStorageKey = "global";
@@ -121,11 +124,7 @@ async function loadNotificationBar() {
// Example: '{"bitwarden.com":null}'
const excludedDomainsDict = globalSettings.neverDomains;
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) {
// Set local disabled preferences
disabledAddLoginNotification = globalSettings.disableAddLoginNotification;
disabledChangedPasswordNotification = globalSettings.disableChangedPasswordNotification;
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
handlePageChange();
}
@@ -352,9 +351,7 @@ async function loadNotificationBar() {
// to avoid missing any forms that are added after the page loads
observeDom();
sendPlatformMessage({
command: "checkNotificationQueue",
});
void sendExtensionMessage("checkNotificationQueue");
}
// This is a safeguard in case the observer misses a SPA page change.
@@ -392,10 +389,7 @@ async function loadNotificationBar() {
*
* */
function collectPageDetails() {
sendPlatformMessage({
command: "bgCollectPageDetails",
sender: "notificationBar",
});
void sendExtensionMessage("bgCollectPageDetails", { sender: "notificationBar" });
}
// End Page Detail Collection Methods
@@ -620,10 +614,9 @@ async function loadNotificationBar() {
continue;
}
const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification;
// if user has not disabled both notifications and we have a username and password field,
// if user has enabled either add login or change password notification, and we have a username and password field
if (
!disabledBoth &&
(enableChangedPasswordPrompt || enableAddedLoginPrompt) &&
watchedForms[i].usernameEl != null &&
watchedForms[i].passwordEl != null
) {
@@ -639,10 +632,7 @@ async function loadNotificationBar() {
const passwordPopulated = login.password != null && login.password !== "";
if (userNamePopulated && passwordPopulated) {
processedForm(form);
sendPlatformMessage({
command: "bgAddLogin",
login,
});
void sendExtensionMessage("bgAddLogin", { login });
break;
} else if (
userNamePopulated &&
@@ -659,7 +649,7 @@ async function loadNotificationBar() {
// if user has not disabled the password changed notification and we have multiple password fields,
// then check if the user has changed their password
if (!disabledChangedPasswordNotification && watchedForms[i].passwordEls != null) {
if (enableChangedPasswordPrompt && watchedForms[i].passwordEls != null) {
// Get the values of the password fields
const passwords: string[] = watchedForms[i].passwordEls
.filter((el: HTMLInputElement) => el.value != null && el.value !== "")
@@ -716,7 +706,7 @@ async function loadNotificationBar() {
currentPassword: curPass,
url: document.URL,
};
sendPlatformMessage({ command: "bgChangedPassword", data });
void sendExtensionMessage("bgChangedPassword", { data });
break;
}
}
@@ -954,9 +944,7 @@ async function loadNotificationBar() {
switch (barType) {
case "add":
case "change":
sendPlatformMessage({
command: "bgRemoveTabFromNotificationQueue",
});
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
break;
default:
break;
@@ -981,12 +969,6 @@ async function loadNotificationBar() {
// End Notification Bar Functions (open, close, height adjustment, etc.)
// Helper Functions
function sendPlatformMessage(msg: any) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
chrome.runtime.sendMessage(msg);
}
function isInIframe() {
try {
return window.self !== window.top;

View File

@@ -166,11 +166,10 @@ describe("AutofillService", () => {
jest
.spyOn(autofillService, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
});
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
[autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
@@ -195,11 +194,6 @@ describe("AutofillService", () => {
});
it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => {
jest
.spyOn(autofillService, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
@@ -218,7 +212,6 @@ describe("AutofillService", () => {
jest
.spyOn(autofillService, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.Off);
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
@@ -235,8 +228,6 @@ describe("AutofillService", () => {
});
it("injects the content-message-handler script if not injecting on page load", async () => {
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {

View File

@@ -39,10 +39,7 @@ export type UserSettings = {
vaultTimeoutAction: VaultTimeoutAction;
};
export type GlobalSettings = Pick<
GlobalState,
"disableAddLoginNotification" | "disableChangedPasswordNotification" | "neverDomains"
>;
export type GlobalSettings = Pick<GlobalState, "neverDomains">;
/**
* A HTMLElement (usually a form element) with additional custom properties added by this script

View File

@@ -55,6 +55,10 @@ import {
BadgeSettingsServiceAbstraction,
BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service";
import {
UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -89,6 +93,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import {
@@ -96,6 +101,7 @@ import {
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
@@ -103,6 +109,7 @@ import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
@@ -248,6 +255,7 @@ export default class MainBackground {
searchService: SearchServiceAbstraction;
notificationsService: NotificationsServiceAbstraction;
stateService: StateServiceAbstraction;
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
autofillSettingsService: AutofillSettingsServiceAbstraction;
badgeSettingsService: BadgeSettingsServiceAbstraction;
systemService: SystemServiceAbstraction;
@@ -294,11 +302,9 @@ export default class MainBackground {
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
vaultSettingsService: VaultSettingsServiceAbstraction;
biometricStateService: BiometricStateService;
stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
backgroundWindow = window;
onUpdatedRan: boolean;
onReplacedRan: boolean;
loginToAutoFill: CipherView = null;
@@ -361,10 +367,24 @@ export default class MainBackground {
this.keyGenerationService,
)
: new BackgroundMemoryStorageService();
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders,
const storageServiceProvider = new StorageServiceProvider(
this.storageService as BrowserLocalStorageService,
this.memoryStorageForStateProviders,
);
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const stateEventRegistrarService = new StateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);
this.encryptService = flagEnabled("multithreadDecryption")
? new MultithreadEncryptServiceImplementation(
this.cryptoFunctionService,
@@ -374,8 +394,8 @@ export default class MainBackground {
: new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true);
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.accountService = new AccountServiceImplementation(
this.messagingService,
@@ -384,8 +404,8 @@ export default class MainBackground {
);
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
this.memoryStorageForStateProviders,
this.storageService as BrowserLocalStorageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.derivedStateProvider = new BackgroundDerivedStateProvider(
this.memoryStorageForStateProviders,
@@ -419,6 +439,7 @@ export default class MainBackground {
this.environmentService,
migrationRunner,
);
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
this.platformUtilsService = new BrowserPlatformUtilsService(
this.messagingService,
(clipboardValue, clearMs) => {
@@ -660,6 +681,7 @@ export default class MainBackground {
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
lockedCallback,
logoutCallback,
);
@@ -675,7 +697,7 @@ export default class MainBackground {
this.fileUploadService,
this.sendService,
);
this.providerService = new ProviderService(this.stateService);
this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService(
this.apiService,
this.settingsService,
@@ -846,6 +868,7 @@ export default class MainBackground {
this.policyService,
this.folderService,
this.stateService,
this.userNotificationSettingsService,
this.environmentService,
this.logService,
);
@@ -1088,9 +1111,11 @@ export default class MainBackground {
this.keyConnectorService.clear(),
this.vaultFilterService.clear(),
this.biometricStateService.logout(userId),
this.providerService.save(null, userId),
/* We intentionally do not clear:
* - autofillSettingsService
* - badgeSettingsService
* - userNotificationSettingsService
*/
]);
@@ -1104,6 +1129,8 @@ export default class MainBackground {
this.searchService.clearIndex();
}
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId);
if (newActiveUser != null) {
// we have a new active user, do not continue tearing down application
await this.switchAccount(newActiveUser as UserId);

View File

@@ -21,6 +21,10 @@ import {
platformUtilsServiceFactory,
PlatformUtilsServiceInitOptions,
} from "../../platform/background/service-factories/platform-utils-service.factory";
import {
stateEventRunnerServiceFactory,
StateEventRunnerServiceInitOptions,
} from "../../platform/background/service-factories/state-event-runner-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
@@ -62,7 +66,8 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
SearchServiceInitOptions &
StateServiceInitOptions &
AuthServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions;
VaultTimeoutSettingsServiceInitOptions &
StateEventRunnerServiceInitOptions;
export function vaultTimeoutServiceFactory(
cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices,
@@ -84,6 +89,7 @@ export function vaultTimeoutServiceFactory(
await stateServiceFactory(cache, opts),
await authServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await stateEventRunnerServiceFactory(cache, opts),
opts.vaultTimeoutServiceOptions.lockedCallback,
opts.vaultTimeoutServiceOptions.loggedOutCallback,
),

View File

@@ -108,6 +108,7 @@
},
"web_accessible_resources": [
"content/fido2/page-script.js",
"content/lp-suppress-import-download.js",
"notification/bar.html",
"images/icon38.png",
"images/icon38_locked.png",

View File

@@ -31,6 +31,12 @@
"matches": ["http://*/*", "https://*/*", "file:///*"],
"run_at": "document_start"
},
{
"all_frames": false,
"js": ["content/lp-fileless-importer.js"],
"matches": ["https://lastpass.com/export.php"],
"run_at": "document_start"
},
{
"all_frames": true,
"css": ["content/autofill.css"],

View File

@@ -9,18 +9,20 @@ import {
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StateEventRegistrarServiceInitOptions,
stateEventRegistrarServiceFactory,
} from "./state-event-registrar-service.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type ActiveUserStateProviderFactory = FactoryOptions;
export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory &
AccountServiceInitOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
StorageServiceProviderInitOptions &
StateEventRegistrarServiceInitOptions;
export async function activeUserStateProviderFactory(
cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices,
@@ -33,8 +35,8 @@ export async function activeUserStateProviderFactory(
async () =>
new DefaultActiveUserStateProvider(
await accountServiceFactory(cache, opts),
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
await stateEventRegistrarServiceFactory(cache, opts),
),
);
}

View File

@@ -4,17 +4,14 @@ import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/imp
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type GlobalStateProviderFactoryOptions = FactoryOptions;
export type GlobalStateProviderInitOptions = GlobalStateProviderFactoryOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
StorageServiceProviderInitOptions;
export async function globalStateProviderFactory(
cache: { globalStateProvider?: GlobalStateProvider } & CachedServices,
@@ -24,10 +21,6 @@ export async function globalStateProviderFactory(
cache,
"globalStateProvider",
opts,
async () =>
new DefaultGlobalStateProvider(
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
),
async () => new DefaultGlobalStateProvider(await storageServiceProviderFactory(cache, opts)),
);
}

View File

@@ -4,17 +4,19 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
DiskStorageServiceInitOptions,
MemoryStorageServiceInitOptions,
observableDiskStorageServiceFactory,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StateEventRegistrarServiceInitOptions,
stateEventRegistrarServiceFactory,
} from "./state-event-registrar-service.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type SingleUserStateProviderFactoryOptions = FactoryOptions;
export type SingleUserStateProviderInitOptions = SingleUserStateProviderFactoryOptions &
MemoryStorageServiceInitOptions &
DiskStorageServiceInitOptions;
StorageServiceProviderInitOptions &
StateEventRegistrarServiceInitOptions;
export async function singleUserStateProviderFactory(
cache: { singleUserStateProvider?: SingleUserStateProvider } & CachedServices,
@@ -26,8 +28,8 @@ export async function singleUserStateProviderFactory(
opts,
async () =>
new DefaultSingleUserStateProvider(
await observableMemoryStorageServiceFactory(cache, opts),
await observableDiskStorageServiceFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
await stateEventRegistrarServiceFactory(cache, opts),
),
);
}

View File

@@ -0,0 +1,33 @@
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type StateEventRunnerServiceFactoryOptions = FactoryOptions;
export type StateEventRunnerServiceInitOptions = StateEventRunnerServiceFactoryOptions &
GlobalStateProviderInitOptions &
StorageServiceProviderInitOptions;
export function stateEventRunnerServiceFactory(
cache: { stateEventRunnerService?: StateEventRunnerService } & CachedServices,
opts: StateEventRunnerServiceInitOptions,
): Promise<StateEventRunnerService> {
return factory(
cache,
"stateEventRunnerService",
opts,
async () =>
new StateEventRunnerService(
await globalStateProviderFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
),
);
}

View File

@@ -320,6 +320,60 @@ describe("BrowserApi", () => {
},
files: [injectDetails.file],
injectImmediately: true,
world: "ISOLATED",
});
expect(result).toEqual(executeScriptResult);
});
it("injects the script into a specified frameId when the extension is built for manifest v3", async () => {
const tabId = 1;
const frameId = 2;
const injectDetails = mock<chrome.tabs.InjectDetails>({
file: "file.js",
allFrames: true,
runAt: "document_start",
frameId,
});
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
await BrowserApi.executeScriptInTab(tabId, injectDetails);
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
target: {
tabId: tabId,
allFrames: injectDetails.allFrames,
frameIds: [frameId],
},
files: [injectDetails.file],
injectImmediately: true,
world: "ISOLATED",
});
});
it("injects the script into the MAIN world context when injecting a script for manifest v3", async () => {
const tabId = 1;
const injectDetails = mock<chrome.tabs.InjectDetails>({
file: null,
allFrames: true,
runAt: "document_start",
frameId: null,
});
const scriptingApiDetails = { world: "MAIN" as chrome.scripting.ExecutionWorld };
jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3);
(chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult);
const result = await BrowserApi.executeScriptInTab(tabId, injectDetails, scriptingApiDetails);
expect(chrome.scripting.executeScript).toHaveBeenCalledWith({
target: {
tabId: tabId,
allFrames: injectDetails.allFrames,
frameIds: null,
},
files: null,
injectImmediately: true,
world: "MAIN",
});
expect(result).toEqual(executeScriptResult);
});

View File

@@ -475,12 +475,19 @@ export class BrowserApi {
/**
* Extension API helper method used to execute a script in a tab.
*
* @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript
* @param {number} tabId
* @param {chrome.tabs.InjectDetails} details
* @returns {Promise<unknown>}
* @param tabId - The id of the tab to execute the script in.
* @param details {@link "InjectDetails" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/extensionTypes/InjectDetails}
* @param scriptingApiDetails {@link "ExecutionWorld" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld}
*/
static executeScriptInTab(tabId: number, details: chrome.tabs.InjectDetails) {
static executeScriptInTab(
tabId: number,
details: chrome.tabs.InjectDetails,
scriptingApiDetails?: {
world: chrome.scripting.ExecutionWorld;
},
): Promise<unknown> {
if (BrowserApi.manifestVersion === 3) {
return chrome.scripting.executeScript({
target: {
@@ -490,6 +497,7 @@ export class BrowserApi {
},
files: details.file ? [details.file] : null,
injectImmediately: details.runAt === "document_start",
world: scriptingApiDetails?.world || "ISOLATED",
});
}

View File

@@ -15,7 +15,6 @@ import {
LoginStrategyServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
@@ -25,7 +24,6 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
@@ -42,6 +40,10 @@ import {
AutofillSettingsService,
AutofillSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -207,7 +209,6 @@ function getBgService<T>(service: keyof MainBackground) {
},
deps: [LogServiceAbstraction, I18nServiceAbstraction],
},
{ provide: AuditService, useFactory: getBgService<AuditService>("auditService"), deps: [] },
{
provide: CipherFileUploadService,
useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"),
@@ -434,11 +435,6 @@ function getBgService<T>(service: keyof MainBackground) {
AccountServiceAbstraction,
],
},
{
provide: ProviderService,
useFactory: getBgService<ProviderService>("providerService"),
deps: [],
},
{
provide: SECURE_STORAGE,
useFactory: getBgService<AbstractStorageService>("secureStorageService"),
@@ -513,13 +509,15 @@ function getBgService<T>(service: keyof MainBackground) {
stateService: StateServiceAbstraction,
platformUtilsService: PlatformUtilsService,
) => {
return new ThemingService(
stateService,
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
// In Safari we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
platformUtilsService.isSafari() ? getBgService<Window>("backgroundWindow")() : window,
document,
);
// Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light.
// In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed.
let windowContext = window;
const backgroundWindow = BrowserApi.getBackgroundPage();
if (platformUtilsService.isSafari() && backgroundWindow) {
windowContext = backgroundWindow;
}
return new ThemingService(stateService, windowContext, document);
},
deps: [StateServiceAbstraction, PlatformUtilsService],
},
@@ -551,6 +549,11 @@ function getBgService<T>(service: keyof MainBackground) {
useClass: AutofillSettingsService,
deps: [StateProvider, PolicyService],
},
{
provide: UserNotificationSettingsServiceAbstraction,
useClass: UserNotificationSettingsService,
deps: [StateProvider],
},
],
})
export class ServicesModule {}

View File

@@ -5,6 +5,7 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -47,6 +48,7 @@ export class OptionsComponent implements OnInit {
constructor(
private messagingService: MessagingService,
private stateService: StateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService,
@@ -95,10 +97,13 @@ export class OptionsComponent implements OnInit {
this.autofillSettingsService.autofillOnPageLoadDefault$,
);
this.enableAddLoginNotification = !(await this.stateService.getDisableAddLoginNotification());
this.enableAddLoginNotification = await firstValueFrom(
this.userNotificationSettingsService.enableAddedLoginPrompt$,
);
this.enableChangedPasswordNotification =
!(await this.stateService.getDisableChangedPasswordNotification());
this.enableChangedPasswordNotification = await firstValueFrom(
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
);
this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem());
@@ -122,12 +127,14 @@ export class OptionsComponent implements OnInit {
}
async updateAddLoginNotification() {
await this.stateService.setDisableAddLoginNotification(!this.enableAddLoginNotification);
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
this.enableAddLoginNotification,
);
}
async updateChangedPasswordNotification() {
await this.stateService.setDisableChangedPasswordNotification(
!this.enableChangedPasswordNotification,
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
this.enableChangedPasswordNotification,
);
}

View File

@@ -1,5 +1,10 @@
import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums";
type SuppressDownloadScriptInjectionConfig = {
file: string;
scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld };
};
type FilelessImportPortMessage = {
command?: string;
importType?: FilelessImportTypeKeys;
@@ -27,6 +32,7 @@ interface FilelessImporterBackground {
}
export {
SuppressDownloadScriptInjectionConfig,
FilelessImportPortMessage,
ImportNotificationMessageHandlers,
LpImporterMessageHandlers,

View File

@@ -1,4 +1,5 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@@ -14,10 +15,20 @@ import {
sendPortMessage,
triggerRuntimeOnConnectEvent,
} from "../../autofill/spec/testing-utils";
import { BrowserApi } from "../../platform/browser/browser-api";
import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums";
import FilelessImporterBackground from "./fileless-importer.background";
jest.mock("rxjs", () => {
const rxjs = jest.requireActual("rxjs");
const { firstValueFrom } = rxjs;
return {
...rxjs,
firstValueFrom: jest.fn(firstValueFrom),
};
});
describe("FilelessImporterBackground ", () => {
let filelessImporterBackground: FilelessImporterBackground;
const configService = mock<ConfigService>();
@@ -51,14 +62,17 @@ describe("FilelessImporterBackground ", () => {
describe("handle ports onConnect", () => {
let lpImporterPort: chrome.runtime.Port;
let manifestVersionSpy: jest.SpyInstance;
let executeScriptInTabSpy: jest.SpyInstance;
beforeEach(() => {
lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter);
manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null);
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
jest
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
.mockResolvedValue(false);
jest.spyOn(filelessImporterBackground as any, "removeIndividualVault");
(firstValueFrom as jest.Mock).mockResolvedValue(false);
});
it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => {
@@ -83,9 +97,7 @@ describe("FilelessImporterBackground ", () => {
});
it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => {
jest
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
.mockResolvedValue(true);
(firstValueFrom as jest.Mock).mockResolvedValue(true);
triggerRuntimeOnConnectEvent(lpImporterPort);
await flushPromises();
@@ -117,6 +129,35 @@ describe("FilelessImporterBackground ", () => {
filelessImportEnabled: true,
});
});
it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => {
manifestVersionSpy.mockReturnValue(3);
triggerRuntimeOnConnectEvent(lpImporterPort);
await flushPromises();
expect(executeScriptInTabSpy).toHaveBeenCalledWith(
lpImporterPort.sender.tab.id,
{ file: "content/lp-suppress-import-download.js", runAt: "document_start" },
{ world: "MAIN" },
);
});
it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => {
manifestVersionSpy.mockReturnValue(2);
triggerRuntimeOnConnectEvent(lpImporterPort);
await flushPromises();
expect(executeScriptInTabSpy).toHaveBeenCalledWith(
lpImporterPort.sender.tab.id,
{
file: "content/lp-suppress-import-download-script-append-mv2.js",
runAt: "document_start",
},
undefined,
);
});
});
describe("port messages", () => {
@@ -126,9 +167,7 @@ describe("FilelessImporterBackground ", () => {
beforeEach(async () => {
jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked);
jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true);
jest
.spyOn(filelessImporterBackground as any, "removeIndividualVault")
.mockResolvedValue(false);
(firstValueFrom as jest.Mock).mockResolvedValue(false);
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar));
triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter));
await flushPromises();

View File

@@ -11,6 +11,7 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core";
import NotificationBackground from "../../autofill/background/notification.background";
import { BrowserApi } from "../../platform/browser/browser-api";
import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts";
import {
FilelessImportPort,
FilelessImportType,
@@ -22,6 +23,7 @@ import {
LpImporterMessageHandlers,
FilelessImporterBackground as FilelessImporterBackgroundInterface,
FilelessImportPortMessage,
SuppressDownloadScriptInjectionConfig,
} from "./abstractions/fileless-importer.background";
class FilelessImporterBackground implements FilelessImporterBackgroundInterface {
@@ -108,6 +110,23 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
await this.notificationBackground.requestFilelessImport(tab, importType);
}
/**
* Injects the script used to suppress the download of the LP importer export file.
*
* @param sender - The sender of the message.
* @param injectionConfig - The configuration for the injection.
*/
private async injectScriptConfig(
sender: chrome.runtime.MessageSender,
injectionConfig: SuppressDownloadScriptInjectionConfig,
) {
await BrowserApi.executeScriptInTab(
sender.tab.id,
{ file: injectionConfig.file, runAt: "document_start" },
injectionConfig.scriptingApiDetails,
);
}
/**
* Triggers the download of the CSV file from the LP importer. This is triggered
* when the user opts to not save the export to Bitwarden within the notification bar.
@@ -200,6 +219,12 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
switch (port.name) {
case FilelessImportPort.LpImporter:
this.lpImporterPort = port;
await this.injectScriptConfig(
port.sender,
BrowserApi.manifestVersion === 3
? FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3
: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2,
);
break;
case FilelessImportPort.NotificationBar:
this.importNotificationsPort = port;

View File

@@ -0,0 +1,22 @@
import { SuppressDownloadScriptInjectionConfig } from "../background/abstractions/fileless-importer.background";
type FilelessImporterInjectedScriptsConfigurations = {
LpSuppressImportDownload: {
mv2: SuppressDownloadScriptInjectionConfig;
mv3: SuppressDownloadScriptInjectionConfig;
};
};
const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = {
LpSuppressImportDownload: {
mv2: {
file: "content/lp-suppress-import-download-script-append-mv2.js",
},
mv3: {
file: "content/lp-suppress-import-download.js",
scriptingApiDetails: { world: "MAIN" },
},
},
} as const;
export { FilelessImporterInjectedScriptsConfig };

View File

@@ -43,20 +43,6 @@ describe("LpFilelessImporter", () => {
expect(portSpy.disconnect).toHaveBeenCalled();
});
it("injects a script element that suppresses the download of the LastPass export", () => {
const script = document.createElement("script");
jest.spyOn(document, "createElement").mockReturnValue(script);
jest.spyOn(document.documentElement, "appendChild");
lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: true });
expect(document.createElement).toHaveBeenCalledWith("script");
expect(document.documentElement.appendChild).toHaveBeenCalled();
expect(script.textContent).toContain(
"const defaultAppendChild = Element.prototype.appendChild;",
);
});
it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => {
Object.defineProperty(document, "readyState", {
value: "loading",

View File

@@ -36,7 +36,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface {
return;
}
this.suppressDownload();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", this.loadImporter);
return;
@@ -52,46 +51,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface {
this.postWindowMessage({ command: "triggerCsvDownload" });
}
/**
* Suppresses the download of the CSV file by overriding the `download` attribute of the
* anchor element that is created by the LP importer. This is done by injecting a script
* into the page that overrides the `appendChild` method of the `Element` prototype.
*/
private suppressDownload() {
const script = document.createElement("script");
script.textContent = `
let csvDownload = '';
let csvHref = '';
const defaultAppendChild = Element.prototype.appendChild;
Element.prototype.appendChild = function (newChild) {
if (newChild.nodeName.toLowerCase() === 'a' && newChild.download) {
csvDownload = newChild.download;
csvHref = newChild.href;
newChild.setAttribute('href', 'javascript:void(0)');
newChild.setAttribute('download', '');
Element.prototype.appendChild = defaultAppendChild;
}
return defaultAppendChild.call(this, newChild);
};
window.addEventListener('message', (event) => {
const command = event.data?.command;
if (event.source !== window || command !== 'triggerCsvDownload') {
return;
}
const anchor = document.createElement('a');
anchor.setAttribute('href', csvHref);
anchor.setAttribute('download', csvDownload);
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
});
`;
document.documentElement.appendChild(script);
}
/**
* Initializes the importing mechanism used to import the CSV file into Bitwarden.
* This is done by observing the DOM for the addition of the LP importer element.

View File

@@ -0,0 +1,21 @@
describe("LP Suppress Import Download for Manifest v2", () => {
it("appends the `lp-suppress-import-download.js` script to the document element", () => {
let createdScriptElement: HTMLScriptElement;
jest.spyOn(window.document, "createElement");
jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => {
createdScriptElement = node as HTMLScriptElement;
return node;
});
require("./lp-suppress-import-download-script-append.mv2");
expect(window.document.createElement).toHaveBeenCalledWith("script");
expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js");
expect(window.document.documentElement.appendChild).toHaveBeenCalledWith(
expect.any(HTMLScriptElement),
);
expect(createdScriptElement.src).toBe(
"chrome-extension://id/content/lp-suppress-import-download.js",
);
});
});

View File

@@ -0,0 +1,9 @@
/**
* This script handles injection of the LP suppress import download script into the document.
* This is required for manifest v2, but will be removed when we migrate fully to manifest v3.
*/
(function (globalContext) {
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js");
globalContext.document.documentElement.appendChild(script);
})(window);

View File

@@ -0,0 +1,81 @@
import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils";
describe("LP Suppress Import Download", () => {
const downloadAttribute = "file.csv";
const hrefAttribute = "https://example.com/file.csv";
const overridenHrefAttribute = "javascript:void(0)";
let anchor: HTMLAnchorElement;
beforeEach(() => {
jest.spyOn(Element.prototype, "appendChild");
jest.spyOn(window, "addEventListener");
require("./lp-suppress-import-download");
anchor = document.createElement("a");
anchor.download = downloadAttribute;
anchor.href = hrefAttribute;
anchor.click = jest.fn();
});
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
it("disables the automatic download anchor", () => {
document.body.appendChild(anchor);
expect(anchor.href).toBe(overridenHrefAttribute);
expect(anchor.download).toBe("");
});
it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => {
window.document.createElement = jest.fn(() => anchor);
jest.spyOn(window, "removeEventListener");
document.body.appendChild(anchor);
// Precondition - Ensure the anchor in the document has overridden href and download attributes
expect(anchor.href).toBe(overridenHrefAttribute);
expect(anchor.download).toBe("");
postWindowMessage({ command: "triggerCsvDownload" });
await flushPromises();
expect(anchor.click).toHaveBeenCalled();
expect(anchor.href).toEqual(hrefAttribute);
expect(anchor.download).toEqual(downloadAttribute);
expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function));
});
it("skips subsequent calls to trigger a CSVDownload", async () => {
window.document.createElement = jest.fn(() => anchor);
document.body.appendChild(anchor);
postWindowMessage({ command: "triggerCsvDownload" });
await flushPromises();
postWindowMessage({ command: "triggerCsvDownload" });
await flushPromises();
expect(anchor.click).toHaveBeenCalledTimes(1);
});
it("skips triggering the CSV download for window messages that do not have the correct command", () => {
document.body.appendChild(anchor);
postWindowMessage({ command: "notTriggerCsvDownload" });
expect(anchor.click).not.toHaveBeenCalled();
});
it("skips triggering the CSV download for window messages that do not have a data value", () => {
document.body.appendChild(anchor);
postWindowMessage(null);
expect(anchor.click).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,50 @@
/**
* Handles intercepting the injection of the CSV download link, and ensures the
* download of the script is suppressed until the user opts to download the file.
* The download is triggered by a window message sent from the LpFilelessImporter
* content script.
*/
(function (globalContext) {
let csvDownload = "";
let csvHref = "";
let isCsvDownloadTriggered = false;
const defaultAppendChild = Element.prototype.appendChild;
Element.prototype.appendChild = function (newChild: Node) {
if (isAnchorElement(newChild) && newChild.download) {
csvDownload = newChild.download;
csvHref = newChild.href;
newChild.setAttribute("href", "javascript:void(0)");
newChild.setAttribute("download", "");
Element.prototype.appendChild = defaultAppendChild;
}
return defaultAppendChild.call(this, newChild);
};
function isAnchorElement(node: Node): node is HTMLAnchorElement {
return node.nodeName.toLowerCase() === "a";
}
const handleWindowMessage = (event: MessageEvent) => {
const command = event.data?.command;
if (
event.source !== globalContext ||
command !== "triggerCsvDownload" ||
isCsvDownloadTriggered
) {
return;
}
isCsvDownloadTriggered = true;
globalContext.removeEventListener("message", handleWindowMessage);
const anchor = globalContext.document.createElement("a");
anchor.setAttribute("href", csvHref);
anchor.setAttribute("download", csvDownload);
globalContext.document.body.appendChild(anchor);
anchor.click();
globalContext.document.body.removeChild(anchor);
};
globalContext.addEventListener("message", handleWindowMessage);
})(window);

View File

@@ -179,6 +179,7 @@ const mainConfig = {
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
"content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts",
},
optimization: {
minimize: ENV !== "development",
@@ -276,6 +277,8 @@ if (manifestVersion == 2) {
// Manifest V2 background pages can be run through the regular build pipeline.
// Since it's a standard webpage.
mainConfig.entry.background = "./src/platform/background.ts";
mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] =
"./src/tools/content/lp-suppress-import-download-script-append.mv2.ts";
configs.push(mainConfig);
} else {

View File

@@ -60,11 +60,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import {
ActiveUserStateProvider,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
@@ -73,6 +75,7 @@ import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/im
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */
import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -208,6 +211,7 @@ export class Main {
derivedStateProvider: DerivedStateProvider;
stateProvider: StateProvider;
loginStrategyService: LoginStrategyServiceAbstraction;
stateEventRunnerService: StateEventRunnerService;
biometricStateService: BiometricStateService;
constructor() {
@@ -249,14 +253,26 @@ export class Main {
this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
this.globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders,
const storageServiceProvider = new StorageServiceProvider(
this.storageService,
this.memoryStorageForStateProviders,
);
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const stateEventRegistrarService = new StateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
this.memoryStorageForStateProviders,
this.storageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.messagingService = new NoopMessagingService();
@@ -269,8 +285,8 @@ export class Main {
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
this.memoryStorageForStateProviders,
this.storageService,
storageServiceProvider,
stateEventRegistrarService,
);
this.derivedStateProvider = new DefaultDerivedStateProvider(
@@ -372,7 +388,7 @@ export class Main {
this.stateProvider,
);
this.providerService = new ProviderService(this.stateService);
this.providerService = new ProviderService(this.stateProvider);
this.organizationService = new OrganizationService(this.stateService, this.stateProvider);
@@ -530,6 +546,7 @@ export class Main {
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
lockedCallback,
null,
);
@@ -638,7 +655,11 @@ export class Main {
this.collectionService.clear(userId as UserId),
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
this.providerService.save(null, userId as UserId),
]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
await this.stateService.clean();
process.env.BW_SESSION = null;
}

View File

@@ -23,6 +23,7 @@ import { SettingsService } from "@bitwarden/common/abstractions/settings.service
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@@ -39,6 +40,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -149,6 +151,8 @@ export class AppComponent implements OnInit, OnDestroy {
private configService: ConfigServiceAbstraction,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private providerService: ProviderService,
) {}
ngOnInit() {
@@ -219,13 +223,13 @@ export class AppComponent implements OnInit, OnDestroy {
const currentUser = await this.stateService.getUserId();
const accounts = await firstValueFrom(this.stateService.accounts$);
await this.vaultTimeoutService.lock(currentUser);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Promise.all(
Object.keys(accounts)
.filter((u) => u !== currentUser)
.map((u) => this.vaultTimeoutService.lock(u)),
);
for (const account of Object.keys(accounts)) {
if (account === currentUser) {
continue;
}
await this.vaultTimeoutService.lock(account);
}
break;
}
case "locked":
@@ -582,6 +586,9 @@ export class AppComponent implements OnInit, OnDestroy {
await this.policyService.clear(userBeingLoggedOut);
await this.keyConnectorService.clear();
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
await this.providerService.save(null, userBeingLoggedOut as UserId);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut });

View File

@@ -13,11 +13,13 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */
@@ -104,10 +106,11 @@ export class Main {
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
const globalStateProvider = new DefaultGlobalStateProvider(
this.memoryStorageForStateProviders,
const storageServiceProvider = new StorageServiceProvider(
this.storageService,
this.memoryStorageForStateProviders,
);
const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
const accountService = new AccountServiceImplementation(
new NoopMessagingService(),
@@ -115,13 +118,18 @@ export class Main {
globalStateProvider,
);
const stateEventRegistrarService = new StateEventRegistrarService(
globalStateProvider,
storageServiceProvider,
);
const stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider(
accountService,
this.memoryStorageForStateProviders,
this.storageService,
storageServiceProvider,
stateEventRegistrarService,
),
new DefaultSingleUserStateProvider(this.memoryStorageForStateProviders, this.storageService),
new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
globalStateProvider,
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
);

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2024.2.3",
"version": "2024.2.4",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -8,6 +8,7 @@ import {
canAccessBillingTab,
canAccessGroupsTab,
canAccessMembersTab,
canAccessOrgAdmin,
canAccessReportingTab,
canAccessSettingsTab,
canAccessVaultTab,
@@ -43,7 +44,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
protected readonly logo = AdminConsoleLogo;
protected orgFilter = (org: Organization) => org.isAdmin;
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
organization$: Observable<Organization>;
showPaymentAndHistory$: Observable<boolean>;

View File

@@ -23,6 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -89,6 +90,7 @@ export class AppComponent implements OnDestroy, OnInit {
private configService: ConfigServiceAbstraction,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private paymentMethodWarningService: PaymentMethodWarningService,
private organizationService: OrganizationService,
) {}
@@ -284,6 +286,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.paymentMethodWarningService.clear(),
]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
this.searchService.clearIndex();
this.authService.logOut(async () => {
if (expired) {

View File

@@ -14,7 +14,6 @@ import {
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@@ -28,21 +27,16 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import {
ActiveUserStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
} from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
import { PolicyListService } from "../admin-console/core/policy-list.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
import { WebActiveUserStateProvider } from "../platform/web-active-user-state.provider";
import { WebGlobalStateProvider } from "../platform/web-global-state.provider";
import { WebMigrationRunner } from "../platform/web-migration-runner";
import { WebSingleUserStateProvider } from "../platform/web-single-user-state.provider";
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
import { WindowStorageService } from "../platform/window-storage.service";
import { CollectionAdminService } from "../vault/core/collection-admin.service";
@@ -124,24 +118,9 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
useFactory: () => new WindowStorageService(window.localStorage),
},
{
provide: SingleUserStateProvider,
useClass: WebSingleUserStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
},
{
provide: ActiveUserStateProvider,
useClass: WebActiveUserStateProvider,
deps: [
AccountService,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_DISK_LOCAL_STORAGE,
],
},
{
provide: GlobalStateProvider,
useClass: WebGlobalStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
provide: StorageServiceProvider,
useClass: WebStorageServiceProvider,
deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE],
},
{
provide: MigrationRunner,

View File

@@ -58,9 +58,9 @@ export class ProductSwitcherContentComponent {
// If the active route org doesn't have access to AC, find the first org that does.
const acOrg =
routeOrg != null && canAccessOrgAdmin(routeOrg) && routeOrg.enabled
routeOrg != null && canAccessOrgAdmin(routeOrg)
? routeOrg
: orgs.find((o) => canAccessOrgAdmin(o) && o.enabled);
: orgs.find((o) => canAccessOrgAdmin(o));
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
const providers = await this.providerService.getAll();

View File

@@ -1,44 +0,0 @@
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebActiveUserStateProvider extends DefaultActiveUserStateProvider {
constructor(
accountService: AccountService,
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
sessionStorage: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
) {
super(accountService, memoryStorage, sessionStorage);
}
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorage;
}
}
}

View File

@@ -1,42 +0,0 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { KeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code*/
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebGlobalStateProvider extends DefaultGlobalStateProvider {
constructor(
memoryStorage: AbstractMemoryStorageService & ObservableStorageService,
sessionStorage: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorage: AbstractStorageService & ObservableStorageService,
) {
super(memoryStorage, sessionStorage);
}
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorage;
}
}
}

View File

@@ -1,43 +0,0 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- Needed to extend service & and in platform owned file */
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
/* eslint-enable import/no-restricted-paths */
export class WebSingleUserStateProvider extends DefaultSingleUserStateProvider {
constructor(
memoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
sessionStorageService: AbstractStorageService & ObservableStorageService,
private readonly diskLocalStorageService: AbstractStorageService & ObservableStorageService,
) {
super(memoryStorageService, sessionStorageService);
}
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
return (
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
keyDefinition.stateDefinition.defaultStorageLocation
);
}
protected override getLocation(
stateDefinition: StateDefinition,
): AbstractStorageService & ObservableStorageService {
const location =
stateDefinition.storageLocationOverrides["web"] ?? stateDefinition.defaultStorageLocation;
switch (location) {
case "disk":
return this.diskStorage;
case "memory":
return this.memoryStorage;
case "disk-local":
return this.diskLocalStorageService;
}
}
}