bitwarden-estensione-browser/libs/common/src/platform/services/environment.service.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

419 lines
12 KiB
TypeScript
Raw Normal View History

import {
concatMap,
distinctUntilChanged,
firstValueFrom,
map,
Observable,
ReplaySubject,
} from "rxjs";
import { AccountService } from "../../auth/abstractions/account.service";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { UserId } from "../../types/guid";
import {
EnvironmentService as EnvironmentServiceAbstraction,
Region,
RegionDomain,
Urls,
} from "../abstractions/environment.service";
import { Utils } from "../misc/utils";
import { ENVIRONMENT_DISK, GlobalState, KeyDefinition, StateProvider } from "../state";
const REGION_KEY = new KeyDefinition<Region>(ENVIRONMENT_DISK, "region", {
deserializer: (s) => s,
});
const URLS_KEY = new KeyDefinition<EnvironmentUrls>(ENVIRONMENT_DISK, "urls", {
deserializer: EnvironmentUrls.fromJSON,
});
2018-01-10 02:20:54 +01:00
2018-01-23 23:29:15 +01:00
export class EnvironmentService implements EnvironmentServiceAbstraction {
private readonly urlsSubject = new ReplaySubject<void>(1);
urls: Observable<void> = this.urlsSubject.asObservable();
selectedRegion?: Region;
initialized = false;
2021-12-16 13:36:21 +01:00
protected baseUrl: string;
protected webVaultUrl: string;
protected apiUrl: string;
protected identityUrl: string;
protected iconsUrl: string;
protected notificationsUrl: string;
protected eventsUrl: string;
private keyConnectorUrl: string;
private scimUrl: string = null;
private cloudWebVaultUrl: string;
2021-12-16 13:36:21 +01:00
private regionGlobalState: GlobalState<Region | null>;
private urlsGlobalState: GlobalState<EnvironmentUrls | null>;
private activeAccountId$: Observable<UserId | null>;
readonly usUrls: Urls = {
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",
};
readonly euUrls: Urls = {
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",
};
constructor(
private stateProvider: StateProvider,
private accountService: AccountService,
) {
// We intentionally don't want the helper on account service, we want the null back if there is no active user
this.activeAccountId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
// TODO: Get rid of early subscription during EnvironmentService refactor
this.activeAccountId$
.pipe(
// Use == here to not trigger on undefined -> null transition
distinctUntilChanged((oldUserId: string, newUserId: string) => oldUserId == newUserId),
concatMap(async () => {
if (!this.initialized) {
return;
}
await this.setUrlsFromStorage();
}),
)
.subscribe();
this.regionGlobalState = this.stateProvider.getGlobal(REGION_KEY);
this.urlsGlobalState = this.stateProvider.getGlobal(URLS_KEY);
Misc Account Switching Fixes & Refactors (#600) * [refactor] Restructure EnvironmentUrls in state * Patch up (add missing fields) and more extensivly use the EnvironmentUrls class instead of passing around an any * Add environmentUrls to the AccountSettings model in addition to GlobalState for use in both scopes * Move EnvironmentUrls initialization to the model level and out of StateSerice * Adjust the StateMigrationService to account for these changes * [refactor] Improve order of operations for LockGuardService We currently jump through a bunch of hoops to verify users can access the Lock page, like checking authentication first. If a user is not authenticated, they are not locked, so we can improve performance for the happy path of this serivice by checking isLocked first and using isAuthenticated to deviate from the normal flow if needed. * [bug] Subscribe to State.accounts in EnvironmentService and set urls accordingly The EnvironmentService has no context for account changes currently and does not update actively used urls based on active account. This commit addresses this issue by subscribing to State.accounts and resetting the service's urls on account change. * [bug] Clear AccessToken from State on clean In order for logout flows to function as expected we need to deauthenticate users when cleaning up state before checking for the next active user Otherwise the service will continue to think the user being logged out is active * [refactor] Stop pushing accounts when modifying disk state There is no reason to push new accounts to subscribers when updating disk state. Subscribers recieve a copy of in memory state, so changes to disk will not be refelected and have to be fetched seperatly from the service. Pushing when saving disk state is just creating an unecassary performance burden. * [refactor] Default to in memory active user if availible, even when accessing disk state Sometimes we need to pull activeUserId from storage to access a bit of data, like on initial boot, but most of the time this isn't necassary. Since we pull this userId a lot, checking disk each time is a performance burden. Defaulting to the in memory user ID if avaible helps alleviate this. * [style] Ran prettier * [style] Change a let to a const
2022-01-07 15:30:54 +01:00
}
2021-12-16 13:36:21 +01:00
hasBaseUrl() {
return this.baseUrl != null;
2021-12-16 13:36:21 +01:00
}
getNotificationsUrl() {
if (this.notificationsUrl != null) {
return this.notificationsUrl;
}
if (this.baseUrl != null) {
return this.baseUrl + "/notifications";
}
2018-06-25 14:06:19 +02:00
return "https://notifications.bitwarden.com";
}
getWebVaultUrl() {
if (this.webVaultUrl != null) {
2018-06-25 14:06:19 +02:00
return this.webVaultUrl;
}
if (this.baseUrl) {
return this.baseUrl;
}
return "https://vault.bitwarden.com";
2021-12-16 13:36:21 +01:00
}
getCloudWebVaultUrl() {
if (this.cloudWebVaultUrl != null) {
return this.cloudWebVaultUrl;
}
return this.usUrls.webVault;
}
setCloudWebVaultUrl(region: Region) {
switch (region) {
case Region.EU:
this.cloudWebVaultUrl = this.euUrls.webVault;
break;
case Region.US:
default:
this.cloudWebVaultUrl = this.usUrls.webVault;
break;
}
}
getSendUrl() {
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
? "https://send.bitwarden.com/#"
: this.getWebVaultUrl() + "/#/send/";
2021-12-16 13:36:21 +01:00
}
getIconsUrl() {
if (this.iconsUrl != null) {
return this.iconsUrl;
}
if (this.baseUrl) {
return this.baseUrl + "/icons";
}
return "https://icons.bitwarden.net";
}
getApiUrl() {
if (this.apiUrl != null) {
return this.apiUrl;
}
if (this.baseUrl) {
return this.baseUrl + "/api";
}
return "https://api.bitwarden.com";
}
getIdentityUrl() {
if (this.identityUrl != null) {
return this.identityUrl;
}
if (this.baseUrl) {
return this.baseUrl + "/identity";
}
return "https://identity.bitwarden.com";
}
getEventsUrl() {
if (this.eventsUrl != null) {
return this.eventsUrl;
2018-01-10 02:20:54 +01:00
}
if (this.baseUrl) {
return this.baseUrl + "/events";
}
return "https://events.bitwarden.com";
2021-12-16 13:36:21 +01:00
}
getKeyConnectorUrl() {
return this.keyConnectorUrl;
2021-12-16 13:36:21 +01:00
}
getScimUrl() {
if (this.scimUrl != null) {
return this.scimUrl + "/v2";
}
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
? "https://scim.bitwarden.com/v2"
: this.getWebVaultUrl() + "/scim/v2";
}
2018-01-10 02:20:54 +01:00
async setUrlsFromStorage(): Promise<void> {
const activeUserId = await firstValueFrom(this.activeAccountId$);
const region = await this.getRegion(activeUserId);
const savedUrls = await this.getEnvironmentUrls(activeUserId);
2018-01-10 02:20:54 +01:00
const envUrls = new EnvironmentUrls();
2021-12-16 13:36:21 +01:00
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
// This check will detect these cases and convert them to the proper region instead.
// We are detecting this by checking for the presence of the web vault URL in the `base` and the absence of the `notifications` property.
// This is because the `notifications` will not be `null` in the web vault, and we don't want to migrate the URLs in that case.
if (savedUrls.base === "https://vault.bitwarden.com" && savedUrls.notifications == null) {
await this.setRegion(Region.US);
return;
}
switch (region) {
case Region.EU:
await this.setRegion(Region.EU);
return;
case Region.US:
await this.setRegion(Region.US);
return;
case Region.SelfHosted:
case null:
default:
this.baseUrl = envUrls.base = savedUrls.base;
this.webVaultUrl = savedUrls.webVault;
this.apiUrl = envUrls.api = savedUrls.api;
this.identityUrl = envUrls.identity = savedUrls.identity;
this.iconsUrl = savedUrls.icons;
this.notificationsUrl = savedUrls.notifications;
this.eventsUrl = envUrls.events = savedUrls.events;
this.keyConnectorUrl = savedUrls.keyConnector;
await this.setRegion(Region.SelfHosted);
// scimUrl is not saved to storage
this.urlsSubject.next();
break;
}
2021-12-16 13:36:21 +01:00
}
async setUrls(urls: Urls): Promise<Urls> {
2018-01-10 02:20:54 +01:00
urls.base = this.formatUrl(urls.base);
urls.webVault = this.formatUrl(urls.webVault);
urls.api = this.formatUrl(urls.api);
urls.identity = this.formatUrl(urls.identity);
urls.icons = this.formatUrl(urls.icons);
2018-08-20 19:45:32 +02:00
urls.notifications = this.formatUrl(urls.notifications);
2019-06-20 14:56:45 +02:00
urls.events = this.formatUrl(urls.events);
urls.keyConnector = this.formatUrl(urls.keyConnector);
2021-12-16 13:36:21 +01:00
// scimUrl cannot be cleared
urls.scim = this.formatUrl(urls.scim) ?? this.scimUrl;
// Don't save scim url
await this.urlsGlobalState.update(() => ({
base: urls.base,
api: urls.api,
identity: urls.identity,
webVault: urls.webVault,
icons: urls.icons,
notifications: urls.notifications,
events: urls.events,
keyConnector: urls.keyConnector,
}));
2018-01-10 02:20:54 +01:00
this.baseUrl = urls.base;
this.webVaultUrl = urls.webVault;
this.apiUrl = urls.api;
this.identityUrl = urls.identity;
this.iconsUrl = urls.icons;
2018-08-20 19:45:32 +02:00
this.notificationsUrl = urls.notifications;
2019-06-20 14:56:45 +02:00
this.eventsUrl = urls.events;
this.keyConnectorUrl = urls.keyConnector;
this.scimUrl = urls.scim;
2021-12-16 13:36:21 +01:00
await this.setRegion(Region.SelfHosted);
this.urlsSubject.next();
2021-12-16 13:36:21 +01:00
2018-01-10 02:20:54 +01:00
return urls;
2021-12-16 13:36:21 +01:00
}
getUrls() {
2021-12-16 13:36:21 +01:00
return {
base: this.baseUrl,
webVault: this.webVaultUrl,
cloudWebVault: this.cloudWebVaultUrl,
api: this.apiUrl,
identity: this.identityUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
events: this.eventsUrl,
keyConnector: this.keyConnectorUrl,
scim: this.scimUrl,
2021-12-16 13:36:21 +01:00
};
}
isEmpty(): boolean {
return (
this.baseUrl == null &&
this.webVaultUrl == null &&
this.apiUrl == null &&
this.identityUrl == null &&
this.iconsUrl == null &&
this.notificationsUrl == null &&
this.eventsUrl == null
);
}
async getHost(userId?: UserId) {
const region = await this.getRegion(userId);
switch (region) {
case Region.US:
return RegionDomain.US;
case Region.EU:
return RegionDomain.EU;
default: {
// Environment is self-hosted
const envUrls = await this.getEnvironmentUrls(userId);
return Utils.getHost(envUrls.webVault || envUrls.base);
}
}
}
private async getRegion(userId: UserId | null) {
// Previous rules dictated that we only get from user scoped state if there is an active user.
const activeUserId = await firstValueFrom(this.activeAccountId$);
return activeUserId == null
? await firstValueFrom(this.regionGlobalState.state$)
: await firstValueFrom(this.stateProvider.getUser(userId ?? activeUserId, REGION_KEY).state$);
}
private async getEnvironmentUrls(userId: UserId | null) {
return userId == null
? (await firstValueFrom(this.urlsGlobalState.state$)) ?? new EnvironmentUrls()
: (await firstValueFrom(this.stateProvider.getUser(userId, URLS_KEY).state$)) ??
new EnvironmentUrls();
}
async setRegion(region: Region) {
this.selectedRegion = region;
await this.regionGlobalState.update(() => region);
if (region === Region.SelfHosted) {
// If user saves a self-hosted region with empty fields, default to US
if (this.isEmpty()) {
await this.setRegion(Region.US);
}
} else {
// If we are setting the region to EU or US, clear the self-hosted URLs
await this.urlsGlobalState.update(() => new EnvironmentUrls());
if (region === Region.EU) {
this.setUrlsInternal(this.euUrls);
} else if (region === Region.US) {
this.setUrlsInternal(this.usUrls);
}
}
}
async seedUserEnvironment(userId: UserId) {
const globalRegion = await firstValueFrom(this.regionGlobalState.state$);
const globalUrls = await firstValueFrom(this.urlsGlobalState.state$);
await this.stateProvider.getUser(userId, REGION_KEY).update(() => globalRegion);
await this.stateProvider.getUser(userId, URLS_KEY).update(() => globalUrls);
}
private setUrlsInternal(urls: Urls) {
this.baseUrl = this.formatUrl(urls.base);
this.webVaultUrl = this.formatUrl(urls.webVault);
this.apiUrl = this.formatUrl(urls.api);
this.identityUrl = this.formatUrl(urls.identity);
this.iconsUrl = this.formatUrl(urls.icons);
this.notificationsUrl = this.formatUrl(urls.notifications);
this.eventsUrl = this.formatUrl(urls.events);
this.keyConnectorUrl = this.formatUrl(urls.keyConnector);
// scimUrl cannot be cleared
this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl;
this.urlsSubject.next();
}
private formatUrl(url: string): string {
2018-01-10 02:20:54 +01:00
if (url == null || url === "") {
return null;
2021-12-16 13:36:21 +01:00
}
2018-01-10 02:20:54 +01:00
url = url.replace(/\/+$/g, "");
2019-06-20 14:56:45 +02:00
if (!url.startsWith("http://") && !url.startsWith("https://")) {
2018-01-10 02:20:54 +01:00
url = "https://" + url;
}
2021-12-16 13:36:21 +01:00
2019-02-15 22:53:01 +01:00
return url.trim();
2021-12-16 13:36:21 +01:00
}
SM-90: Add Server Version to Browser About Page (#3223) * Add structure to display server version on browser * Add getConfig to State Service interface * Clean up settings component code * Switch to ServerConfig, use Observables in the ConfigService, and more * Fix runtime error * Sm 90 addison (#3275) * Use await instead of then * Rename stateServerConfig -> storedServerConfig * Move config validation logic to the model * Use implied check for undefined * Rename getStateServicerServerConfig -> buildServerConfig * Rename getApiServiceServerConfig -> pollServerConfig * Build server config in async * small fixes and add last seen text * Move config server to /config folder * Update with concatMap and other changes * Config project updates * Rename fileds to convention and remove unneeded migration * Update libs/common/src/services/state.service.ts Update based on Oscar's recommendation Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Update options for Oscar's rec * Rename abstractions to abstracitons * Fix null issues and add options * Combine classes into one file, per Oscar's rec * Add null checking * Fix dependency issue * Add null checks, await, and fix date issue * Remove unneeded null check * In progress commit, unsuitable for for more than dev env, just backing up changes made with Oscar * Fix temp code to force last seen state * Add localization and escapes in the browser about section * Call complete on destroy subject rather than unsubscribe * use mediumDate and formatDate for the last seen date messaging * Add ThirdPartyServerName in example * Add deprecated note per Oscar's comment * [SM-90] Change to using a modal for browser about (#3417) * Fix inconsistent constructor null checking * ServerConfig can be null, fixes this * Switch to call super first, as required * remove unneeded null checks * Remove null checks from server-config.data.ts class * Update via PR comments and add back needed null check in server conf obj * Remove type annotation from serverConfig$ * Update self-hosted to be <small> per design decision * Re-fetch config every hour * Make third party server version <small> and change wording per Oscar's PR comment * Add expiresSoon function and re-fetch if the serverConfig will expire soon (older than 18 hours) * Fix misaligned small third party server message text Co-authored-by: Addison Beck <addisonbeck1@gmail.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
2022-09-08 14:27:19 +02:00
isCloud(): boolean {
return [
"https://api.bitwarden.com",
"https://vault.bitwarden.com/api",
"https://api.bitwarden.eu",
"https://vault.bitwarden.eu/api",
].includes(this.getApiUrl());
SM-90: Add Server Version to Browser About Page (#3223) * Add structure to display server version on browser * Add getConfig to State Service interface * Clean up settings component code * Switch to ServerConfig, use Observables in the ConfigService, and more * Fix runtime error * Sm 90 addison (#3275) * Use await instead of then * Rename stateServerConfig -> storedServerConfig * Move config validation logic to the model * Use implied check for undefined * Rename getStateServicerServerConfig -> buildServerConfig * Rename getApiServiceServerConfig -> pollServerConfig * Build server config in async * small fixes and add last seen text * Move config server to /config folder * Update with concatMap and other changes * Config project updates * Rename fileds to convention and remove unneeded migration * Update libs/common/src/services/state.service.ts Update based on Oscar's recommendation Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Update options for Oscar's rec * Rename abstractions to abstracitons * Fix null issues and add options * Combine classes into one file, per Oscar's rec * Add null checking * Fix dependency issue * Add null checks, await, and fix date issue * Remove unneeded null check * In progress commit, unsuitable for for more than dev env, just backing up changes made with Oscar * Fix temp code to force last seen state * Add localization and escapes in the browser about section * Call complete on destroy subject rather than unsubscribe * use mediumDate and formatDate for the last seen date messaging * Add ThirdPartyServerName in example * Add deprecated note per Oscar's comment * [SM-90] Change to using a modal for browser about (#3417) * Fix inconsistent constructor null checking * ServerConfig can be null, fixes this * Switch to call super first, as required * remove unneeded null checks * Remove null checks from server-config.data.ts class * Update via PR comments and add back needed null check in server conf obj * Remove type annotation from serverConfig$ * Update self-hosted to be <small> per design decision * Re-fetch config every hour * Make third party server version <small> and change wording per Oscar's PR comment * Add expiresSoon function and re-fetch if the serverConfig will expire soon (older than 18 hours) * Fix misaligned small third party server message text Co-authored-by: Addison Beck <addisonbeck1@gmail.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
2022-09-08 14:27:19 +02:00
}
2018-01-10 02:20:54 +01:00
}