1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[PM-5979] Refactor EnvironmentService (#8040)

Refactor environment service to emit a single observable. This required significant changes to how the environment service behaves and tackles much of the tech debt planned for it.
This commit is contained in:
Oscar Hinton
2024-03-21 17:09:44 +01:00
committed by GitHub
parent 7a42b4ebc6
commit e767295c86
88 changed files with 1710 additions and 1379 deletions

View File

@@ -2386,12 +2386,6 @@
"message": "EU",
"description": "European Union"
},
"usDomain": {
"message": "bitwarden.com"
},
"euDomain": {
"message": "bitwarden.eu"
},
"accessDenied": {
"message": "Access denied. You do not have permission to view this page."
},

View File

@@ -65,7 +65,7 @@ export class AccountSwitcherService {
name: account.name ?? account.email,
email: account.email,
id: id,
server: await this.environmentService.getHost(id),
server: (await this.environmentService.getEnvironment(id))?.getHostname(),
status: account.status,
isActive: id === activeAccount?.id,
avatarColor: await firstValueFrom(

View File

@@ -94,10 +94,6 @@ export class HomeComponent implements OnInit, OnDestroy {
this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
}
get selfHostedDomain() {
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
}
setFormValues() {
this.loginService.setEmail(this.formGroup.value.email);
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);

View File

@@ -1,6 +1,7 @@
import { Component, NgZone } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
@@ -114,7 +115,8 @@ export class LoginComponent extends BaseLoginComponent {
await this.ssoLoginService.setCodeVerifier(codeVerifier);
await this.ssoLoginService.setSsoState(state);
let url = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
let url = env.getWebVaultUrl();
if (url == null) {
url = "https://vault.bitwarden.com";
}

View File

@@ -1,4 +1,5 @@
import { Component, Inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
@@ -64,9 +65,9 @@ export class SsoComponent extends BaseSsoComponent {
configService,
);
const url = this.environmentService.getWebVaultUrl();
this.redirectUri = url + "/sso-connector.html";
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
});
this.clientId = "browser";
super.onSuccessfulLogin = async () => {

View File

@@ -1,6 +1,6 @@
import { Component, Inject } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, Subscription } from "rxjs";
import { Subject, Subscription, firstValueFrom } from "rxjs";
import { filter, first, takeUntil } from "rxjs/operators";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
@@ -225,7 +225,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
}
}
override launchDuoFrameless() {
override async launchDuoFrameless() {
const duoHandOffMessage = {
title: this.i18nService.t("youSuccessfullyLoggedIn"),
message: this.i18nService.t("youMayCloseThisWindow"),
@@ -234,8 +234,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
// we're using the connector here as a way to set a cookie with translations
// before continuing to the duo frameless url
const env = await firstValueFrom(this.environmentService.environment$);
const launchUrl =
this.environmentService.getWebVaultUrl() +
env.getWebVaultUrl() +
"/duo-redirect-connector.html" +
"?duoFramelessUrl=" +
encodeURIComponent(this.duoFramelessUrl) +

View File

@@ -113,7 +113,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
bgGetExcludedDomains: () => Promise<NeverDomains>;
getWebVaultUrlForNotification: () => string;
getWebVaultUrlForNotification: () => Promise<string>;
};
export {

View File

@@ -1,13 +1,14 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { BehaviorSubject, 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 { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } 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 { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@@ -1348,16 +1349,21 @@ describe("NotificationBackground", () => {
const message: NotificationBackgroundExtensionMessage = {
command: "getWebVaultUrlForNotification",
};
const webVaultUrl = "https://example.com";
const env = new SelfHostedEnvironment({ webVault: "https://example.com" });
Object.defineProperty(environmentService, "environment$", {
configurable: true,
get: () => null,
});
const environmentServiceSpy = jest
.spyOn(environmentService, "getWebVaultUrl")
.mockReturnValueOnce(webVaultUrl);
.spyOn(environmentService as any, "environment$", "get")
.mockReturnValue(new BehaviorSubject(env).asObservable());
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(environmentServiceSpy).toHaveBeenCalled();
expect(environmentServiceSpy).toHaveReturnedWith(webVaultUrl);
});
});
});

View File

@@ -165,6 +165,7 @@ export default class NotificationBackground {
notificationQueueMessage: NotificationQueueMessageItem,
) {
const notificationType = notificationQueueMessage.type;
const typeData: Record<string, any> = {
isVaultLocked: notificationQueueMessage.wasVaultLocked,
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
@@ -655,8 +656,9 @@ export default class NotificationBackground {
return await firstValueFrom(this.folderService.folderViews$);
}
private getWebVaultUrl(): string {
return this.environmentService.getWebVaultUrl();
private async getWebVaultUrl(): Promise<string> {
const env = await firstValueFrom(this.environmentService.environment$);
return env.getWebVaultUrl();
}
private async removeIndividualVault(): Promise<boolean> {

View File

@@ -1,5 +1,5 @@
import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
@@ -12,9 +12,13 @@ import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import {
EnvironmentService,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import {
@@ -48,8 +52,6 @@ import {
import OverlayBackground from "./overlay.background";
const iconServerUrl = "https://icons.bitwarden.com/";
describe("OverlayBackground", () => {
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@@ -61,9 +63,15 @@ describe("OverlayBackground", () => {
const cipherService = mock<CipherService>();
const autofillService = mock<AutofillService>();
const authService = mock<AuthService>();
const environmentService = mock<EnvironmentService>({
getIconsUrl: () => iconServerUrl,
});
const environmentService = mock<EnvironmentService>();
environmentService.environment$ = new BehaviorSubject(
new CloudEnvironment({
key: Region.US,
domain: "bitwarden.com",
urls: { icons: "https://icons.bitwarden.com/" },
}),
);
const stateService = mock<BrowserStateService>();
const autofillSettingsService = mock<AutofillSettingsService>();
const i18nService = mock<I18nService>();

View File

@@ -53,7 +53,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private overlayListPort: chrome.runtime.Port;
private focusedFieldData: FocusedFieldData;
private overlayPageTranslations: Record<string, string>;
private readonly iconsServerUrl: string;
private iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
openAutofillOverlay: () => this.openOverlay(false),
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
@@ -98,9 +98,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private themeStateService: ThemeStateService,
) {
this.iconsServerUrl = this.environmentService.getIconsUrl();
}
) {}
/**
* Removes cached page details for a tab
@@ -118,6 +116,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
*/
async init() {
this.setupExtensionMessageListeners();
const env = await firstValueFrom(this.environmentService.environment$);
this.iconsServerUrl = env.getIconsUrl();
await this.getOverlayVisibility();
await this.getAuthStatus();
}

View File

@@ -613,6 +613,7 @@ export default class MainBackground {
this.authService,
this.environmentService,
this.logService,
this.stateProvider,
true,
);
@@ -1032,10 +1033,6 @@ export default class MainBackground {
return new Promise<void>((resolve) => {
setTimeout(async () => {
await this.environmentService.setUrlsFromStorage();
// Workaround to ignore stateService.activeAccount until URLs are set
// TODO: Remove this when implementing ticket PM-2637
this.environmentService.initialized = true;
if (!this.isPrivateMode) {
await this.refreshBadge();
}

View File

@@ -1,3 +1,5 @@
import { firstValueFrom } from "rxjs";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
@@ -220,7 +222,8 @@ export default class RuntimeBackground {
}
break;
case "authResult": {
const vaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
return;
@@ -241,7 +244,8 @@ export default class RuntimeBackground {
break;
}
case "webAuthnResult": {
const vaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
return;
@@ -364,7 +368,8 @@ export default class RuntimeBackground {
async sendBwInstalledMessageToVault() {
try {
const vaultUrl = this.environmentService.getWebVaultUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
const urlObj = new URL(vaultUrl);
const tabs = await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });

View File

@@ -13,6 +13,7 @@ import {
} from "./environment-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { stateProviderFactory } from "./state-provider.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type ConfigServiceFactoryOptions = FactoryOptions & {
@@ -43,6 +44,7 @@ export function configServiceFactory(
await authServiceFactory(cache, opts),
await environmentServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
opts.configServiceOptions?.subscribe ?? true,
),
);

View File

@@ -7,6 +7,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
@@ -21,8 +22,17 @@ export class BrowserConfigService extends ConfigService {
authService: AuthService,
environmentService: EnvironmentService,
logService: LogService,
stateProvider: StateProvider,
subscribe = false,
) {
super(stateService, configApiService, authService, environmentService, logService, subscribe);
super(
stateService,
configApiService,
authService,
environmentService,
logService,
stateProvider,
subscribe,
);
}
}

View File

@@ -1,12 +1,15 @@
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GroupPolicyEnvironment } from "../../admin-console/types/group-policy-environment";
import { devFlagEnabled, devFlagValue } from "../flags";
export class BrowserEnvironmentService extends EnvironmentService {
export class BrowserEnvironmentService extends DefaultEnvironmentService {
constructor(
private logService: LogService,
stateProvider: StateProvider,
@@ -29,16 +32,18 @@ export class BrowserEnvironmentService extends EnvironmentService {
return false;
}
const env = await this.getManagedEnvironment();
const managedEnv = await this.getManagedEnvironment();
const env = await firstValueFrom(this.environment$);
const urls = env.getUrls();
return (
env.base != this.baseUrl ||
env.webVault != this.webVaultUrl ||
env.api != this.webVaultUrl ||
env.identity != this.identityUrl ||
env.icons != this.iconsUrl ||
env.notifications != this.notificationsUrl ||
env.events != this.eventsUrl
managedEnv.base != urls.base ||
managedEnv.webVault != urls.webVault ||
managedEnv.api != urls.api ||
managedEnv.identity != urls.identity ||
managedEnv.icons != urls.icons ||
managedEnv.notifications != urls.notifications ||
managedEnv.events != urls.events
);
}
@@ -62,7 +67,7 @@ export class BrowserEnvironmentService extends EnvironmentService {
async setUrlsToManagedEnvironment() {
const env = await this.getManagedEnvironment();
await this.setUrls({
await this.setEnvironment(Region.SelfHosted, {
base: env.base,
webVault: env.webVault,
api: env.api,

View File

@@ -222,12 +222,12 @@ function getBgService<T>(service: keyof MainBackground) {
},
{
provide: BrowserEnvironmentService,
useExisting: EnvironmentService,
useClass: BrowserEnvironmentService,
deps: [LogService, StateProvider, AccountServiceAbstraction],
},
{
provide: EnvironmentService,
useFactory: getBgService<EnvironmentService>("environmentService"),
deps: [],
useExisting: BrowserEnvironmentService,
},
{ provide: TotpService, useFactory: getBgService<TotpService>("totpService"), deps: [] },
{
@@ -480,6 +480,7 @@ function getBgService<T>(service: keyof MainBackground) {
ConfigApiServiceAbstraction,
AuthServiceAbstraction,
EnvironmentService,
StateProvider,
LogService,
],
},

View File

@@ -6,33 +6,33 @@
<div bitDialogContent>
<p>&copy; Bitwarden Inc. 2015-{{ year }}</p>
<p>{{ "version" | i18n }}: {{ version }}</p>
<ng-container *ngIf="serverConfig$ | async as serverConfig">
<p *ngIf="isCloud">
{{ "serverVersion" | i18n }}: {{ this.serverConfig?.version }}
<span *ngIf="!serverConfig.isValid()">
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
<ng-container *ngIf="data$ | async as data">
<p *ngIf="data.isCloud">
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()">
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
</span>
</p>
<ng-container *ngIf="!isCloud">
<ng-container *ngIf="serverConfig.server">
<ng-container *ngIf="!data.isCloud">
<ng-container *ngIf="data.serverConfig.server">
<p>
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
{{ this.serverConfig?.version }}
<span *ngIf="!serverConfig.isValid()">
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
{{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()">
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
</span>
</p>
<div>
<small>{{ "thirdPartyServerMessage" | i18n: serverConfig.server?.name }}</small>
<small>{{ "thirdPartyServerMessage" | i18n: data.serverConfig.server?.name }}</small>
</div>
</ng-container>
<p *ngIf="!serverConfig.server">
<p *ngIf="!data.serverConfig.server">
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
{{ this.serverConfig?.version }}
<span *ngIf="!serverConfig.isValid()">
({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})
{{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()">
({{ "lastSeenOn" | i18n: (data.serverConfig.utcDate | date: "mediumDate") }})
</span>
</p>
</ng-container>

View File

@@ -1,10 +1,9 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Observable } from "rxjs";
import { combineLatest, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { ButtonModule, DialogModule } from "@bitwarden/components";
@@ -16,11 +15,13 @@ import { BrowserApi } from "../../platform/browser/browser-api";
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
})
export class AboutComponent {
protected serverConfig$: Observable<ServerConfig> = this.configService.serverConfig$;
protected year = new Date().getFullYear();
protected version = BrowserApi.getApplicationVersion();
protected isCloud = this.environmentService.isCloud();
protected data$ = combineLatest([
this.configService.serverConfig$,
this.environmentService.environment$.pipe(map((env) => env.isCloud())),
]).pipe(map(([serverConfig, isCloud]) => ({ serverConfig, isCloud })));
constructor(
private configService: ConfigServiceAbstraction,

View File

@@ -446,9 +446,8 @@ export class SettingsComponent implements OnInit {
type: "info",
});
if (confirmed) {
// 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
BrowserApi.createNewTab(this.environmentService.getWebVaultUrl());
const env = await firstValueFrom(this.environmentService.environment$);
await BrowserApi.createNewTab(env.getWebVaultUrl());
}
}
@@ -479,10 +478,9 @@ export class SettingsComponent implements OnInit {
}
async webVault() {
const url = this.environmentService.getWebVaultUrl();
// 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
BrowserApi.createNewTab(url);
const env = await firstValueFrom(this.environmentService.environment$);
const url = env.getWebVaultUrl();
await BrowserApi.createNewTab(url);
}
async import() {