mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
Merge branch 'main' into pm-13345-Add-Remove-Bitwarden-Families-policy-in-Admin-Console
This commit is contained in:
@@ -17,7 +17,6 @@ export enum FeatureFlag {
|
||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||
MemberAccessReport = "ac-2059-member-access-report",
|
||||
TwoFactorComponentRefactor = "two-factor-component-refactor",
|
||||
EnableTimeThreshold = "PM-5864-dollar-threshold",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
@@ -36,6 +35,7 @@ export enum FeatureFlag {
|
||||
AccessIntelligence = "pm-13227-access-intelligence",
|
||||
Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions",
|
||||
LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split",
|
||||
CriticalApps = "pm-14466-risk-insights-critical-application",
|
||||
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||
[FeatureFlag.MemberAccessReport]: FALSE,
|
||||
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
|
||||
[FeatureFlag.EnableTimeThreshold]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
@@ -83,6 +82,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.AccessIntelligence]: FALSE,
|
||||
[FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE,
|
||||
[FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE,
|
||||
[FeatureFlag.CriticalApps]: FALSE,
|
||||
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SemVer } from "semver";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
import { Region } from "../environment.service";
|
||||
|
||||
import { ServerConfig } from "./server-config";
|
||||
@@ -10,6 +11,8 @@ import { ServerConfig } from "./server-config";
|
||||
export abstract class ConfigService {
|
||||
/** The server config of the currently active user */
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
/** The server settings of the currently active user */
|
||||
serverSettings$: Observable<ServerSettings | null>;
|
||||
/** The cloud region of the currently active user */
|
||||
cloudRegion$: Observable<Region>;
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ThirdPartyServerConfigData,
|
||||
EnvironmentServerConfigData,
|
||||
} from "../../models/data/server-config.data";
|
||||
import { ServerSettings } from "../../models/domain/server-settings";
|
||||
|
||||
const dayInMilliseconds = 24 * 3600 * 1000;
|
||||
|
||||
@@ -16,6 +17,7 @@ export class ServerConfig {
|
||||
environment?: EnvironmentServerConfigData;
|
||||
utcDate: Date;
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
settings: ServerSettings;
|
||||
|
||||
constructor(serverConfigData: ServerConfigData) {
|
||||
this.version = serverConfigData.version;
|
||||
@@ -24,6 +26,7 @@ export class ServerConfig {
|
||||
this.utcDate = new Date(serverConfigData.utcDate);
|
||||
this.environment = serverConfigData.environment;
|
||||
this.featureStates = serverConfigData.featureStates;
|
||||
this.settings = serverConfigData.settings;
|
||||
|
||||
if (this.server?.name == null && this.server?.url == null) {
|
||||
this.server = null;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export type SharedFlags = {
|
||||
showPasswordless?: boolean;
|
||||
sdk?: boolean;
|
||||
prereleaseBuild?: boolean;
|
||||
};
|
||||
|
||||
// required to avoid linting errors when there are no flags
|
||||
|
||||
@@ -16,6 +16,9 @@ describe("ServerConfigData", () => {
|
||||
name: "test",
|
||||
url: "https://test.com",
|
||||
},
|
||||
settings: {
|
||||
disableUserRegistration: false,
|
||||
},
|
||||
environment: {
|
||||
cloudRegion: Region.EU,
|
||||
vault: "https://vault.com",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
import { ServerSettings } from "../domain/server-settings";
|
||||
import {
|
||||
ServerConfigResponse,
|
||||
ThirdPartyServerConfigResponse,
|
||||
@@ -15,6 +16,7 @@ export class ServerConfigData {
|
||||
environment?: EnvironmentServerConfigData;
|
||||
utcDate: string;
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
settings: ServerSettings;
|
||||
|
||||
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
|
||||
this.version = serverConfigResponse?.version;
|
||||
@@ -27,6 +29,7 @@ export class ServerConfigData {
|
||||
? new EnvironmentServerConfigData(serverConfigResponse.environment)
|
||||
: null;
|
||||
this.featureStates = serverConfigResponse?.featureStates;
|
||||
this.settings = new ServerSettings(serverConfigResponse.settings);
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ServerSettings } from "./server-settings";
|
||||
|
||||
describe("ServerSettings", () => {
|
||||
describe("disableUserRegistration", () => {
|
||||
it("defaults disableUserRegistration to false", () => {
|
||||
const settings = new ServerSettings();
|
||||
expect(settings.disableUserRegistration).toBe(false);
|
||||
});
|
||||
|
||||
it("sets disableUserRegistration to true when provided", () => {
|
||||
const settings = new ServerSettings({ disableUserRegistration: true });
|
||||
expect(settings.disableUserRegistration).toBe(true);
|
||||
});
|
||||
|
||||
it("sets disableUserRegistration to false when provided", () => {
|
||||
const settings = new ServerSettings({ disableUserRegistration: false });
|
||||
expect(settings.disableUserRegistration).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export class ServerSettings {
|
||||
disableUserRegistration: boolean;
|
||||
|
||||
constructor(data?: ServerSettings) {
|
||||
this.disableUserRegistration = data?.disableUserRegistration ?? false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { Region } from "../../abstractions/environment.service";
|
||||
import { ServerSettings } from "../domain/server-settings";
|
||||
|
||||
export class ServerConfigResponse extends BaseResponse {
|
||||
version: string;
|
||||
@@ -8,6 +9,7 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
server: ThirdPartyServerConfigResponse;
|
||||
environment: EnvironmentServerConfigResponse;
|
||||
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
|
||||
settings: ServerSettings;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -21,6 +23,7 @@ export class ServerConfigResponse extends BaseResponse {
|
||||
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
|
||||
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
|
||||
this.featureStates = this.getResponseProperty("FeatureStates");
|
||||
this.settings = new ServerSettings(this.getResponseProperty("Settings"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Environment, EnvironmentService, Region } from "../../abstractions/envi
|
||||
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 { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state";
|
||||
|
||||
export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs")
|
||||
@@ -57,6 +58,8 @@ export class DefaultConfigService implements ConfigService {
|
||||
|
||||
serverConfig$: Observable<ServerConfig>;
|
||||
|
||||
serverSettings$: Observable<ServerSettings>;
|
||||
|
||||
cloudRegion$: Observable<Region>;
|
||||
|
||||
constructor(
|
||||
@@ -111,6 +114,10 @@ export class DefaultConfigService implements ConfigService {
|
||||
this.cloudRegion$ = this.serverConfig$.pipe(
|
||||
map((config) => config?.environment?.cloudRegion ?? Region.US),
|
||||
);
|
||||
|
||||
this.serverSettings$ = this.serverConfig$.pipe(
|
||||
map((config) => config?.settings ?? new ServerSettings()),
|
||||
);
|
||||
}
|
||||
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ConfigService } from "../abstractions/config/config.service";
|
||||
import { ServerSettings } from "../models/domain/server-settings";
|
||||
|
||||
import { DefaultServerSettingsService } from "./default-server-settings.service";
|
||||
|
||||
describe("DefaultServerSettingsService", () => {
|
||||
let service: DefaultServerSettingsService;
|
||||
let configServiceMock: { serverSettings$: any };
|
||||
|
||||
beforeEach(() => {
|
||||
configServiceMock = { serverSettings$: of() };
|
||||
service = new DefaultServerSettingsService(configServiceMock as ConfigService);
|
||||
});
|
||||
|
||||
describe("getSettings$", () => {
|
||||
it("returns server settings", () => {
|
||||
const mockSettings = new ServerSettings({ disableUserRegistration: true });
|
||||
configServiceMock.serverSettings$ = of(mockSettings);
|
||||
|
||||
service.getSettings$().subscribe((settings) => {
|
||||
expect(settings).toEqual(mockSettings);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isUserRegistrationDisabled$", () => {
|
||||
it("returns true when user registration is disabled", () => {
|
||||
const mockSettings = new ServerSettings({ disableUserRegistration: true });
|
||||
configServiceMock.serverSettings$ = of(mockSettings);
|
||||
|
||||
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
|
||||
expect(isDisabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when user registration is enabled", () => {
|
||||
const mockSettings = new ServerSettings({ disableUserRegistration: false });
|
||||
configServiceMock.serverSettings$ = of(mockSettings);
|
||||
|
||||
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
|
||||
expect(isDisabled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
import { ConfigService } from "../abstractions/config/config.service";
|
||||
import { ServerSettings } from "../models/domain/server-settings";
|
||||
|
||||
export class DefaultServerSettingsService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getSettings$(): Observable<ServerSettings> {
|
||||
return this.configService.serverSettings$;
|
||||
}
|
||||
|
||||
get isUserRegistrationDisabled$(): Observable<boolean> {
|
||||
return this.getSettings$().pipe(
|
||||
map((settings: ServerSettings) => settings.disableUserRegistration),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,7 @@ import { AppIdService } from "../platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "../platform/abstractions/environment.service";
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||
import { flagEnabled } from "../platform/misc/flags";
|
||||
import { Utils } from "../platform/misc/utils";
|
||||
import { SyncResponse } from "../platform/sync";
|
||||
import { UserId } from "../types/guid";
|
||||
@@ -583,7 +584,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> {
|
||||
return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, false);
|
||||
return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, true);
|
||||
}
|
||||
|
||||
postPurgeCiphers(
|
||||
@@ -1843,44 +1844,20 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
const requestUrl =
|
||||
apiUrl + Utils.normalizePath(pathParts[0]) + (pathParts.length > 1 ? `?${pathParts[1]}` : "");
|
||||
|
||||
const headers = new Headers({
|
||||
"Device-Type": this.deviceType,
|
||||
});
|
||||
if (this.customUserAgent != null) {
|
||||
headers.set("User-Agent", this.customUserAgent);
|
||||
}
|
||||
const [requestHeaders, requestBody] = await this.buildHeadersAndBody(
|
||||
authed,
|
||||
hasResponse,
|
||||
body,
|
||||
alterHeaders,
|
||||
);
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
cache: "no-store",
|
||||
credentials: await this.getCredentials(),
|
||||
method: method,
|
||||
};
|
||||
|
||||
if (authed) {
|
||||
const authHeader = await this.getActiveBearerToken();
|
||||
headers.set("Authorization", "Bearer " + authHeader);
|
||||
}
|
||||
if (body != null) {
|
||||
if (typeof body === "string") {
|
||||
requestInit.body = body;
|
||||
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
|
||||
} else if (typeof body === "object") {
|
||||
if (body instanceof FormData) {
|
||||
requestInit.body = body;
|
||||
} else {
|
||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||
requestInit.body = JSON.stringify(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasResponse) {
|
||||
headers.set("Accept", "application/json");
|
||||
}
|
||||
if (alterHeaders != null) {
|
||||
alterHeaders(headers);
|
||||
}
|
||||
|
||||
requestInit.headers = headers;
|
||||
requestInit.headers = requestHeaders;
|
||||
requestInit.body = requestBody;
|
||||
const response = await this.fetch(new Request(requestUrl, requestInit));
|
||||
|
||||
const responseType = response.headers.get("content-type");
|
||||
@@ -1897,6 +1874,51 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
private async buildHeadersAndBody(
|
||||
authed: boolean,
|
||||
hasResponse: boolean,
|
||||
body: any,
|
||||
alterHeaders: (headers: Headers) => void,
|
||||
): Promise<[Headers, any]> {
|
||||
let requestBody: any = null;
|
||||
const headers = new Headers({
|
||||
"Device-Type": this.deviceType,
|
||||
});
|
||||
|
||||
if (flagEnabled("prereleaseBuild")) {
|
||||
headers.set("Is-Prerelease", "1");
|
||||
}
|
||||
if (this.customUserAgent != null) {
|
||||
headers.set("User-Agent", this.customUserAgent);
|
||||
}
|
||||
if (hasResponse) {
|
||||
headers.set("Accept", "application/json");
|
||||
}
|
||||
if (alterHeaders != null) {
|
||||
alterHeaders(headers);
|
||||
}
|
||||
if (authed) {
|
||||
const authHeader = await this.getActiveBearerToken();
|
||||
headers.set("Authorization", "Bearer " + authHeader);
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
if (typeof body === "string") {
|
||||
requestBody = body;
|
||||
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
|
||||
} else if (typeof body === "object") {
|
||||
if (body instanceof FormData) {
|
||||
requestBody = body;
|
||||
} else {
|
||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||
requestBody = JSON.stringify(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [headers, requestBody];
|
||||
}
|
||||
|
||||
private async handleError(
|
||||
response: Response,
|
||||
tokenError: boolean,
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
|
||||
classifier: Classifier<State, Disclosed, Secret>;
|
||||
format: "plain" | "classified";
|
||||
options: UserKeyDefinitionOptions<State>;
|
||||
initial?: State;
|
||||
};
|
||||
|
||||
export function isObjectKey(key: any): key is ObjectKey<unknown> {
|
||||
|
||||
@@ -254,17 +254,18 @@ export class UserStateSubject<
|
||||
withConstraints,
|
||||
map(([loadedState, constraints]) => {
|
||||
// bypass nulls
|
||||
if (!loadedState) {
|
||||
if (!loadedState && !this.objectKey?.initial) {
|
||||
return {
|
||||
constraints: {} as Constraints<State>,
|
||||
state: null,
|
||||
} satisfies Constrained<State>;
|
||||
}
|
||||
|
||||
const unconstrained = loadedState ?? structuredClone(this.objectKey.initial);
|
||||
const calibration = isDynamic(constraints)
|
||||
? constraints.calibrate(loadedState)
|
||||
? constraints.calibrate(unconstrained)
|
||||
: constraints;
|
||||
const adjusted = calibration.adjust(loadedState);
|
||||
const adjusted = calibration.adjust(unconstrained);
|
||||
|
||||
return {
|
||||
constraints: calibration.constraints,
|
||||
|
||||
@@ -119,7 +119,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
* Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally).
|
||||
* @param cipher
|
||||
*/
|
||||
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<void>;
|
||||
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<Cipher>;
|
||||
/**
|
||||
* Bulk update collections for many ciphers with the server
|
||||
* @param orgId
|
||||
|
||||
@@ -880,9 +880,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return new Cipher(updated[cipher.id as CipherId], cipher.localData);
|
||||
}
|
||||
|
||||
async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<void> {
|
||||
async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<Cipher> {
|
||||
const request = new CipherCollectionsRequest(cipher.collectionIds);
|
||||
await this.apiService.putCipherCollectionsAdmin(cipher.id, request);
|
||||
const response = await this.apiService.putCipherCollectionsAdmin(cipher.id, request);
|
||||
const data = new CipherData(response);
|
||||
return new Cipher(data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user