mirror of
https://github.com/bitwarden/browser
synced 2026-02-26 09:33:22 +00:00
Parse the CommunicationServerConfigResponse within the ConfigService and expose changes via an observable. (#19184)
Model was introduced on the server with https://github.com/bitwarden/server/pull/6892 Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0a1baa7e42
commit
181e4767d8
@@ -3,6 +3,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { ServerCommunicationConfig } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
@@ -13,6 +15,12 @@ import { ServerConfig } from "./server-config";
|
||||
export abstract class ConfigService {
|
||||
/** The server config of the currently active user */
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
/**
|
||||
* Emits whenever a server config is successfully fetched, pairing the server's
|
||||
* API URL with the parsed ServerCommunicationConfig. Use this to react to
|
||||
* communication config changes without coupling to the config fetch pipeline.
|
||||
*/
|
||||
serverCommunicationConfig$: Observable<{ hostname: string; config: ServerCommunicationConfig }>;
|
||||
/** The server settings of the currently active user */
|
||||
serverSettings$: Observable<ServerSettings | null>;
|
||||
/** The cloud region of the currently active user */
|
||||
|
||||
@@ -13,6 +13,7 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
push: PushSettingsConfigResponse;
|
||||
settings: ServerSettings;
|
||||
communication: CommunicationServerConfigResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -28,6 +29,9 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
this.featureStates = this.getResponseProperty("FeatureStates");
|
||||
this.push = new PushSettingsConfigResponse(this.getResponseProperty("Push"));
|
||||
this.settings = new ServerSettings(this.getResponseProperty("Settings"));
|
||||
this.communication = new CommunicationServerConfigResponse(
|
||||
this.getResponseProperty("Communication"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,3 +90,37 @@ export class ThirdPartyServerConfigResponse extends BaseResponse {
|
||||
this.url = this.getResponseProperty("Url");
|
||||
}
|
||||
}
|
||||
|
||||
export class CommunicationServerConfigResponse extends BaseResponse {
|
||||
bootstrap: BootstrapServerConfigResponse;
|
||||
|
||||
constructor(data: any = null) {
|
||||
super(data);
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bootstrap = new BootstrapServerConfigResponse(this.getResponseProperty("Bootstrap"));
|
||||
}
|
||||
}
|
||||
|
||||
export class BootstrapServerConfigResponse extends BaseResponse {
|
||||
type: string;
|
||||
idpLoginUrl: string;
|
||||
cookieName: string;
|
||||
cookieDomain: string;
|
||||
|
||||
constructor(data: any = null) {
|
||||
super(data);
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.idpLoginUrl = this.getResponseProperty("IdpLoginUrl");
|
||||
this.cookieName = this.getResponseProperty("CookieName");
|
||||
this.cookieDomain = this.getResponseProperty("CookieDomain");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,46 @@ describe("ConfigService", () => {
|
||||
|
||||
expect(actual).toEqual(newConfig);
|
||||
});
|
||||
|
||||
describe("serverCommunicationConfig$", () => {
|
||||
it("emits direct bootstrap config when response has no communication config", async () => {
|
||||
await firstValueFrom(sut.serverConfig$);
|
||||
|
||||
const result = await firstValueFrom(sut.serverCommunicationConfig$);
|
||||
|
||||
expect(result).toEqual({
|
||||
hostname: activeApiUrl,
|
||||
config: { bootstrap: { type: "direct" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("emits ssoCookieVendor config when response includes ssoCookieVendor bootstrap", async () => {
|
||||
const ssoResponse = serverConfigResponseFactory("hash", {
|
||||
type: "ssoCookieVendor",
|
||||
idpLoginUrl: "https://idp.example.com",
|
||||
cookieName: "auth_token",
|
||||
cookieDomain: ".example.com",
|
||||
});
|
||||
configApiService.get.mockResolvedValue(ssoResponse);
|
||||
|
||||
await firstValueFrom(sut.serverConfig$);
|
||||
|
||||
const result = await firstValueFrom(sut.serverCommunicationConfig$);
|
||||
|
||||
expect(result).toEqual({
|
||||
hostname: activeApiUrl,
|
||||
config: {
|
||||
bootstrap: {
|
||||
type: "ssoCookieVendor",
|
||||
idpLoginUrl: "https://idp.example.com",
|
||||
cookieName: "auth_token",
|
||||
cookieDomain: ".example.com",
|
||||
cookieValue: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,7 +415,10 @@ function serverConfigDataFactory(hash?: string) {
|
||||
return new ServerConfigData(serverConfigResponseFactory(hash));
|
||||
}
|
||||
|
||||
function serverConfigResponseFactory(hash?: string) {
|
||||
function serverConfigResponseFactory(
|
||||
hash?: string,
|
||||
bootstrap?: { type: string; idpLoginUrl?: string; cookieName?: string; cookieDomain?: string },
|
||||
) {
|
||||
return new ServerConfigResponse({
|
||||
version: "myConfigVersion",
|
||||
gitHash: hash ?? Utils.newGuid(), // Use optional git hash to store uniqueness value
|
||||
@@ -391,6 +434,7 @@ function serverConfigResponseFactory(hash?: string) {
|
||||
feat2: "on",
|
||||
feat3: "off",
|
||||
},
|
||||
...(bootstrap != null ? { communication: { bootstrap } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { ServerCommunicationConfig } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { FeatureFlag, getFeatureFlagValue } from "../../../enums/feature-flag.enum";
|
||||
@@ -28,6 +30,7 @@ import { LogService } from "../../abstractions/log.service";
|
||||
import { devFlagEnabled, devFlagValue } from "../../misc/flags";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
import { ServerConfigResponse } from "../../models/response/server-config.response";
|
||||
import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs")
|
||||
@@ -58,8 +61,13 @@ const environmentComparer = (previous: Environment, current: Environment) => {
|
||||
// FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it.
|
||||
export class DefaultConfigService implements ConfigService {
|
||||
private failedFetchFallbackSubject = new Subject<ServerConfig | null>();
|
||||
private serverCommunicationConfigSubject = new ReplaySubject<{
|
||||
hostname: string;
|
||||
config: ServerCommunicationConfig;
|
||||
}>(1);
|
||||
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
serverCommunicationConfig$ = this.serverCommunicationConfigSubject.asObservable();
|
||||
|
||||
serverSettings$: Observable<ServerSettings>;
|
||||
|
||||
@@ -206,6 +214,8 @@ export class DefaultConfigService implements ConfigService {
|
||||
clearTimeout(handle);
|
||||
const newConfig = new ServerConfig(new ServerConfigData(response));
|
||||
|
||||
this.parseBoostrapConfig(environment.getApiUrl(), response);
|
||||
|
||||
// Update the environment region
|
||||
if (
|
||||
newConfig?.environment?.cloudRegion != null &&
|
||||
@@ -241,4 +251,26 @@ export class DefaultConfigService implements ConfigService {
|
||||
private userConfigFor$(userId: UserId): Observable<ServerConfig | null> {
|
||||
return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$;
|
||||
}
|
||||
|
||||
private parseBoostrapConfig(hostname: string, response: ServerConfigResponse) {
|
||||
const bootstrap = response.communication?.bootstrap ?? null;
|
||||
|
||||
// Emit communication config so subscribers (e.g. ServerCommunicationConfigService) can persist it
|
||||
const communicationConfig: ServerCommunicationConfig =
|
||||
bootstrap?.type === "ssoCookieVendor"
|
||||
? {
|
||||
bootstrap: {
|
||||
type: "ssoCookieVendor",
|
||||
idpLoginUrl: bootstrap.idpLoginUrl,
|
||||
cookieName: bootstrap.cookieName,
|
||||
cookieDomain: bootstrap.cookieDomain,
|
||||
cookieValue: undefined,
|
||||
},
|
||||
}
|
||||
: { bootstrap: { type: "direct" } };
|
||||
this.serverCommunicationConfigSubject.next({
|
||||
hostname,
|
||||
config: communicationConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user