1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +01:00

Auth/PM-13114 - WebEnvService Refactor + Unit Tests to support QA Env Selector (#11397)

* PM-13114 - WebEnvSvc - use hostname vs domain check for init and setEnv (tests TODO)

* PM-13114 - WebEnvSvc + URLs webpack config - use expected string variable on process.env.URLS to ensure tests can properly mock the WebEnvSvc

* PM-13114 - WebEnvSvc - setEnvironment - fix issue with returning currentRegion urls instead of currentEnv urls.

* PM-13114 - WebEnvSvc - setEnv - refactor names to improve clarity.

* PM-13114 - WebEnvSvc spec file - Test all prod scenarios

* PM-13144 - Work with Justin to move process.env.Urls access into injection token and remove webpack string type conversion.

* PM-13114 - WIP on getting additionalRegionConfigs injected via injection token to default env service.

* PM-13114 - Update all background inits to pass process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[] to env service.

* PM-13114 - WebEnvSvc - adjust order of constructor deps

* PM-13114 - WebEnvSvc - add WebRegionConfig to extend RegionConfig type and be accurate for what the WebEnvSvc uses.

* PM-13114 - WebEnvSvc Tests - US QA tested

* PM-13114 - WebEnvSvc tests - refactor QA naming to make it more clear.

* PM-13114 - WebEnvSvc - test QA EU

* PM-13114 - WebEnvSvc - remove promise resolve per PR feedback.
This commit is contained in:
Jared Snider 2024-10-04 14:57:40 -04:00 committed by GitHub
parent e6ff647343
commit 87cb45c520
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 560 additions and 33 deletions

View File

@ -80,6 +80,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
@ -570,6 +571,7 @@ export default class MainBackground {
this.logService,
this.stateProvider,
this.accountService,
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
);
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);

View File

@ -1,7 +1,7 @@
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { Region, RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { StateProvider } from "@bitwarden/common/platform/state";
@ -14,8 +14,9 @@ export class BrowserEnvironmentService extends DefaultEnvironmentService {
private logService: LogService,
stateProvider: StateProvider,
accountService: AccountService,
additionalRegionConfigs: RegionConfig[] = [],
) {
super(stateProvider, accountService);
super(stateProvider, accountService, additionalRegionConfigs);
}
async hasManagedEnvironment(): Promise<boolean> {

View File

@ -14,6 +14,7 @@ import {
DEFAULT_VAULT_TIMEOUT,
INTRAPROCESS_MESSAGING_SUBJECT,
CLIENT_TYPE,
ENV_ADDITIONAL_REGIONS,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular";
@ -197,7 +198,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: BrowserEnvironmentService,
useClass: BrowserEnvironmentService,
deps: [LogService, StateProvider, AccountServiceAbstraction],
deps: [LogService, StateProvider, AccountServiceAbstraction, ENV_ADDITIONAL_REGIONS],
}),
safeProvider({
provide: I18nServiceAbstraction,

View File

@ -59,7 +59,10 @@ import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/bill
import { ClientType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
EnvironmentService,
RegionConfig,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
@ -346,6 +349,7 @@ export class ServiceContainer {
this.environmentService = new DefaultEnvironmentService(
this.stateProvider,
this.accountService,
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
);
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);

View File

@ -5,6 +5,7 @@ import { Subject, firstValueFrom } from "rxjs";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { ClientType } from "@bitwarden/common/enums";
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- For dependency creation
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
@ -152,7 +153,11 @@ export class Main {
new DefaultDerivedStateProvider(),
);
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
this.environmentService = new DefaultEnvironmentService(
stateProvider,
accountService,
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
);
this.migrationRunner = new MigrationRunner(
this.storageService,

View File

@ -11,6 +11,7 @@ import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/sa
import {
CLIENT_TYPE,
DEFAULT_VAULT_TIMEOUT,
ENV_ADDITIONAL_REGIONS,
LOCALES_DIRECTORY,
MEMORY_STORAGE,
OBSERVABLE_DISK_LOCAL_STORAGE,
@ -42,7 +43,10 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
EnvironmentService,
Urls,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -84,6 +88,7 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi
import { EventService } from "./event.service";
import { InitService } from "./init.service";
import { ENV_URLS } from "./injection-tokens";
import { ModalService } from "./modal.service";
import { RouterService } from "./router.service";
import { WebFileDownloadService } from "./web-file-download.service";
@ -173,10 +178,14 @@ const safeProviders: SafeProvider[] = [
useClass: WebMigrationRunner,
deps: [AbstractStorageService, LogService, MigrationBuilderService, WindowStorageService],
}),
safeProvider({
provide: ENV_URLS,
useValue: process.env.URLS as Urls,
}),
safeProvider({
provide: EnvironmentService,
useClass: WebEnvironmentService,
deps: [WINDOW, StateProvider, AccountService, Router],
deps: [WINDOW, StateProvider, AccountService, ENV_ADDITIONAL_REGIONS, Router, ENV_URLS],
}),
safeProvider({
provide: BiometricsService,

View File

@ -0,0 +1,10 @@
// Put web specific injection tokens here
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
import { Urls } from "@bitwarden/common/platform/abstractions/environment.service";
/**
* Injection token for injecting the NodeJS process.env urls into services.
* Using an injection token allows services to be tested without needing to
* mock the process.env.
*/
export const ENV_URLS = new SafeInjectionToken<Urls>("ENV_URLS");

View File

@ -0,0 +1,457 @@
import { Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { Region, Urls } from "@bitwarden/common/platform/abstractions/environment.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PRODUCTION_REGIONS } from "@bitwarden/common/platform/services/default-environment.service";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import {
WebCloudEnvironment,
WebEnvironmentService,
WebRegionConfig,
} from "./web-environment.service";
describe("WebEnvironmentService", () => {
let service: WebEnvironmentService;
let window: MockProxy<Window>;
let stateProvider: FakeStateProvider;
let accountService: FakeAccountService;
let router: MockProxy<Router>;
const mockUserId = Utils.newGuid() as UserId;
describe("Production Environment", () => {
describe("US Region", () => {
const mockInitialProdUSUrls = {
base: null,
api: "https://api.bitwarden.com",
identity: "https://identity.bitwarden.com",
icons: "https://icons.bitwarden.net",
webVault: "https://vault.bitwarden.com",
notifications: "https://notifications.bitwarden.com",
events: "https://events.bitwarden.com",
scim: "https://scim.bitwarden.com",
} as Urls;
const mockProdUSBaseUrl = "https://vault.bitwarden.com";
const expectedProdUSUrls: Urls = {
...mockInitialProdUSUrls,
base: mockProdUSBaseUrl,
};
const expectedModifiedScimUrl = expectedProdUSUrls.scim + "/v2";
const expectedSendUrl = "https://send.bitwarden.com/#";
const PROD_US_REGION = PRODUCTION_REGIONS.find((r) => r.key === Region.US);
const prodUSEnv = new WebCloudEnvironment(PROD_US_REGION, expectedProdUSUrls);
beforeEach(() => {
window = mock<Window>();
window.location = {
origin: mockProdUSBaseUrl,
href: mockProdUSBaseUrl + "/#/example",
} as Location;
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
router = mock<Router>();
(router as any).url = "";
service = new WebEnvironmentService(
window,
stateProvider,
accountService,
[], // no additional region configs required for prod envs
router,
mockInitialProdUSUrls,
);
});
it("initializes the environment with the US production urls", async () => {
const env = await firstValueFrom(service.environment$);
expect(env).toEqual(prodUSEnv);
expect(env.getRegion()).toEqual(Region.US);
expect(env.getUrls()).toEqual(expectedProdUSUrls);
expect(env.isCloud()).toBeTruthy();
expect(env.getApiUrl()).toEqual(expectedProdUSUrls.api);
expect(env.getIdentityUrl()).toEqual(expectedProdUSUrls.identity);
expect(env.getIconsUrl()).toEqual(expectedProdUSUrls.icons);
expect(env.getWebVaultUrl()).toEqual(expectedProdUSUrls.webVault);
expect(env.getNotificationsUrl()).toEqual(expectedProdUSUrls.notifications);
expect(env.getEventsUrl()).toEqual(expectedProdUSUrls.events);
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
expect(env.getSendUrl()).toEqual(expectedSendUrl);
expect(env.getHostname()).toEqual(PROD_US_REGION.domain);
});
describe("setEnvironment", () => {
it("throws an error when trying to set the environment to self-hosted", async () => {
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
"setEnvironment does not work in web for self-hosted.",
);
});
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
const urls = await service.setEnvironment(Region.US);
expect(urls).toEqual(expectedProdUSUrls);
});
it("errors if the selected region is unknown", async () => {
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
"The selected region is not known as an available region.",
);
});
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
const routeAndQueryParams = "/signup?example=1&another=2";
(router as any).url = routeAndQueryParams;
const newRegion = Region.EU;
const newRegionConfig = PRODUCTION_REGIONS.find((r) => r.key === newRegion);
await service.setEnvironment(newRegion);
expect(window.location.href).toEqual(
newRegionConfig.urls.webVault + "/#" + routeAndQueryParams,
);
});
});
});
describe("EU Region", () => {
const mockInitialProdEUUrls = {
base: null,
api: "https://api.bitwarden.eu",
identity: "https://identity.bitwarden.eu",
icons: "https://icons.bitwarden.eu",
webVault: "https://vault.bitwarden.eu",
notifications: "https://notifications.bitwarden.eu",
events: "https://events.bitwarden.eu",
scim: "https://scim.bitwarden.eu",
} as Urls;
const mockProdEUBaseUrl = "https://vault.bitwarden.eu";
const expectedProdEUUrls: Urls = {
...mockInitialProdEUUrls,
base: mockProdEUBaseUrl,
};
const expectedModifiedScimUrl = expectedProdEUUrls.scim + "/v2";
const expectedSendUrl = expectedProdEUUrls.webVault + "/#/send/";
const prodEURegionConfig = PRODUCTION_REGIONS.find((r) => r.key === Region.EU);
const prodEUEnv = new WebCloudEnvironment(prodEURegionConfig, expectedProdEUUrls);
beforeEach(() => {
window = mock<Window>();
window.location = {
origin: mockProdEUBaseUrl,
href: mockProdEUBaseUrl + "/#/example",
} as Location;
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
router = mock<Router>();
service = new WebEnvironmentService(
window,
stateProvider,
accountService,
[], // no additional region configs required for prod envs
router,
mockInitialProdEUUrls,
);
});
it("initializes the environment to be the prod EU environment", async () => {
const env = await firstValueFrom(service.environment$);
expect(env).toEqual(prodEUEnv);
expect(env.getRegion()).toEqual(Region.EU);
expect(env.getUrls()).toEqual(expectedProdEUUrls);
expect(env.isCloud()).toBeTruthy();
expect(env.getApiUrl()).toEqual(expectedProdEUUrls.api);
expect(env.getIdentityUrl()).toEqual(expectedProdEUUrls.identity);
expect(env.getIconsUrl()).toEqual(expectedProdEUUrls.icons);
expect(env.getWebVaultUrl()).toEqual(expectedProdEUUrls.webVault);
expect(env.getNotificationsUrl()).toEqual(expectedProdEUUrls.notifications);
expect(env.getEventsUrl()).toEqual(expectedProdEUUrls.events);
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
expect(env.getSendUrl()).toEqual(expectedSendUrl);
expect(env.getHostname()).toEqual(prodEURegionConfig.domain);
});
describe("setEnvironment", () => {
it("throws an error when trying to set the environment to self-hosted", async () => {
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
"setEnvironment does not work in web for self-hosted.",
);
});
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
const urls = await service.setEnvironment(Region.EU);
expect(urls).toEqual(expectedProdEUUrls);
});
it("errors if the selected region is unknown", async () => {
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
"The selected region is not known as an available region.",
);
});
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
const routeAndQueryParams = "/signup?example=1&another=2";
(router as any).url = routeAndQueryParams;
const newRegion = Region.US;
const newRegionConfig = PRODUCTION_REGIONS.find((r) => r.key === newRegion);
await service.setEnvironment(newRegion);
expect(window.location.href).toEqual(
newRegionConfig.urls.webVault + "/#" + routeAndQueryParams,
);
});
});
});
});
describe("QA Environment", () => {
const QA_US_REGION_KEY = "USQA";
const QA_US_WEB_REGION_CONFIG = {
key: QA_US_REGION_KEY,
domain: "qa.bitwarden.pw",
urls: {
webVault: "https://vault.qa.bitwarden.pw",
},
} as WebRegionConfig;
const QA_EU_REGION_KEY = "EUQA";
const QA_EU_WEB_REGION_CONFIG = {
key: QA_EU_REGION_KEY,
domain: "euqa.bitwarden.pw",
urls: {
webVault: "https://vault.euqa.bitwarden.pw",
},
} as WebRegionConfig;
const additionalRegionConfigs: WebRegionConfig[] = [
QA_US_WEB_REGION_CONFIG,
QA_EU_WEB_REGION_CONFIG,
];
describe("US Region", () => {
const initial_QA_US_Urls = {
icons: "https://icons.qa.bitwarden.pw",
notifications: "https://notifications.qa.bitwarden.pw",
scim: "https://scim.qa.bitwarden.pw",
} as Urls;
const mock_QA_US_BaseUrl = "https://vault.qa.bitwarden.pw";
const expected_QA_US_Urls: Urls = {
...initial_QA_US_Urls,
base: mock_QA_US_BaseUrl,
};
const expectedModifiedScimUrl = expected_QA_US_Urls.scim + "/v2";
const expectedSendUrl = QA_US_WEB_REGION_CONFIG.urls.webVault + "/#/send/";
const QA_US_Env = new WebCloudEnvironment(QA_US_WEB_REGION_CONFIG, expected_QA_US_Urls);
beforeEach(() => {
window = mock<Window>();
window.location = {
origin: mock_QA_US_BaseUrl,
href: mock_QA_US_BaseUrl + "/#/example",
} as Location;
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
router = mock<Router>();
(router as any).url = "";
service = new WebEnvironmentService(
window,
stateProvider,
accountService,
additionalRegionConfigs,
router,
initial_QA_US_Urls,
);
});
it("initializes the environment to be the QA US environment", async () => {
const env = await firstValueFrom(service.environment$);
expect(env).toEqual(QA_US_Env);
expect(env.getRegion()).toEqual(QA_US_REGION_KEY);
expect(env.getUrls()).toEqual(expected_QA_US_Urls);
expect(env.isCloud()).toBeTruthy();
expect(env.getApiUrl()).toEqual(expected_QA_US_Urls.base + "/api");
expect(env.getIdentityUrl()).toEqual(expected_QA_US_Urls.base + "/identity");
expect(env.getIconsUrl()).toEqual(expected_QA_US_Urls.icons);
expect(env.getWebVaultUrl()).toEqual(QA_US_WEB_REGION_CONFIG.urls.webVault);
expect(env.getNotificationsUrl()).toEqual(expected_QA_US_Urls.notifications);
expect(env.getEventsUrl()).toEqual(expected_QA_US_Urls.base + "/events");
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
expect(env.getSendUrl()).toEqual(expectedSendUrl);
expect(env.getHostname()).toEqual(QA_US_WEB_REGION_CONFIG.domain);
});
describe("setEnvironment", () => {
it("throws an error when trying to set the environment to self-hosted", async () => {
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
"setEnvironment does not work in web for self-hosted.",
);
});
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
const urls = await service.setEnvironment(QA_US_REGION_KEY);
expect(urls).toEqual(expected_QA_US_Urls);
});
it("errors if the selected region is unknown", async () => {
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
"The selected region is not known as an available region.",
);
});
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
const routeAndQueryParams = "/signup?example=1&another=2";
(router as any).url = routeAndQueryParams;
await service.setEnvironment(QA_EU_REGION_KEY);
expect(window.location.href).toEqual(
QA_EU_WEB_REGION_CONFIG.urls.webVault + "/#" + routeAndQueryParams,
);
});
});
});
describe("EU Region", () => {
const initial_QA_EU_Urls = {
icons: "https://icons.euqa.bitwarden.pw",
notifications: "https://notifications.euqa.bitwarden.pw",
scim: "https://scim.euqa.bitwarden.pw",
} as Urls;
const mock_QA_EU_BaseUrl = "https://vault.euqa.bitwarden.pw";
const expected_QA_EU_Urls: Urls = {
...initial_QA_EU_Urls,
base: mock_QA_EU_BaseUrl,
};
const expectedModifiedScimUrl = expected_QA_EU_Urls.scim + "/v2";
const expectedSendUrl = QA_EU_WEB_REGION_CONFIG.urls.webVault + "/#/send/";
const QA_EU_Env = new WebCloudEnvironment(QA_EU_WEB_REGION_CONFIG, expected_QA_EU_Urls);
beforeEach(() => {
window = mock<Window>();
window.location = {
origin: mock_QA_EU_BaseUrl,
href: mock_QA_EU_BaseUrl + "/#/example",
} as Location;
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
router = mock<Router>();
(router as any).url = "";
service = new WebEnvironmentService(
window,
stateProvider,
accountService,
additionalRegionConfigs,
router,
initial_QA_EU_Urls,
);
});
it("initializes the environment to be the QA US environment", async () => {
const env = await firstValueFrom(service.environment$);
expect(env).toEqual(QA_EU_Env);
expect(env.getRegion()).toEqual(QA_EU_REGION_KEY);
expect(env.getUrls()).toEqual(expected_QA_EU_Urls);
expect(env.isCloud()).toBeTruthy();
expect(env.getApiUrl()).toEqual(expected_QA_EU_Urls.base + "/api");
expect(env.getIdentityUrl()).toEqual(expected_QA_EU_Urls.base + "/identity");
expect(env.getIconsUrl()).toEqual(expected_QA_EU_Urls.icons);
expect(env.getWebVaultUrl()).toEqual(QA_EU_WEB_REGION_CONFIG.urls.webVault);
expect(env.getNotificationsUrl()).toEqual(expected_QA_EU_Urls.notifications);
expect(env.getEventsUrl()).toEqual(expected_QA_EU_Urls.base + "/events");
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
expect(env.getSendUrl()).toEqual(expectedSendUrl);
expect(env.getHostname()).toEqual(QA_EU_WEB_REGION_CONFIG.domain);
});
describe("setEnvironment", () => {
it("throws an error when trying to set the environment to self-hosted", async () => {
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
"setEnvironment does not work in web for self-hosted.",
);
});
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
const urls = await service.setEnvironment(QA_EU_REGION_KEY);
expect(urls).toEqual(expected_QA_EU_Urls);
});
it("errors if the selected region is unknown", async () => {
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
"The selected region is not known as an available region.",
);
});
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
const routeAndQueryParams = "/signup?example=1&another=2";
(router as any).url = routeAndQueryParams;
await service.setEnvironment(QA_US_REGION_KEY);
expect(window.location.href).toEqual(
QA_US_WEB_REGION_CONFIG.urls.webVault + "/#" + routeAndQueryParams,
);
});
});
});
});
});

View File

@ -1,5 +1,5 @@
import { Router } from "@angular/router";
import { ReplaySubject } from "rxjs";
import { firstValueFrom, ReplaySubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
@ -8,7 +8,6 @@ import {
RegionConfig,
Urls,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
CloudEnvironment,
DefaultEnvironmentService,
@ -16,6 +15,12 @@ import {
} from "@bitwarden/common/platform/services/default-environment.service";
import { StateProvider } from "@bitwarden/common/platform/state";
export type WebRegionConfig = RegionConfig & {
key: Region | string; // strings are used for custom environments
domain: string;
urls: Urls;
};
/**
* Web specific environment service. Ensures that the urls are set from the window location.
*/
@ -24,23 +29,30 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
private win: Window,
stateProvider: StateProvider,
accountService: AccountService,
additionalRegionConfigs: WebRegionConfig[] = [],
private router: Router,
private envUrls: Urls,
) {
super(stateProvider, accountService);
super(stateProvider, accountService, additionalRegionConfigs);
// The web vault always uses the current location as the base url
const urls = process.env.URLS as Urls;
urls.base ??= this.win.location.origin;
envUrls.base ??= this.win.location.origin;
// Find the region
const domain = Utils.getDomain(this.win.location.href);
const region = this.availableRegions().find((r) => Utils.getDomain(r.urls.webVault) === domain);
const currentHostname = new URL(this.win.location.href).hostname;
const availableRegions = this.availableRegions();
const region = availableRegions.find((r) => {
// We must use hostname as our QA envs use the same
// domain (bitwarden.pw) but different subdomains (qa and euqa)
const webVaultHostname = new URL(r.urls.webVault).hostname;
return webVaultHostname === currentHostname;
});
let environment: Environment;
if (region) {
environment = new WebCloudEnvironment(region, urls);
environment = new WebCloudEnvironment(region, envUrls);
} else {
environment = new SelfHostedEnvironment(urls);
environment = new SelfHostedEnvironment(envUrls);
}
// Override the environment observable with a replay subject
@ -50,37 +62,45 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
}
// Web setting env means navigating to a new location
setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
async setEnvironment(region: Region | string, urls?: Urls): Promise<Urls> {
if (region === Region.SelfHosted) {
throw new Error("setEnvironment does not work in web for self-hosted.");
}
const currentDomain = Utils.getDomain(this.win.location.href);
const currentRegion = this.availableRegions().find(
(r) => Utils.getDomain(r.urls.webVault) === currentDomain,
);
// Find the region
const currentHostname = new URL(this.win.location.href).hostname;
const availableRegions = this.availableRegions();
const currentRegionConfig = availableRegions.find((r) => {
// We must use hostname as our QA envs use the same
// domain (bitwarden.pw) but different subdomains (qa and euqa)
const webVaultHostname = new URL(r.urls.webVault).hostname;
return webVaultHostname === currentHostname;
});
if (currentRegion.key === region) {
// They have selected the current region, nothing to do
return Promise.resolve(currentRegion.urls);
if (currentRegionConfig.key === region) {
// They have selected the current region, return the current env urls
// We can't return the region urls because the env base url is modified
// in the constructor to match the current window.location.origin.
const currentEnv = await firstValueFrom(this.environment$);
return currentEnv.getUrls();
}
const chosenRegion = this.availableRegions().find((r) => r.key === region);
const chosenRegionConfig = this.availableRegions().find((r) => r.key === region);
if (chosenRegion == null) {
if (chosenRegionConfig == null) {
throw new Error("The selected region is not known as an available region.");
}
// Preserve the current in app route + params in the new location
const routeAndParams = `/#${this.router.url}`;
this.win.location.href = chosenRegion.urls.webVault + routeAndParams;
this.win.location.href = chosenRegionConfig.urls.webVault + routeAndParams;
// This return shouldn't matter as we are about to leave the current window
return Promise.resolve(chosenRegion.urls);
return chosenRegionConfig.urls;
}
}
class WebCloudEnvironment extends CloudEnvironment {
export class WebCloudEnvironment extends CloudEnvironment {
constructor(config: RegionConfig, urls: Urls) {
super(config);
// We override the urls to avoid CORS issues

View File

@ -3,6 +3,7 @@ import { Observable, Subject } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { ClientType } from "@bitwarden/common/enums";
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
import {
AbstractStorageService,
ObservableStorageService,
@ -58,3 +59,12 @@ export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE");
export const REFRESH_ACCESS_TOKEN_ERROR_CALLBACK = new SafeInjectionToken<() => void>(
"REFRESH_ACCESS_TOKEN_ERROR_CALLBACK",
);
/**
* Injection token for injecting the NodeJS process.env additional regions into services.
* Using an injection token allows services to be tested without needing to
* mock the process.env.
*/
export const ENV_ADDITIONAL_REGIONS = new SafeInjectionToken<RegionConfig[]>(
"ENV_ADDITIONAL_REGIONS",
);

View File

@ -141,7 +141,10 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
EnvironmentService,
RegionConfig,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -298,6 +301,7 @@ import {
INTRAPROCESS_MESSAGING_SUBJECT,
CLIENT_TYPE,
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
ENV_ADDITIONAL_REGIONS,
} from "./injection-tokens";
import { ModalService } from "./modal.service";
@ -530,10 +534,14 @@ const safeProviders: SafeProvider[] = [
useClass: CollectionService,
deps: [CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, StateProvider],
}),
safeProvider({
provide: ENV_ADDITIONAL_REGIONS,
useValue: process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
}),
safeProvider({
provide: EnvironmentService,
useClass: DefaultEnvironmentService,
deps: [StateProvider, AccountServiceAbstraction],
deps: [StateProvider, AccountServiceAbstraction, ENV_ADDITIONAL_REGIONS],
}),
safeProvider({
provide: InternalUserDecryptionOptionsServiceAbstraction,

View File

@ -136,6 +136,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
constructor(
private stateProvider: StateProvider,
private accountService: AccountService,
private additionalRegionConfigs: RegionConfig[] = [],
) {
this.globalState = this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY);
this.globalCloudRegionState = this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY);
@ -177,8 +178,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
}
availableRegions(): RegionConfig[] {
const additionalRegions = (process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[]) ?? [];
return PRODUCTION_REGIONS.concat(additionalRegions);
return PRODUCTION_REGIONS.concat(this.additionalRegionConfigs);
}
/**