diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 0fe16cd0ab9..5e2feb15dd0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -2,6 +2,8 @@ import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarde import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; +import { ConfigApiServiceAbstraction } from "@bitwarden/common/abstractions/config/config-api.service.abstraction"; +import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; @@ -49,6 +51,7 @@ import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-u import { ApiService } from "@bitwarden/common/services/api.service"; import { AppIdService } from "@bitwarden/common/services/appId.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; +import { ConfigService } from "@bitwarden/common/services/config/config.service"; import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; import { ContainerService } from "@bitwarden/common/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation"; @@ -185,6 +188,8 @@ export default class MainBackground { avatarUpdateService: AvatarUpdateServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; + configService: ConfigServiceAbstraction; + configApiService: ConfigApiServiceAbstraction; // Passed to the popup for Safari to workaround issues with theming, downloading, etc. backgroundWindow = window; @@ -493,6 +498,12 @@ export default class MainBackground { this.userVerificationApiService ); + this.configService = new ConfigService( + this.stateService, + this.configApiService, + this.authService + ); + const systemUtilsServiceReloadCallback = () => { const forceWindowReload = this.platformUtilsService.isSafari() || @@ -522,7 +533,8 @@ export default class MainBackground { this.systemService, this.environmentService, this.messagingService, - this.logService + this.logService, + this.configService ); this.nativeMessagingBackground = new NativeMessagingBackground( this.cryptoService, diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 317896d9ece..6f66e28a68a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,3 +1,4 @@ +import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; @@ -28,7 +29,8 @@ export default class RuntimeBackground { private systemService: SystemService, private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, - private logService: LogService + private logService: LogService, + private configService: ConfigServiceAbstraction ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -94,6 +96,7 @@ export default class RuntimeBackground { await this.main.refreshMenu(); }, 2000); this.main.avatarUpdateService.loadColorFromState(); + this.configService.fetchServerConfig(); } break; case "openPopup": diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 9862c588153..5700d6abbb3 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -16,6 +16,7 @@ import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -133,7 +134,8 @@ export class AppComponent implements OnInit, OnDestroy { private eventUploadService: EventUploadService, private policyService: InternalPolicyService, private modalService: ModalService, - private keyConnectorService: KeyConnectorService + private keyConnectorService: KeyConnectorService, + private configService: ConfigServiceAbstraction ) {} ngOnInit() { @@ -217,6 +219,7 @@ export class AppComponent implements OnInit, OnDestroy { break; case "syncCompleted": await this.updateAppMenu(); + await this.configService.fetchServerConfig(); break; case "openSettings": await this.openModal(SettingsComponent, this.settingsRef); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 6826da0467d..05ae0287b25 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -8,6 +8,7 @@ import { Subject, takeUntil } from "rxjs"; import Swal from "sweetalert2"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -77,7 +78,8 @@ export class AppComponent implements OnDestroy, OnInit { private eventUploadService: EventUploadService, private policyService: InternalPolicyService, protected policyListService: PolicyListService, - private keyConnectorService: KeyConnectorService + private keyConnectorService: KeyConnectorService, + private configService: ConfigServiceAbstraction ) {} ngOnInit() { @@ -127,6 +129,7 @@ export class AppComponent implements OnDestroy, OnInit { case "syncStarted": break; case "syncCompleted": + await this.configService.fetchServerConfig(); break; case "upgradeOrganization": { const upgradeConfirmed = await this.platformUtilsService.showDialog( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 46e6932d9f5..73b7c284a92 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -615,7 +615,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; { provide: ConfigServiceAbstraction, useClass: ConfigService, - deps: [StateServiceAbstraction, ConfigApiServiceAbstraction], + deps: [StateServiceAbstraction, ConfigApiServiceAbstraction, AuthServiceAbstraction], }, { provide: ConfigApiServiceAbstraction, diff --git a/libs/common/src/abstractions/config/config.service.abstraction.ts b/libs/common/src/abstractions/config/config.service.abstraction.ts index ee9f946c7f3..dd9d435ed3a 100644 --- a/libs/common/src/abstractions/config/config.service.abstraction.ts +++ b/libs/common/src/abstractions/config/config.service.abstraction.ts @@ -1,7 +1,13 @@ import { Observable } from "rxjs"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; + import { ServerConfig } from "./server-config"; export abstract class ConfigServiceAbstraction { serverConfig$: Observable; + fetchServerConfig: () => Promise; + getFeatureFlagBool: (key: FeatureFlag, defaultValue?: boolean) => Promise; + getFeatureFlagString: (key: FeatureFlag, defaultValue?: string) => Promise; + getFeatureFlagNumber: (key: FeatureFlag, defaultValue?: number) => Promise; } diff --git a/libs/common/src/abstractions/config/server-config.ts b/libs/common/src/abstractions/config/server-config.ts index 15dfa8c978a..2fa250202e4 100644 --- a/libs/common/src/abstractions/config/server-config.ts +++ b/libs/common/src/abstractions/config/server-config.ts @@ -15,6 +15,7 @@ export class ServerConfig { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: Date; + featureStates: { [key: string]: string } = {}; constructor(serverConfigData: ServerConfigData) { this.version = serverConfigData.version; @@ -22,6 +23,7 @@ export class ServerConfig { this.server = serverConfigData.server; this.utcDate = new Date(serverConfigData.utcDate); this.environment = serverConfigData.environment; + this.featureStates = serverConfigData.featureStates; if (this.server?.name == null && this.server?.url == null) { this.server = null; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts new file mode 100644 index 00000000000..8ba47bcd96d --- /dev/null +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -0,0 +1,3 @@ +export enum FeatureFlag { + DisplayEuEnvironmentFlag = "display-eu-environment", +} diff --git a/libs/common/src/models/data/server-config.data.ts b/libs/common/src/models/data/server-config.data.ts index 34ed4f190b7..574fa8c63a3 100644 --- a/libs/common/src/models/data/server-config.data.ts +++ b/libs/common/src/models/data/server-config.data.ts @@ -12,6 +12,7 @@ export class ServerConfigData { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: string; + featureStates: { [key: string]: string } = {}; constructor(serverConfigResponse: Partial) { this.version = serverConfigResponse?.version; @@ -23,6 +24,7 @@ export class ServerConfigData { this.environment = serverConfigResponse?.environment ? new EnvironmentServerConfigData(serverConfigResponse.environment) : null; + this.featureStates = serverConfigResponse?.featureStates; } static fromJSON(obj: Jsonify): ServerConfigData { diff --git a/libs/common/src/models/response/server-config.response.ts b/libs/common/src/models/response/server-config.response.ts index 1d6f8ffe1bc..7594f86aa80 100644 --- a/libs/common/src/models/response/server-config.response.ts +++ b/libs/common/src/models/response/server-config.response.ts @@ -5,6 +5,7 @@ export class ServerConfigResponse extends BaseResponse { gitHash: string; server: ThirdPartyServerConfigResponse; environment: EnvironmentServerConfigResponse; + featureStates: { [key: string]: string } = {}; constructor(response: any) { super(response); @@ -17,6 +18,7 @@ export class ServerConfigResponse extends BaseResponse { this.gitHash = this.getResponseProperty("GitHash"); this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server")); this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment")); + this.featureStates = this.getResponseProperty("FeatureStates"); } } diff --git a/libs/common/src/services/config/config-api.service.ts b/libs/common/src/services/config/config-api.service.ts index 318eae413ce..743f48a4967 100644 --- a/libs/common/src/services/config/config-api.service.ts +++ b/libs/common/src/services/config/config-api.service.ts @@ -6,7 +6,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction { constructor(private apiService: ApiService) {} async get(): Promise { - const r = await this.apiService.send("GET", "/config", null, true, true); + const r = await this.apiService.send("GET", "/config", null, false, true); return new ServerConfigResponse(r); } } diff --git a/libs/common/src/services/config/config.service.ts b/libs/common/src/services/config/config.service.ts index e82d3ccb31d..0dd6aa71d4a 100644 --- a/libs/common/src/services/config/config.service.ts +++ b/libs/common/src/services/config/config.service.ts @@ -1,9 +1,12 @@ -import { BehaviorSubject, concatMap, map, switchMap, timer, EMPTY } from "rxjs"; +import { BehaviorSubject, concatMap, timer } from "rxjs"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; import { StateService } from "../../abstractions/state.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { ServerConfigData } from "../../models/data/server-config.data"; export class ConfigService implements ConfigServiceAbstraction { @@ -12,21 +15,14 @@ export class ConfigService implements ConfigServiceAbstraction { constructor( private stateService: StateService, - private configApiService: ConfigApiServiceAbstraction + private configApiService: ConfigApiServiceAbstraction, + private authService: AuthService ) { - this.stateService.activeAccountUnlocked$ + // Re-fetch the server config every hour + timer(0, 1000 * 3600) .pipe( - switchMap((unlocked) => { - if (!unlocked) { - this._serverConfig.next(null); - return EMPTY; - } - - // Re-fetch the server config every hour - return timer(0, 3600 * 1000).pipe(map(() => unlocked)); - }), - concatMap(async (unlocked) => { - return unlocked ? await this.buildServerConfig() : null; + concatMap(async () => { + return await this.fetchServerConfig(); }) ) .subscribe((serverConfig) => { @@ -34,9 +30,51 @@ export class ConfigService implements ConfigServiceAbstraction { }); } + async fetchServerConfig(): Promise { + try { + const response = await this.configApiService.get(); + + if (response != null) { + const data = new ServerConfigData(response); + const serverConfig = new ServerConfig(data); + this._serverConfig.next(serverConfig); + if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) { + return serverConfig; + } + await this.stateService.setServerConfig(data); + } + } catch { + return null; + } + } + + async getFeatureFlagBool(key: FeatureFlag, defaultValue = false): Promise { + return await this.getFeatureFlag(key, defaultValue); + } + + async getFeatureFlagString(key: FeatureFlag, defaultValue = ""): Promise { + return await this.getFeatureFlag(key, defaultValue); + } + + async getFeatureFlagNumber(key: FeatureFlag, defaultValue = 0): Promise { + return await this.getFeatureFlag(key, defaultValue); + } + + private async getFeatureFlag(key: FeatureFlag, defaultValue: T): Promise { + const serverConfig = await this.buildServerConfig(); + if ( + serverConfig == null || + serverConfig.featureStates == null || + serverConfig.featureStates[key] == null + ) { + return defaultValue; + } + return serverConfig.featureStates[key] as T; + } + private async buildServerConfig(): Promise { const data = await this.stateService.getServerConfig(); - const domain = data ? new ServerConfig(data) : null; + const domain = data ? new ServerConfig(data) : this._serverConfig.getValue(); if (domain == null || !domain.isValid() || domain.expiresSoon()) { const value = await this.fetchServerConfig(); @@ -45,18 +83,4 @@ export class ConfigService implements ConfigServiceAbstraction { return domain; } - - private async fetchServerConfig(): Promise { - try { - const response = await this.configApiService.get(); - - if (response != null) { - const data = new ServerConfigData(response); - await this.stateService.setServerConfig(data); - return new ServerConfig(data); - } - } catch { - return null; - } - } }