1
0
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:
Cy Okeke
2024-11-08 10:26:51 +01:00
156 changed files with 2330 additions and 1128 deletions

View File

@@ -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>;

View File

@@ -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>;
/**

View File

@@ -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;

View File

@@ -3,6 +3,7 @@
export type SharedFlags = {
showPasswordless?: boolean;
sdk?: boolean;
prereleaseBuild?: boolean;
};
// required to avoid linting errors when there are no flags

View File

@@ -16,6 +16,9 @@ describe("ServerConfigData", () => {
name: "test",
url: "https://test.com",
},
settings: {
disableUserRegistration: false,
},
environment: {
cloudRegion: Region.EU,
vault: "https://vault.com",

View File

@@ -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 {

View File

@@ -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);
});
});
});

View File

@@ -0,0 +1,7 @@
export class ServerSettings {
disableUserRegistration: boolean;
constructor(data?: ServerSettings) {
this.disableUserRegistration = data?.disableUserRegistration ?? false;
}
}

View File

@@ -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"));
}
}

View File

@@ -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) {

View File

@@ -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);
});
});
});
});

View File

@@ -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),
);
}
}

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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,

View File

@@ -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

View File

@@ -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);
}
/**