mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 22:44:11 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"devFlags": {},
|
||||
"flags": {
|
||||
"showPasswordless": true,
|
||||
"accountSwitching": false
|
||||
"accountSwitching": false,
|
||||
"sdk": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
|
||||
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
|
||||
import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
|
||||
@@ -89,6 +90,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
@@ -121,6 +123,8 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
@@ -227,6 +231,7 @@ import AutofillService from "../autofill/services/autofill.service";
|
||||
import { SafariApp } from "../browser/safariApp";
|
||||
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
||||
@@ -244,6 +249,7 @@ import { LocalBackedSessionStorageService } from "../platform/services/local-bac
|
||||
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||
import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service";
|
||||
import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory";
|
||||
import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service";
|
||||
import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service";
|
||||
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
||||
@@ -363,6 +369,7 @@ export default class MainBackground {
|
||||
syncServiceListener: SyncServiceListener;
|
||||
themeStateService: DefaultThemeStateService;
|
||||
autoSubmitLoginBackground: AutoSubmitLoginBackground;
|
||||
sdkService: SdkService;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
@@ -570,6 +577,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.stateProvider,
|
||||
this.accountService,
|
||||
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
|
||||
);
|
||||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||
|
||||
@@ -717,6 +725,16 @@ export default class MainBackground {
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
const sdkClientFactory = flagEnabled("sdk")
|
||||
? new BrowserSdkClientFactory()
|
||||
: new NoopSdkClientFactory();
|
||||
this.sdkService = new DefaultSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.apiService,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
@@ -1313,6 +1331,20 @@ export default class MainBackground {
|
||||
|
||||
await this.initOverlayAndTabsBackground();
|
||||
|
||||
if (flagEnabled("sdk")) {
|
||||
// Warn if the SDK for some reason can't be initialized
|
||||
let supported = false;
|
||||
try {
|
||||
supported = await firstValueFrom(this.sdkService.supported$);
|
||||
} catch (e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
if (!supported) {
|
||||
this.sdkService.failedToInitialize().catch(this.logService.error);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
await this.refreshBadge();
|
||||
|
||||
@@ -39,8 +39,7 @@
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_icon": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Region, RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -14,8 +14,9 @@ export class BrowserEnvironmentService extends DefaultEnvironmentService {
|
||||
private logService: LogService,
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountService,
|
||||
additionalRegionConfigs: RegionConfig[] = [],
|
||||
) {
|
||||
super(stateProvider, accountService);
|
||||
super(stateProvider, accountService, additionalRegionConfigs);
|
||||
}
|
||||
|
||||
async hasManagedEnvironment(): Promise<boolean> {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import type { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
// https://stackoverflow.com/a/47880734
|
||||
const supported = (() => {
|
||||
try {
|
||||
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
|
||||
const module = new WebAssembly.Module(
|
||||
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00),
|
||||
);
|
||||
if (module instanceof WebAssembly.Module) {
|
||||
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (supported) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("WebAssembly is supported in this environment");
|
||||
import("./wasm");
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("WebAssembly is not supported in this environment");
|
||||
import("./fallback");
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK client factory with a js fallback for when WASM is not supported.
|
||||
*
|
||||
* Works both in popup and service worker.
|
||||
*/
|
||||
export class BrowserSdkClientFactory implements SdkClientFactory {
|
||||
async createSdkClient(
|
||||
...args: ConstructorParameters<typeof BitwardenClient>
|
||||
): Promise<BitwardenClient> {
|
||||
return Promise.resolve((globalThis as any).init_sdk(...args));
|
||||
}
|
||||
}
|
||||
8
apps/browser/src/platform/services/sdk/fallback.ts
Normal file
8
apps/browser/src/platform/services/sdk/fallback.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as sdk from "@bitwarden/sdk-internal";
|
||||
import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js";
|
||||
|
||||
(globalThis as any).init_sdk = (...args: ConstructorParameters<typeof sdk.BitwardenClient>) => {
|
||||
(sdk as any).init(wasm);
|
||||
|
||||
return new sdk.BitwardenClient(...args);
|
||||
};
|
||||
8
apps/browser/src/platform/services/sdk/wasm.ts
Normal file
8
apps/browser/src/platform/services/sdk/wasm.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as sdk from "@bitwarden/sdk-internal";
|
||||
import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm";
|
||||
|
||||
(globalThis as any).init_sdk = (...args: ConstructorParameters<typeof sdk.BitwardenClient>) => {
|
||||
(sdk as any).init(wasm);
|
||||
|
||||
return new sdk.BitwardenClient(...args);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, catchError, of } from "rxjs";
|
||||
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -8,7 +9,9 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -20,6 +23,7 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
|
||||
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
|
||||
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
||||
@@ -62,7 +66,28 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private animationControlService: AnimationControlService,
|
||||
) {}
|
||||
private logService: LogService,
|
||||
private sdkService: SdkService,
|
||||
) {
|
||||
if (flagEnabled("sdk")) {
|
||||
// Warn if the SDK for some reason can't be initialized
|
||||
this.sdkService.supported$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
catchError(() => {
|
||||
return of(false);
|
||||
}),
|
||||
)
|
||||
.subscribe((supported) => {
|
||||
if (!supported) {
|
||||
this.logService.debug("SDK is not supported");
|
||||
this.sdkService.failedToInitialize().catch(this.logService.error);
|
||||
} else {
|
||||
this.logService.debug("SDK is supported");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
initPopupClosedListener();
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
CLIENT_TYPE,
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular";
|
||||
@@ -57,6 +58,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/ke
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
@@ -65,9 +67,11 @@ import {
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { flagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import {
|
||||
@@ -113,6 +117,7 @@ import BrowserLocalStorageService from "../../platform/services/browser-local-st
|
||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||
import I18nService from "../../platform/services/i18n.service";
|
||||
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
|
||||
import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory";
|
||||
import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service";
|
||||
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
|
||||
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
||||
@@ -197,7 +202,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: BrowserEnvironmentService,
|
||||
useClass: BrowserEnvironmentService,
|
||||
deps: [LogService, StateProvider, AccountServiceAbstraction],
|
||||
deps: [LogService, StateProvider, AccountServiceAbstraction, ENV_ADDITIONAL_REGIONS],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: I18nServiceAbstraction,
|
||||
@@ -571,6 +576,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ForegroundLockService,
|
||||
deps: [MessageSender, MessageListener],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkClientFactory,
|
||||
useClass: flagEnabled("sdk") ? BrowserSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -122,7 +122,7 @@ const moduleRules = [
|
||||
loader: "@ngtools/webpack",
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
test: /argon2(-simd)?\.wasm$/,
|
||||
loader: "base64-loader",
|
||||
type: "javascript/auto",
|
||||
},
|
||||
@@ -320,9 +320,12 @@ const mainConfig = {
|
||||
clean: true,
|
||||
},
|
||||
module: {
|
||||
noParse: /\.wasm$/,
|
||||
noParse: /argon2(-simd)?\.wasm$/,
|
||||
rules: moduleRules,
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
@@ -395,12 +398,15 @@ if (manifestVersion == 2) {
|
||||
loader: "ts-loader",
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
test: /argon2(-simd)?\.wasm$/,
|
||||
loader: "base64-loader",
|
||||
type: "javascript/auto",
|
||||
},
|
||||
],
|
||||
noParse: /\.wasm$/,
|
||||
noParse: /argon2(-simd)?\.wasm$/,
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
|
||||
5
apps/cli/config/base.json
Normal file
5
apps/cli/config/base.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"flags": {
|
||||
"sdk": true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,27 @@
|
||||
function load(envName) {
|
||||
const base = require("./base.json");
|
||||
const env = loadConfig(envName);
|
||||
const local = loadConfig("local");
|
||||
|
||||
return {
|
||||
...loadConfig(envName),
|
||||
...loadConfig("local"),
|
||||
...base,
|
||||
...env,
|
||||
...local,
|
||||
dev: {
|
||||
...base.dev,
|
||||
...env.dev,
|
||||
...local.dev,
|
||||
},
|
||||
flags: {
|
||||
...base.flags,
|
||||
...env.flags,
|
||||
...local.flags,
|
||||
},
|
||||
devFlags: {
|
||||
...base.devFlags,
|
||||
...env.devFlags,
|
||||
...local.devFlags,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
moduleNameMapper: {
|
||||
"@bitwarden/common/platform/services/sdk/default-sdk-client-factory":
|
||||
"<rootDir>/../../libs/common/spec/jest-sdk-client-factory",
|
||||
...pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,8 +59,12 @@ import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/bill
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
@@ -83,6 +87,9 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
@@ -148,6 +155,7 @@ import {
|
||||
VaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service";
|
||||
import { ConsoleLogService } from "../platform/services/console-log.service";
|
||||
import { I18nService } from "../platform/services/i18n.service";
|
||||
@@ -246,6 +254,7 @@ export class ServiceContainer {
|
||||
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
||||
kdfConfigService: KdfConfigServiceAbstraction;
|
||||
taskSchedulerService: TaskSchedulerService;
|
||||
sdkService: SdkService;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -346,6 +355,7 @@ export class ServiceContainer {
|
||||
this.environmentService = new DefaultEnvironmentService(
|
||||
this.stateProvider,
|
||||
this.accountService,
|
||||
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
|
||||
);
|
||||
|
||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||
@@ -518,6 +528,17 @@ export class ServiceContainer {
|
||||
this.globalStateProvider,
|
||||
);
|
||||
|
||||
const sdkClientFactory = flagEnabled("sdk")
|
||||
? new DefaultSdkClientFactory()
|
||||
: new NoopSdkClientFactory();
|
||||
this.sdkService = new DefaultSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.apiService,
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
@@ -826,5 +847,19 @@ export class ServiceContainer {
|
||||
}
|
||||
|
||||
this.inited = true;
|
||||
|
||||
if (flagEnabled("sdk")) {
|
||||
// Warn if the SDK for some reason can't be initialized
|
||||
let supported = false;
|
||||
try {
|
||||
supported = await firstValueFrom(this.sdkService.supported$);
|
||||
} catch (e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
if (!supported) {
|
||||
this.sdkService.failedToInitialize().catch(this.logService.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2016",
|
||||
"module": "es6",
|
||||
"module": "ES2020",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
@@ -37,8 +37,10 @@ const plugins = [
|
||||
contextRegExp: /node-fetch/,
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
ENV: ENV,
|
||||
BWCLI_ENV: ENV,
|
||||
FLAGS: envConfig.flags,
|
||||
DEV_FLAGS: envConfig.devFlags,
|
||||
}),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /canvas/,
|
||||
@@ -79,6 +81,9 @@ const webpackConfig = {
|
||||
allowlist: [/@bitwarden/],
|
||||
}),
|
||||
],
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = webpackConfig;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"devFlags": {},
|
||||
"flags": {}
|
||||
"flags": {
|
||||
"sdk": true
|
||||
},
|
||||
"devFlags": {}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs";
|
||||
import { catchError, filter, firstValueFrom, map, of, Subject, takeUntil, timeout } from "rxjs";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -21,7 +22,6 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
@@ -38,6 +38,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize";
|
||||
@@ -56,6 +57,7 @@ import { BiometricStateService } from "@bitwarden/key-management";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginApprovalComponent } from "../auth/login/login-approval.component";
|
||||
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { PremiumComponent } from "../vault/app/accounts/premium.component";
|
||||
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
|
||||
|
||||
@@ -150,9 +152,28 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private dialogService: DialogService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private providerService: ProviderService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
private sdkService: SdkService,
|
||||
) {
|
||||
if (flagEnabled("sdk")) {
|
||||
// Warn if the SDK for some reason can't be initialized
|
||||
this.sdkService.supported$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
catchError(() => {
|
||||
return of(false);
|
||||
}),
|
||||
)
|
||||
.subscribe((supported) => {
|
||||
if (!supported) {
|
||||
this.logService.debug("SDK is not supported");
|
||||
this.sdkService.failedToInitialize().catch(this.logService.error);
|
||||
} else {
|
||||
this.logService.debug("SDK is supported");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
} from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
@@ -60,6 +61,8 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||
@@ -73,6 +76,7 @@ import { BiometricStateService, BiometricsService } from "@bitwarden/key-managem
|
||||
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
|
||||
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
|
||||
@@ -302,6 +306,11 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkClientFactory,
|
||||
useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; style-src 'self' 'unsafe-inline';
|
||||
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: *; child-src *; frame-src *; connect-src *;"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- For dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
@@ -152,7 +153,11 @@ export class Main {
|
||||
new DefaultDerivedStateProvider(),
|
||||
);
|
||||
|
||||
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
||||
this.environmentService = new DefaultEnvironmentService(
|
||||
stateProvider,
|
||||
accountService,
|
||||
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
|
||||
);
|
||||
|
||||
this.migrationRunner = new MigrationRunner(
|
||||
this.storageService,
|
||||
|
||||
@@ -331,7 +331,7 @@ export class NativeMessagingMain {
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
|
||||
if (isDev()) {
|
||||
return path.join(
|
||||
const devPath = path.join(
|
||||
this.appPath,
|
||||
"..",
|
||||
"desktop_native",
|
||||
@@ -339,6 +339,12 @@ export class NativeMessagingMain {
|
||||
"debug",
|
||||
`desktop_proxy${ext}`,
|
||||
);
|
||||
|
||||
// isDev() returns true when using a production build with ELECTRON_IS_DEV=1,
|
||||
// so we need to fall back to the prod binary if the dev binary doesn't exist.
|
||||
if (existsSync(devPath)) {
|
||||
return devPath;
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
|
||||
|
||||
@@ -42,7 +42,7 @@ const common = {
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
test: /argon2(-simd)?\.wasm$/,
|
||||
loader: "base64-loader",
|
||||
type: "javascript/auto",
|
||||
},
|
||||
@@ -143,11 +143,15 @@ const renderer = {
|
||||
parser: { system: true },
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
test: /argon2(-simd)?\.wasm$/,
|
||||
loader: "base64-loader",
|
||||
type: "javascript/auto",
|
||||
},
|
||||
],
|
||||
noParse: /argon2(-simd)?\.wasm$/,
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
plugins: [
|
||||
new AngularWebpackPlugin({
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"allowedHosts": "auto"
|
||||
},
|
||||
"flags": {
|
||||
"showPasswordless": false
|
||||
}
|
||||
"showPasswordless": false,
|
||||
"sdk": true
|
||||
},
|
||||
"devFlags": {}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,19 @@ module.exports = {
|
||||
...sharedConfig,
|
||||
preset: "jest-preset-angular",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(
|
||||
// lets us use @bitwarden/common/spec in web tests
|
||||
{ "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||
{
|
||||
prefix: "<rootDir>/",
|
||||
},
|
||||
),
|
||||
moduleNameMapper: {
|
||||
// Replace ESM SDK with Node compatible SDK
|
||||
"@bitwarden/common/platform/services/sdk/default-sdk-client-factory":
|
||||
"<rootDir>/../../libs/common/spec/jest-sdk-client-factory",
|
||||
...pathsToModuleNameMapper(
|
||||
{
|
||||
// lets us use @bitwarden/common/spec in web tests
|
||||
"@bitwarden/common/spec": ["../../libs/common/spec"],
|
||||
...(compilerOptions?.paths ?? {}),
|
||||
},
|
||||
{
|
||||
prefix: "<rootDir>/",
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import * as jq from "jquery";
|
||||
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
|
||||
import { Subject, filter, firstValueFrom, map, takeUntil, timeout, catchError, of } from "rxjs";
|
||||
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
@@ -19,7 +20,9 @@ import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broa
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
@@ -30,6 +33,8 @@ import { DialogService, ToastOptions, ToastService } from "@bitwarden/components
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { flagEnabled } from "../utils/flags";
|
||||
|
||||
import { PolicyListService } from "./admin-console/core/policy-list.service";
|
||||
import {
|
||||
DisableSendPolicy,
|
||||
@@ -85,7 +90,28 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
private logService: LogService,
|
||||
private sdkService: SdkService,
|
||||
) {
|
||||
if (flagEnabled("sdk")) {
|
||||
// Warn if the SDK for some reason can't be initialized
|
||||
this.sdkService.supported$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
catchError(() => {
|
||||
return of(false);
|
||||
}),
|
||||
)
|
||||
.subscribe((supported) => {
|
||||
if (!supported) {
|
||||
this.logService.debug("SDK is not supported");
|
||||
this.sdkService.failedToInitialize().catch(this.logService.error);
|
||||
} else {
|
||||
this.logService.debug("SDK is supported");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/sa
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
LOCALES_DIRECTORY,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||
@@ -42,11 +43,15 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Urls,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
@@ -54,6 +59,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
|
||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -68,6 +74,7 @@ import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/va
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
import {
|
||||
WebSetPasswordJitService,
|
||||
@@ -80,10 +87,12 @@ import { I18nService } from "../core/i18n.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||
import { WebSdkClientFactory } from "../platform/web-sdk-client-factory";
|
||||
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
||||
|
||||
import { EventService } from "./event.service";
|
||||
import { InitService } from "./init.service";
|
||||
import { ENV_URLS } from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { RouterService } from "./router.service";
|
||||
import { WebFileDownloadService } from "./web-file-download.service";
|
||||
@@ -173,10 +182,14 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebMigrationRunner,
|
||||
deps: [AbstractStorageService, LogService, MigrationBuilderService, WindowStorageService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ENV_URLS,
|
||||
useValue: process.env.URLS as Urls,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EnvironmentService,
|
||||
useClass: WebEnvironmentService,
|
||||
deps: [WINDOW, StateProvider, AccountService, Router],
|
||||
deps: [WINDOW, StateProvider, AccountService, ENV_ADDITIONAL_REGIONS, Router, ENV_URLS],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BiometricsService,
|
||||
@@ -236,6 +249,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultCollectionAdminService,
|
||||
deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkClientFactory,
|
||||
useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
10
apps/web/src/app/core/injection-tokens.ts
Normal file
10
apps/web/src/app/core/injection-tokens.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Put web specific injection tokens here
|
||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { Urls } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
/**
|
||||
* Injection token for injecting the NodeJS process.env urls into services.
|
||||
* Using an injection token allows services to be tested without needing to
|
||||
* mock the process.env.
|
||||
*/
|
||||
export const ENV_URLS = new SafeInjectionToken<Urls>("ENV_URLS");
|
||||
457
apps/web/src/app/platform/web-environment.service.spec.ts
Normal file
457
apps/web/src/app/platform/web-environment.service.spec.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Region, Urls } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PRODUCTION_REGIONS } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
WebCloudEnvironment,
|
||||
WebEnvironmentService,
|
||||
WebRegionConfig,
|
||||
} from "./web-environment.service";
|
||||
|
||||
describe("WebEnvironmentService", () => {
|
||||
let service: WebEnvironmentService;
|
||||
|
||||
let window: MockProxy<Window>;
|
||||
|
||||
let stateProvider: FakeStateProvider;
|
||||
let accountService: FakeAccountService;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
|
||||
describe("Production Environment", () => {
|
||||
describe("US Region", () => {
|
||||
const mockInitialProdUSUrls = {
|
||||
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",
|
||||
} as Urls;
|
||||
|
||||
const mockProdUSBaseUrl = "https://vault.bitwarden.com";
|
||||
|
||||
const expectedProdUSUrls: Urls = {
|
||||
...mockInitialProdUSUrls,
|
||||
base: mockProdUSBaseUrl,
|
||||
};
|
||||
|
||||
const expectedModifiedScimUrl = expectedProdUSUrls.scim + "/v2";
|
||||
const expectedSendUrl = "https://send.bitwarden.com/#";
|
||||
|
||||
const PROD_US_REGION = PRODUCTION_REGIONS.find((r) => r.key === Region.US);
|
||||
|
||||
const prodUSEnv = new WebCloudEnvironment(PROD_US_REGION, expectedProdUSUrls);
|
||||
|
||||
beforeEach(() => {
|
||||
window = mock<Window>();
|
||||
|
||||
window.location = {
|
||||
origin: mockProdUSBaseUrl,
|
||||
href: mockProdUSBaseUrl + "/#/example",
|
||||
} as Location;
|
||||
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
router = mock<Router>();
|
||||
|
||||
(router as any).url = "";
|
||||
|
||||
service = new WebEnvironmentService(
|
||||
window,
|
||||
stateProvider,
|
||||
accountService,
|
||||
[], // no additional region configs required for prod envs
|
||||
router,
|
||||
mockInitialProdUSUrls,
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes the environment with the US production urls", async () => {
|
||||
const env = await firstValueFrom(service.environment$);
|
||||
|
||||
expect(env).toEqual(prodUSEnv);
|
||||
|
||||
expect(env.getRegion()).toEqual(Region.US);
|
||||
expect(env.getUrls()).toEqual(expectedProdUSUrls);
|
||||
expect(env.isCloud()).toBeTruthy();
|
||||
|
||||
expect(env.getApiUrl()).toEqual(expectedProdUSUrls.api);
|
||||
expect(env.getIdentityUrl()).toEqual(expectedProdUSUrls.identity);
|
||||
expect(env.getIconsUrl()).toEqual(expectedProdUSUrls.icons);
|
||||
expect(env.getWebVaultUrl()).toEqual(expectedProdUSUrls.webVault);
|
||||
expect(env.getNotificationsUrl()).toEqual(expectedProdUSUrls.notifications);
|
||||
expect(env.getEventsUrl()).toEqual(expectedProdUSUrls.events);
|
||||
|
||||
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
|
||||
expect(env.getSendUrl()).toEqual(expectedSendUrl);
|
||||
|
||||
expect(env.getHostname()).toEqual(PROD_US_REGION.domain);
|
||||
});
|
||||
|
||||
describe("setEnvironment", () => {
|
||||
it("throws an error when trying to set the environment to self-hosted", async () => {
|
||||
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
|
||||
"setEnvironment does not work in web for self-hosted.",
|
||||
);
|
||||
});
|
||||
|
||||
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
|
||||
const urls = await service.setEnvironment(Region.US);
|
||||
expect(urls).toEqual(expectedProdUSUrls);
|
||||
});
|
||||
|
||||
it("errors if the selected region is unknown", async () => {
|
||||
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
|
||||
"The selected region is not known as an available region.",
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
|
||||
const routeAndQueryParams = "/signup?example=1&another=2";
|
||||
(router as any).url = routeAndQueryParams;
|
||||
|
||||
const newRegion = Region.EU;
|
||||
const newRegionConfig = PRODUCTION_REGIONS.find((r) => r.key === newRegion);
|
||||
|
||||
await service.setEnvironment(newRegion);
|
||||
|
||||
expect(window.location.href).toEqual(
|
||||
newRegionConfig.urls.webVault + "/#" + routeAndQueryParams,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EU Region", () => {
|
||||
const mockInitialProdEUUrls = {
|
||||
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",
|
||||
} as Urls;
|
||||
|
||||
const mockProdEUBaseUrl = "https://vault.bitwarden.eu";
|
||||
|
||||
const expectedProdEUUrls: Urls = {
|
||||
...mockInitialProdEUUrls,
|
||||
base: mockProdEUBaseUrl,
|
||||
};
|
||||
|
||||
const expectedModifiedScimUrl = expectedProdEUUrls.scim + "/v2";
|
||||
const expectedSendUrl = expectedProdEUUrls.webVault + "/#/send/";
|
||||
|
||||
const prodEURegionConfig = PRODUCTION_REGIONS.find((r) => r.key === Region.EU);
|
||||
|
||||
const prodEUEnv = new WebCloudEnvironment(prodEURegionConfig, expectedProdEUUrls);
|
||||
|
||||
beforeEach(() => {
|
||||
window = mock<Window>();
|
||||
|
||||
window.location = {
|
||||
origin: mockProdEUBaseUrl,
|
||||
href: mockProdEUBaseUrl + "/#/example",
|
||||
} as Location;
|
||||
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
router = mock<Router>();
|
||||
|
||||
service = new WebEnvironmentService(
|
||||
window,
|
||||
stateProvider,
|
||||
accountService,
|
||||
[], // no additional region configs required for prod envs
|
||||
router,
|
||||
mockInitialProdEUUrls,
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes the environment to be the prod EU environment", async () => {
|
||||
const env = await firstValueFrom(service.environment$);
|
||||
|
||||
expect(env).toEqual(prodEUEnv);
|
||||
expect(env.getRegion()).toEqual(Region.EU);
|
||||
expect(env.getUrls()).toEqual(expectedProdEUUrls);
|
||||
expect(env.isCloud()).toBeTruthy();
|
||||
|
||||
expect(env.getApiUrl()).toEqual(expectedProdEUUrls.api);
|
||||
expect(env.getIdentityUrl()).toEqual(expectedProdEUUrls.identity);
|
||||
expect(env.getIconsUrl()).toEqual(expectedProdEUUrls.icons);
|
||||
expect(env.getWebVaultUrl()).toEqual(expectedProdEUUrls.webVault);
|
||||
expect(env.getNotificationsUrl()).toEqual(expectedProdEUUrls.notifications);
|
||||
expect(env.getEventsUrl()).toEqual(expectedProdEUUrls.events);
|
||||
|
||||
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
|
||||
expect(env.getSendUrl()).toEqual(expectedSendUrl);
|
||||
|
||||
expect(env.getHostname()).toEqual(prodEURegionConfig.domain);
|
||||
});
|
||||
|
||||
describe("setEnvironment", () => {
|
||||
it("throws an error when trying to set the environment to self-hosted", async () => {
|
||||
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
|
||||
"setEnvironment does not work in web for self-hosted.",
|
||||
);
|
||||
});
|
||||
|
||||
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
|
||||
const urls = await service.setEnvironment(Region.EU);
|
||||
expect(urls).toEqual(expectedProdEUUrls);
|
||||
});
|
||||
|
||||
it("errors if the selected region is unknown", async () => {
|
||||
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
|
||||
"The selected region is not known as an available region.",
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
|
||||
const routeAndQueryParams = "/signup?example=1&another=2";
|
||||
(router as any).url = routeAndQueryParams;
|
||||
|
||||
const newRegion = Region.US;
|
||||
const newRegionConfig = PRODUCTION_REGIONS.find((r) => r.key === newRegion);
|
||||
|
||||
await service.setEnvironment(newRegion);
|
||||
|
||||
expect(window.location.href).toEqual(
|
||||
newRegionConfig.urls.webVault + "/#" + routeAndQueryParams,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("QA Environment", () => {
|
||||
const QA_US_REGION_KEY = "USQA";
|
||||
const QA_US_WEB_REGION_CONFIG = {
|
||||
key: QA_US_REGION_KEY,
|
||||
domain: "qa.bitwarden.pw",
|
||||
urls: {
|
||||
webVault: "https://vault.qa.bitwarden.pw",
|
||||
},
|
||||
} as WebRegionConfig;
|
||||
|
||||
const QA_EU_REGION_KEY = "EUQA";
|
||||
const QA_EU_WEB_REGION_CONFIG = {
|
||||
key: QA_EU_REGION_KEY,
|
||||
domain: "euqa.bitwarden.pw",
|
||||
urls: {
|
||||
webVault: "https://vault.euqa.bitwarden.pw",
|
||||
},
|
||||
} as WebRegionConfig;
|
||||
|
||||
const additionalRegionConfigs: WebRegionConfig[] = [
|
||||
QA_US_WEB_REGION_CONFIG,
|
||||
QA_EU_WEB_REGION_CONFIG,
|
||||
];
|
||||
|
||||
describe("US Region", () => {
|
||||
const initial_QA_US_Urls = {
|
||||
icons: "https://icons.qa.bitwarden.pw",
|
||||
notifications: "https://notifications.qa.bitwarden.pw",
|
||||
scim: "https://scim.qa.bitwarden.pw",
|
||||
} as Urls;
|
||||
|
||||
const mock_QA_US_BaseUrl = "https://vault.qa.bitwarden.pw";
|
||||
|
||||
const expected_QA_US_Urls: Urls = {
|
||||
...initial_QA_US_Urls,
|
||||
base: mock_QA_US_BaseUrl,
|
||||
};
|
||||
|
||||
const expectedModifiedScimUrl = expected_QA_US_Urls.scim + "/v2";
|
||||
|
||||
const expectedSendUrl = QA_US_WEB_REGION_CONFIG.urls.webVault + "/#/send/";
|
||||
|
||||
const QA_US_Env = new WebCloudEnvironment(QA_US_WEB_REGION_CONFIG, expected_QA_US_Urls);
|
||||
|
||||
beforeEach(() => {
|
||||
window = mock<Window>();
|
||||
window.location = {
|
||||
origin: mock_QA_US_BaseUrl,
|
||||
href: mock_QA_US_BaseUrl + "/#/example",
|
||||
} as Location;
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
router = mock<Router>();
|
||||
(router as any).url = "";
|
||||
service = new WebEnvironmentService(
|
||||
window,
|
||||
stateProvider,
|
||||
accountService,
|
||||
additionalRegionConfigs,
|
||||
router,
|
||||
initial_QA_US_Urls,
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes the environment to be the QA US environment", async () => {
|
||||
const env = await firstValueFrom(service.environment$);
|
||||
|
||||
expect(env).toEqual(QA_US_Env);
|
||||
expect(env.getRegion()).toEqual(QA_US_REGION_KEY);
|
||||
expect(env.getUrls()).toEqual(expected_QA_US_Urls);
|
||||
expect(env.isCloud()).toBeTruthy();
|
||||
|
||||
expect(env.getApiUrl()).toEqual(expected_QA_US_Urls.base + "/api");
|
||||
expect(env.getIdentityUrl()).toEqual(expected_QA_US_Urls.base + "/identity");
|
||||
expect(env.getIconsUrl()).toEqual(expected_QA_US_Urls.icons);
|
||||
|
||||
expect(env.getWebVaultUrl()).toEqual(QA_US_WEB_REGION_CONFIG.urls.webVault);
|
||||
|
||||
expect(env.getNotificationsUrl()).toEqual(expected_QA_US_Urls.notifications);
|
||||
expect(env.getEventsUrl()).toEqual(expected_QA_US_Urls.base + "/events");
|
||||
|
||||
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
|
||||
|
||||
expect(env.getSendUrl()).toEqual(expectedSendUrl);
|
||||
|
||||
expect(env.getHostname()).toEqual(QA_US_WEB_REGION_CONFIG.domain);
|
||||
});
|
||||
|
||||
describe("setEnvironment", () => {
|
||||
it("throws an error when trying to set the environment to self-hosted", async () => {
|
||||
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
|
||||
"setEnvironment does not work in web for self-hosted.",
|
||||
);
|
||||
});
|
||||
|
||||
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
|
||||
const urls = await service.setEnvironment(QA_US_REGION_KEY);
|
||||
expect(urls).toEqual(expected_QA_US_Urls);
|
||||
});
|
||||
|
||||
it("errors if the selected region is unknown", async () => {
|
||||
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
|
||||
"The selected region is not known as an available region.",
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
|
||||
const routeAndQueryParams = "/signup?example=1&another=2";
|
||||
(router as any).url = routeAndQueryParams;
|
||||
|
||||
await service.setEnvironment(QA_EU_REGION_KEY);
|
||||
|
||||
expect(window.location.href).toEqual(
|
||||
QA_EU_WEB_REGION_CONFIG.urls.webVault + "/#" + routeAndQueryParams,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EU Region", () => {
|
||||
const initial_QA_EU_Urls = {
|
||||
icons: "https://icons.euqa.bitwarden.pw",
|
||||
notifications: "https://notifications.euqa.bitwarden.pw",
|
||||
scim: "https://scim.euqa.bitwarden.pw",
|
||||
} as Urls;
|
||||
|
||||
const mock_QA_EU_BaseUrl = "https://vault.euqa.bitwarden.pw";
|
||||
|
||||
const expected_QA_EU_Urls: Urls = {
|
||||
...initial_QA_EU_Urls,
|
||||
base: mock_QA_EU_BaseUrl,
|
||||
};
|
||||
|
||||
const expectedModifiedScimUrl = expected_QA_EU_Urls.scim + "/v2";
|
||||
|
||||
const expectedSendUrl = QA_EU_WEB_REGION_CONFIG.urls.webVault + "/#/send/";
|
||||
|
||||
const QA_EU_Env = new WebCloudEnvironment(QA_EU_WEB_REGION_CONFIG, expected_QA_EU_Urls);
|
||||
|
||||
beforeEach(() => {
|
||||
window = mock<Window>();
|
||||
window.location = {
|
||||
origin: mock_QA_EU_BaseUrl,
|
||||
href: mock_QA_EU_BaseUrl + "/#/example",
|
||||
} as Location;
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
router = mock<Router>();
|
||||
(router as any).url = "";
|
||||
service = new WebEnvironmentService(
|
||||
window,
|
||||
stateProvider,
|
||||
accountService,
|
||||
additionalRegionConfigs,
|
||||
router,
|
||||
initial_QA_EU_Urls,
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes the environment to be the QA US environment", async () => {
|
||||
const env = await firstValueFrom(service.environment$);
|
||||
|
||||
expect(env).toEqual(QA_EU_Env);
|
||||
expect(env.getRegion()).toEqual(QA_EU_REGION_KEY);
|
||||
expect(env.getUrls()).toEqual(expected_QA_EU_Urls);
|
||||
expect(env.isCloud()).toBeTruthy();
|
||||
|
||||
expect(env.getApiUrl()).toEqual(expected_QA_EU_Urls.base + "/api");
|
||||
expect(env.getIdentityUrl()).toEqual(expected_QA_EU_Urls.base + "/identity");
|
||||
expect(env.getIconsUrl()).toEqual(expected_QA_EU_Urls.icons);
|
||||
|
||||
expect(env.getWebVaultUrl()).toEqual(QA_EU_WEB_REGION_CONFIG.urls.webVault);
|
||||
|
||||
expect(env.getNotificationsUrl()).toEqual(expected_QA_EU_Urls.notifications);
|
||||
expect(env.getEventsUrl()).toEqual(expected_QA_EU_Urls.base + "/events");
|
||||
|
||||
expect(env.getScimUrl()).toEqual(expectedModifiedScimUrl);
|
||||
|
||||
expect(env.getSendUrl()).toEqual(expectedSendUrl);
|
||||
|
||||
expect(env.getHostname()).toEqual(QA_EU_WEB_REGION_CONFIG.domain);
|
||||
});
|
||||
|
||||
describe("setEnvironment", () => {
|
||||
it("throws an error when trying to set the environment to self-hosted", async () => {
|
||||
await expect(service.setEnvironment(Region.SelfHosted)).rejects.toThrow(
|
||||
"setEnvironment does not work in web for self-hosted.",
|
||||
);
|
||||
});
|
||||
|
||||
it("only returns the current env's urls when trying to set the environment to the current region", async () => {
|
||||
const urls = await service.setEnvironment(QA_EU_REGION_KEY);
|
||||
expect(urls).toEqual(expected_QA_EU_Urls);
|
||||
});
|
||||
|
||||
it("errors if the selected region is unknown", async () => {
|
||||
await expect(service.setEnvironment("unknown" as Region)).rejects.toThrow(
|
||||
"The selected region is not known as an available region.",
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the window location to a new region's web vault url and preserves any query params", async () => {
|
||||
const routeAndQueryParams = "/signup?example=1&another=2";
|
||||
(router as any).url = routeAndQueryParams;
|
||||
|
||||
await service.setEnvironment(QA_US_REGION_KEY);
|
||||
|
||||
expect(window.location.href).toEqual(
|
||||
QA_US_WEB_REGION_CONFIG.urls.webVault + "/#" + routeAndQueryParams,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from "@angular/router";
|
||||
import { ReplaySubject } from "rxjs";
|
||||
import { firstValueFrom, ReplaySubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
RegionConfig,
|
||||
Urls,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
CloudEnvironment,
|
||||
DefaultEnvironmentService,
|
||||
@@ -16,6 +15,12 @@ import {
|
||||
} from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
export type WebRegionConfig = RegionConfig & {
|
||||
key: Region | string; // strings are used for custom environments
|
||||
domain: string;
|
||||
urls: Urls;
|
||||
};
|
||||
|
||||
/**
|
||||
* Web specific environment service. Ensures that the urls are set from the window location.
|
||||
*/
|
||||
@@ -24,23 +29,30 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
|
||||
private win: Window,
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountService,
|
||||
additionalRegionConfigs: WebRegionConfig[] = [],
|
||||
private router: Router,
|
||||
private envUrls: Urls,
|
||||
) {
|
||||
super(stateProvider, accountService);
|
||||
super(stateProvider, accountService, additionalRegionConfigs);
|
||||
|
||||
// The web vault always uses the current location as the base url
|
||||
const urls = process.env.URLS as Urls;
|
||||
urls.base ??= this.win.location.origin;
|
||||
envUrls.base ??= this.win.location.origin;
|
||||
|
||||
// Find the region
|
||||
const domain = Utils.getDomain(this.win.location.href);
|
||||
const region = this.availableRegions().find((r) => Utils.getDomain(r.urls.webVault) === domain);
|
||||
const currentHostname = new URL(this.win.location.href).hostname;
|
||||
const availableRegions = this.availableRegions();
|
||||
const region = availableRegions.find((r) => {
|
||||
// We must use hostname as our QA envs use the same
|
||||
// domain (bitwarden.pw) but different subdomains (qa and euqa)
|
||||
const webVaultHostname = new URL(r.urls.webVault).hostname;
|
||||
return webVaultHostname === currentHostname;
|
||||
});
|
||||
|
||||
let environment: Environment;
|
||||
if (region) {
|
||||
environment = new WebCloudEnvironment(region, urls);
|
||||
environment = new WebCloudEnvironment(region, envUrls);
|
||||
} else {
|
||||
environment = new SelfHostedEnvironment(urls);
|
||||
environment = new SelfHostedEnvironment(envUrls);
|
||||
}
|
||||
|
||||
// Override the environment observable with a replay subject
|
||||
@@ -50,37 +62,45 @@ export class WebEnvironmentService extends DefaultEnvironmentService {
|
||||
}
|
||||
|
||||
// Web setting env means navigating to a new location
|
||||
setEnvironment(region: Region, urls?: Urls): Promise<Urls> {
|
||||
async setEnvironment(region: Region | string, urls?: Urls): Promise<Urls> {
|
||||
if (region === Region.SelfHosted) {
|
||||
throw new Error("setEnvironment does not work in web for self-hosted.");
|
||||
}
|
||||
|
||||
const currentDomain = Utils.getDomain(this.win.location.href);
|
||||
const currentRegion = this.availableRegions().find(
|
||||
(r) => Utils.getDomain(r.urls.webVault) === currentDomain,
|
||||
);
|
||||
// Find the region
|
||||
const currentHostname = new URL(this.win.location.href).hostname;
|
||||
const availableRegions = this.availableRegions();
|
||||
const currentRegionConfig = availableRegions.find((r) => {
|
||||
// We must use hostname as our QA envs use the same
|
||||
// domain (bitwarden.pw) but different subdomains (qa and euqa)
|
||||
const webVaultHostname = new URL(r.urls.webVault).hostname;
|
||||
return webVaultHostname === currentHostname;
|
||||
});
|
||||
|
||||
if (currentRegion.key === region) {
|
||||
// They have selected the current region, nothing to do
|
||||
return Promise.resolve(currentRegion.urls);
|
||||
if (currentRegionConfig.key === region) {
|
||||
// They have selected the current region, return the current env urls
|
||||
// We can't return the region urls because the env base url is modified
|
||||
// in the constructor to match the current window.location.origin.
|
||||
const currentEnv = await firstValueFrom(this.environment$);
|
||||
return currentEnv.getUrls();
|
||||
}
|
||||
|
||||
const chosenRegion = this.availableRegions().find((r) => r.key === region);
|
||||
const chosenRegionConfig = this.availableRegions().find((r) => r.key === region);
|
||||
|
||||
if (chosenRegion == null) {
|
||||
if (chosenRegionConfig == null) {
|
||||
throw new Error("The selected region is not known as an available region.");
|
||||
}
|
||||
|
||||
// Preserve the current in app route + params in the new location
|
||||
const routeAndParams = `/#${this.router.url}`;
|
||||
this.win.location.href = chosenRegion.urls.webVault + routeAndParams;
|
||||
this.win.location.href = chosenRegionConfig.urls.webVault + routeAndParams;
|
||||
|
||||
// This return shouldn't matter as we are about to leave the current window
|
||||
return Promise.resolve(chosenRegion.urls);
|
||||
return chosenRegionConfig.urls;
|
||||
}
|
||||
}
|
||||
|
||||
class WebCloudEnvironment extends CloudEnvironment {
|
||||
export class WebCloudEnvironment extends CloudEnvironment {
|
||||
constructor(config: RegionConfig, urls: Urls) {
|
||||
super(config);
|
||||
// We override the urls to avoid CORS issues
|
||||
|
||||
42
apps/web/src/app/platform/web-sdk-client-factory.ts
Normal file
42
apps/web/src/app/platform/web-sdk-client-factory.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import * as sdk from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* SDK client factory with a js fallback for when WASM is not supported.
|
||||
*/
|
||||
export class WebSdkClientFactory implements SdkClientFactory {
|
||||
async createSdkClient(
|
||||
...args: ConstructorParameters<typeof sdk.BitwardenClient>
|
||||
): Promise<sdk.BitwardenClient> {
|
||||
const module = await load();
|
||||
|
||||
(sdk as any).init(module);
|
||||
|
||||
return Promise.resolve(new sdk.BitwardenClient(...args));
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/47880734
|
||||
const supported = (() => {
|
||||
try {
|
||||
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
|
||||
const module = new WebAssembly.Module(
|
||||
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00),
|
||||
);
|
||||
if (module instanceof WebAssembly.Module) {
|
||||
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
async function load() {
|
||||
if (supported) {
|
||||
return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
|
||||
} else {
|
||||
return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js");
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="tw-ml-auto">
|
||||
<div class="tw-ml-auto" *ngIf="showDelete">
|
||||
<button
|
||||
bitIconButton="bwi-trash"
|
||||
type="button"
|
||||
@@ -73,6 +73,7 @@
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
[disabled]="!canDelete"
|
||||
data-testid="delete-cipher-btn"
|
||||
></button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -179,6 +179,15 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
return this.cipher?.edit ?? false;
|
||||
}
|
||||
|
||||
protected get showDelete() {
|
||||
// Don't show the delete button when cloning a cipher
|
||||
if (this.params.mode == "form" && this.formConfig.mode === "clone") {
|
||||
return false;
|
||||
}
|
||||
// Never show the delete button for new ciphers
|
||||
return this.cipher != null;
|
||||
}
|
||||
|
||||
protected get showCipherView() {
|
||||
return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm);
|
||||
}
|
||||
@@ -332,8 +341,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
cancel = async () => {
|
||||
// We're in View mode, or we don't have a cipher, close the dialog.
|
||||
if (this.params.mode === "view" || this.cipher == null) {
|
||||
// We're in View mode, we don't have a cipher, or we were cloning, close the dialog.
|
||||
if (this.params.mode === "view" || this.cipher == null || this.formConfig.mode === "clone") {
|
||||
this.dialogRef.close(this._cipherModified ? VaultItemDialogResult.Saved : undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,38 @@
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</th>
|
||||
<th bitCell [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'">{{ "name" | i18n }}</th>
|
||||
<!-- Organization vault -->
|
||||
<th
|
||||
*ngIf="showAdminActions"
|
||||
bitCell
|
||||
bitSortable="name"
|
||||
[fn]="sortByName"
|
||||
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
|
||||
>
|
||||
{{ "name" | i18n }}
|
||||
</th>
|
||||
<!-- Individual vault -->
|
||||
<th
|
||||
*ngIf="!showAdminActions"
|
||||
bitCell
|
||||
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
|
||||
>
|
||||
{{ "name" | i18n }}
|
||||
</th>
|
||||
<th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th>
|
||||
<th bitCell class="tw-w-2/5" *ngIf="showGroups">{{ "groups" | i18n }}</th>
|
||||
<th bitCell class="tw-w-2/5" *ngIf="showPermissionsColumn">
|
||||
<th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
|
||||
{{ "groups" | i18n }}
|
||||
</th>
|
||||
<th
|
||||
bitCell
|
||||
bitSortable="permissions"
|
||||
[fn]="sortByPermissions"
|
||||
class="tw-w-2/5"
|
||||
*ngIf="showPermissionsColumn"
|
||||
>
|
||||
{{ "permission" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-12 tw-text-right">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, inject, Input, Output } from "@angular/core";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
|
||||
import {
|
||||
CollectionPermission,
|
||||
convertToPermission,
|
||||
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||
import { VaultItem } from "./vault-item";
|
||||
import { VaultItemEvent } from "./vault-item-event";
|
||||
|
||||
@@ -25,6 +30,7 @@ const MaxSelectionCount = 500;
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class VaultItemsComponent {
|
||||
protected i18nService = inject(I18nService);
|
||||
protected RowHeight = RowHeight;
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@@ -197,7 +203,7 @@ export class VaultItemsComponent {
|
||||
private refreshItems() {
|
||||
const collections: VaultItem[] = this.collections.map((collection) => ({ collection }));
|
||||
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
|
||||
const items: VaultItem[] = [].concat(collections).concat(ciphers);
|
||||
let items: VaultItem[] = [].concat(collections).concat(ciphers);
|
||||
|
||||
this.selection.clear();
|
||||
|
||||
@@ -208,6 +214,11 @@ export class VaultItemsComponent {
|
||||
(item.collection !== undefined && item.collection.id !== Unassigned),
|
||||
);
|
||||
|
||||
// Apply sorting only for organization vault
|
||||
if (this.showAdminActions) {
|
||||
items = items.sort(this.sortByGroups);
|
||||
}
|
||||
|
||||
this.dataSource.data = items;
|
||||
}
|
||||
|
||||
@@ -293,6 +304,112 @@ export class VaultItemsComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
|
||||
*/
|
||||
protected sortByName = (a: VaultItem, b: VaultItem) => {
|
||||
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
|
||||
|
||||
// First, sort collections before ciphers
|
||||
if (a.collection && !b.collection) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.collection && b.collection) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return getName(a).localeCompare(getName(b));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts VaultItems based on group names
|
||||
*/
|
||||
protected sortByGroups = (a: VaultItem, b: VaultItem): number => {
|
||||
const getGroupNames = (item: VaultItem): string => {
|
||||
if (item.collection instanceof CollectionAdminView) {
|
||||
return item.collection.groups
|
||||
.map((group) => this.getGroupName(group.id))
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const aGroupNames = getGroupNames(a);
|
||||
const bGroupNames = getGroupNames(b);
|
||||
|
||||
if (aGroupNames.length !== bGroupNames.length) {
|
||||
return bGroupNames.length - aGroupNames.length;
|
||||
}
|
||||
|
||||
return aGroupNames.localeCompare(bGroupNames);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts VaultItems based on their permissions, with higher permissions taking precedence.
|
||||
* If permissions are equal, it falls back to sorting by name.
|
||||
*/
|
||||
protected sortByPermissions = (a: VaultItem, b: VaultItem): number => {
|
||||
const getPermissionPriority = (item: VaultItem): number => {
|
||||
if (item.collection instanceof CollectionAdminView) {
|
||||
const permission = this.getCollectionPermission(item.collection);
|
||||
|
||||
switch (permission) {
|
||||
case CollectionPermission.Manage:
|
||||
return 5;
|
||||
case CollectionPermission.Edit:
|
||||
return 4;
|
||||
case CollectionPermission.EditExceptPass:
|
||||
return 3;
|
||||
case CollectionPermission.View:
|
||||
return 2;
|
||||
case CollectionPermission.ViewExceptPass:
|
||||
return 1;
|
||||
case "NoAccess":
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const priorityA = getPermissionPriority(a);
|
||||
const priorityB = getPermissionPriority(b);
|
||||
|
||||
// Higher priority first
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityB - priorityA;
|
||||
}
|
||||
|
||||
return this.sortByName(a, b);
|
||||
};
|
||||
|
||||
/**
|
||||
* Default sorting function for vault items.
|
||||
* Sorts by: 1. Collections before ciphers
|
||||
* 2. Highest permission first
|
||||
* 3. Alphabetical order of collections and ciphers
|
||||
*/
|
||||
private defaultSort = (a: VaultItem, b: VaultItem) => {
|
||||
// First, sort collections before ciphers
|
||||
if (a.collection && !b.collection) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.collection && b.collection) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Next, sort by permissions
|
||||
const permissionSort = this.sortByPermissions(a, b);
|
||||
if (permissionSort !== 0) {
|
||||
return permissionSort;
|
||||
}
|
||||
|
||||
// Finally, sort by name
|
||||
return this.sortByName(a, b);
|
||||
};
|
||||
|
||||
private hasPersonalItems(): boolean {
|
||||
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
|
||||
}
|
||||
@@ -306,4 +423,24 @@ export class VaultItemsComponent {
|
||||
private getUniqueOrganizationIds(): Set<string> {
|
||||
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
|
||||
}
|
||||
|
||||
private getGroupName(groupId: string): string | undefined {
|
||||
return this.allGroups.find((g) => g.id === groupId)?.name;
|
||||
}
|
||||
|
||||
private getCollectionPermission(
|
||||
collection: CollectionAdminView,
|
||||
): CollectionPermission | "NoAccess" {
|
||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||
|
||||
if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) {
|
||||
return CollectionPermission.Edit;
|
||||
}
|
||||
|
||||
if (collection.assigned) {
|
||||
return convertToPermission(collection);
|
||||
}
|
||||
|
||||
return "NoAccess";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,6 +1249,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
organizationId: this.organization?.id as OrganizationId,
|
||||
availableCollections,
|
||||
activeCollection: this.activeFilter?.selectedCollectionNode?.node,
|
||||
isSingleCipherAdmin:
|
||||
items.length === 1 && (this.organization?.canEditAllCiphers || items[0].isUnassigned),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const moduleRules = [
|
||||
loader: "@ngtools/webpack",
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
test: /argon2(-simd)?\.wasm$/,
|
||||
loader: "base64-loader",
|
||||
type: "javascript/auto",
|
||||
},
|
||||
@@ -324,6 +324,7 @@ const webpackConfig = {
|
||||
mode: NODE_ENV,
|
||||
devtool: "source-map",
|
||||
devServer: devServer,
|
||||
target: "web",
|
||||
entry: {
|
||||
"app/polyfills": "./src/polyfills.ts",
|
||||
"app/main": "./src/main.ts",
|
||||
@@ -383,9 +384,12 @@ const webpackConfig = {
|
||||
clean: true,
|
||||
},
|
||||
module: {
|
||||
noParse: /\.wasm$/,
|
||||
noParse: /argon2(-simd)?\.wasm$/,
|
||||
rules: moduleRules,
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
setupFilesAfterEnv: ["<rootDir>/../../apps/cli/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
moduleNameMapper: {
|
||||
"@bitwarden/common/platform/services/sdk/default-sdk-client-factory":
|
||||
"<rootDir>/../../libs/common/spec/jest-sdk-client-factory",
|
||||
...pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2016",
|
||||
"module": "es6",
|
||||
"module": "ES2020",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
@@ -58,3 +59,12 @@ export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE");
|
||||
export const REFRESH_ACCESS_TOKEN_ERROR_CALLBACK = new SafeInjectionToken<() => void>(
|
||||
"REFRESH_ACCESS_TOKEN_ERROR_CALLBACK",
|
||||
);
|
||||
|
||||
/**
|
||||
* Injection token for injecting the NodeJS process.env additional regions into services.
|
||||
* Using an injection token allows services to be tested without needing to
|
||||
* mock the process.env.
|
||||
*/
|
||||
export const ENV_ADDITIONAL_REGIONS = new SafeInjectionToken<RegionConfig[]>(
|
||||
"ENV_ADDITIONAL_REGIONS",
|
||||
);
|
||||
|
||||
@@ -141,13 +141,18 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
@@ -176,6 +181,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
@@ -298,6 +304,7 @@ import {
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
CLIENT_TYPE,
|
||||
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
} from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
|
||||
@@ -530,10 +537,14 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: CollectionService,
|
||||
deps: [CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ENV_ADDITIONAL_REGIONS,
|
||||
useValue: process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EnvironmentService,
|
||||
useClass: DefaultEnvironmentService,
|
||||
deps: [StateProvider, AccountServiceAbstraction],
|
||||
deps: [StateProvider, AccountServiceAbstraction, ENV_ADDITIONAL_REGIONS],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
@@ -1316,6 +1327,16 @@ const safeProviders: SafeProvider[] = [
|
||||
useExisting: NoopViewCacheService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkService,
|
||||
useClass: DefaultSdkService,
|
||||
deps: [
|
||||
SdkClientFactory,
|
||||
EnvironmentService,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -605,6 +605,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.collections = this.writeableCollections?.filter(
|
||||
(c) => c.organizationId === this.cipher.organizationId,
|
||||
);
|
||||
// If there's only one collection, check it by default
|
||||
if (this.collections.length === 1) {
|
||||
(this.collections[0] as any).checked = true;
|
||||
}
|
||||
const org = await this.organizationService.get(this.cipher.organizationId);
|
||||
if (org != null) {
|
||||
this.cipher.organizationUseTotp = org.useTotp;
|
||||
|
||||
9
libs/common/spec/jest-sdk-client-factory.ts
Normal file
9
libs/common/spec/jest-sdk-client-factory.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ClientSettings, LogLevel, BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory";
|
||||
|
||||
export class DefaultSdkClientFactory implements SdkClientFactory {
|
||||
createSdkClient(settings?: ClientSettings, log_level?: LogLevel): Promise<BitwardenClient> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* Factory for creating SDK clients.
|
||||
*/
|
||||
export abstract class SdkClientFactory {
|
||||
abstract createSdkClient(
|
||||
...args: ConstructorParameters<typeof BitwardenClient>
|
||||
): Promise<BitwardenClient>;
|
||||
}
|
||||
10
libs/common/src/platform/abstractions/sdk/sdk.service.ts
Normal file
10
libs/common/src/platform/abstractions/sdk/sdk.service.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
export abstract class SdkService {
|
||||
client$: Observable<BitwardenClient>;
|
||||
supported$: Observable<boolean>;
|
||||
|
||||
abstract failedToInitialize(): Promise<void>;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type SharedFlags = {
|
||||
showPasswordless?: boolean;
|
||||
sdk?: boolean;
|
||||
};
|
||||
|
||||
// required to avoid linting errors when there are no flags
|
||||
@@ -28,7 +29,7 @@ function getFlags<T>(envFlags: string | T): T {
|
||||
* @returns The value of the flag
|
||||
*/
|
||||
export function flagEnabled<Flags extends SharedFlags>(flag: keyof Flags): boolean {
|
||||
const flags = getFlags<Flags>(process.env.FLAGS);
|
||||
const flags = getFlags<Flags>(process.env.FLAGS) ?? ({} as Flags);
|
||||
return flags[flag] == null || !!flags[flag];
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
private additionalRegionConfigs: RegionConfig[] = [],
|
||||
) {
|
||||
this.globalState = this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY);
|
||||
this.globalCloudRegionState = this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY);
|
||||
@@ -177,8 +178,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
|
||||
}
|
||||
|
||||
availableRegions(): RegionConfig[] {
|
||||
const additionalRegions = (process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[]) ?? [];
|
||||
return PRODUCTION_REGIONS.concat(additionalRegions);
|
||||
return PRODUCTION_REGIONS.concat(this.additionalRegionConfigs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as sdk from "@bitwarden/sdk-internal";
|
||||
import * as module from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm";
|
||||
|
||||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
|
||||
/**
|
||||
* Directly imports the Bitwarden SDK and initializes it.
|
||||
*
|
||||
* **Warning**: This requires WASM support and will fail if the environment does not support it.
|
||||
*/
|
||||
export class DefaultSdkClientFactory implements SdkClientFactory {
|
||||
async createSdkClient(
|
||||
...args: ConstructorParameters<typeof sdk.BitwardenClient>
|
||||
): Promise<sdk.BitwardenClient> {
|
||||
(sdk as any).init(module);
|
||||
|
||||
return Promise.resolve(new sdk.BitwardenClient(...args));
|
||||
}
|
||||
}
|
||||
111
libs/common/src/platform/services/sdk/default-sdk.service.ts
Normal file
111
libs/common/src/platform/services/sdk/default-sdk.service.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { concatMap, firstValueFrom, shareReplay } from "rxjs";
|
||||
|
||||
import { LogLevel, DeviceType as SdkDeviceType } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { DeviceType } from "../../../enums/device-type.enum";
|
||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
import { SdkService } from "../../abstractions/sdk/sdk.service";
|
||||
|
||||
export class DefaultSdkService implements SdkService {
|
||||
client$ = this.environmentService.environment$.pipe(
|
||||
concatMap(async (env) => {
|
||||
const settings = {
|
||||
apiUrl: env.getApiUrl(),
|
||||
identityUrl: env.getIdentityUrl(),
|
||||
deviceType: this.toDevice(this.platformUtilsService.getDevice()),
|
||||
userAgent: this.userAgent ?? navigator.userAgent,
|
||||
};
|
||||
|
||||
return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
supported$ = this.client$.pipe(
|
||||
concatMap(async (client) => {
|
||||
return client.echo("bitwarden wasm!") === "bitwarden wasm!";
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private sdkClientFactory: SdkClientFactory,
|
||||
private environmentService: EnvironmentService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private apiService: ApiService, // Yes we shouldn't import ApiService, but it's temporary
|
||||
private userAgent: string = null,
|
||||
) {}
|
||||
|
||||
async failedToInitialize(): Promise<void> {
|
||||
// Only log on cloud instances
|
||||
if (
|
||||
this.platformUtilsService.isDev() ||
|
||||
!(await firstValueFrom(this.environmentService.environment$)).isCloud
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.apiService.send("POST", "/wasm-debug", null, false, false, null, (headers) => {
|
||||
headers.append("SDK-Version", "1.0.0");
|
||||
});
|
||||
}
|
||||
|
||||
private toDevice(device: DeviceType): SdkDeviceType {
|
||||
switch (device) {
|
||||
case DeviceType.Android:
|
||||
return "Android";
|
||||
case DeviceType.iOS:
|
||||
return "iOS";
|
||||
case DeviceType.ChromeExtension:
|
||||
return "ChromeExtension";
|
||||
case DeviceType.FirefoxExtension:
|
||||
return "FirefoxExtension";
|
||||
case DeviceType.OperaExtension:
|
||||
return "OperaExtension";
|
||||
case DeviceType.EdgeExtension:
|
||||
return "EdgeExtension";
|
||||
case DeviceType.WindowsDesktop:
|
||||
return "WindowsDesktop";
|
||||
case DeviceType.MacOsDesktop:
|
||||
return "MacOsDesktop";
|
||||
case DeviceType.LinuxDesktop:
|
||||
return "LinuxDesktop";
|
||||
case DeviceType.ChromeBrowser:
|
||||
return "ChromeBrowser";
|
||||
case DeviceType.FirefoxBrowser:
|
||||
return "FirefoxBrowser";
|
||||
case DeviceType.OperaBrowser:
|
||||
return "OperaBrowser";
|
||||
case DeviceType.EdgeBrowser:
|
||||
return "EdgeBrowser";
|
||||
case DeviceType.IEBrowser:
|
||||
return "IEBrowser";
|
||||
case DeviceType.UnknownBrowser:
|
||||
return "UnknownBrowser";
|
||||
case DeviceType.AndroidAmazon:
|
||||
return "AndroidAmazon";
|
||||
case DeviceType.UWP:
|
||||
return "UWP";
|
||||
case DeviceType.SafariBrowser:
|
||||
return "SafariBrowser";
|
||||
case DeviceType.VivaldiBrowser:
|
||||
return "VivaldiBrowser";
|
||||
case DeviceType.VivaldiExtension:
|
||||
return "VivaldiExtension";
|
||||
case DeviceType.SafariExtension:
|
||||
return "SafariExtension";
|
||||
case DeviceType.Server:
|
||||
return "Server";
|
||||
case DeviceType.WindowsCLI:
|
||||
return "WindowsCLI";
|
||||
case DeviceType.MacOsCLI:
|
||||
return "MacOsCLI";
|
||||
case DeviceType.LinuxCLI:
|
||||
return "LinuxCLI";
|
||||
default:
|
||||
return "SDK";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
|
||||
|
||||
/**
|
||||
* Noop SDK client factory.
|
||||
*
|
||||
* Used during SDK rollout to prevent bundling the SDK with some applications.
|
||||
*/
|
||||
export class NoopSdkClientFactory implements SdkClientFactory {
|
||||
createSdkClient(
|
||||
...args: ConstructorParameters<typeof BitwardenClient>
|
||||
): Promise<BitwardenClient> {
|
||||
return Promise.reject(new Error("SDK not available"));
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,13 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
* @returns A promise that resolves when the collections have been saved
|
||||
*/
|
||||
saveCollectionsWithServer: (cipher: Cipher) => Promise<Cipher>;
|
||||
|
||||
/**
|
||||
* Save the collections for a cipher with the server as an admin.
|
||||
* 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>;
|
||||
/**
|
||||
* Bulk update collections for many ciphers with the server
|
||||
* @param orgId
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TreeNode } from "../models/domain/tree-node";
|
||||
import { CollectionView } from "../models/view/collection.view";
|
||||
|
||||
export abstract class CollectionService {
|
||||
encryptedCollections$: Observable<Collection[]>;
|
||||
decryptedCollections$: Observable<CollectionView[]>;
|
||||
|
||||
clearActiveUserCache: () => Promise<void>;
|
||||
|
||||
@@ -858,6 +858,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return new Cipher(updated[cipher.id as CipherId], cipher.localData);
|
||||
}
|
||||
|
||||
async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<void> {
|
||||
const request = new CipherCollectionsRequest(cipher.collectionIds);
|
||||
await this.apiService.putCipherCollectionsAdmin(cipher.id, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update collections for many ciphers with the server
|
||||
* @param orgId
|
||||
|
||||
@@ -96,6 +96,17 @@ describe("UriOptionComponent", () => {
|
||||
expect(component["uriForm"].enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should update form when `writeValue` is invoked", () => {
|
||||
expect(component["uriForm"].value).toEqual({ uri: null, matchDetection: null });
|
||||
|
||||
component.writeValue({ uri: "example.com", matchDetection: UriMatchStrategy.Exact });
|
||||
|
||||
expect(component["uriForm"].value).toEqual({
|
||||
uri: "example.com",
|
||||
matchDetection: UriMatchStrategy.Exact,
|
||||
});
|
||||
});
|
||||
|
||||
describe("match detection", () => {
|
||||
it("should hide the match detection select by default", () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -149,12 +149,12 @@ export class UriOptionComponent implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
// NG_VALUE_ACCESSOR implementation
|
||||
writeValue(value: any): void {
|
||||
writeValue(value: { uri: string; matchDetection: UriMatchStrategySetting | null }): void {
|
||||
if (value) {
|
||||
this.uriForm.setValue(
|
||||
{
|
||||
uri: value.uri ?? "",
|
||||
matchDetection: value.match ?? null,
|
||||
matchDetection: value.matchDetection ?? null,
|
||||
},
|
||||
{ emitEvent: false },
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||
import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -39,9 +39,21 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService {
|
||||
await firstValueFrom(
|
||||
combineLatest([
|
||||
this.organizations$,
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.encryptedCollections$.pipe(
|
||||
switchMap((c) =>
|
||||
this.collectionService.decryptedCollections$.pipe(
|
||||
filter((d) => d.length === c.length), // Ensure all collections have been decrypted
|
||||
),
|
||||
),
|
||||
),
|
||||
this.allowPersonalOwnership$,
|
||||
this.folderService.folderViews$,
|
||||
this.folderService.folders$.pipe(
|
||||
switchMap((f) =>
|
||||
this.folderService.folderViews$.pipe(
|
||||
filter((d) => d.length - 1 === f.length), // -1 for "No Folder" in folderViews$
|
||||
),
|
||||
),
|
||||
),
|
||||
this.getCipher(cipherId),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -64,6 +64,15 @@ export interface CollectionAssignmentParams {
|
||||
* removed from the ciphers upon submission.
|
||||
*/
|
||||
activeCollection?: CollectionView;
|
||||
|
||||
/**
|
||||
* Flag indicating if the user is performing the action as an admin on a SINGLE cipher. When true,
|
||||
* the `/admin` endpoint will be used to update the cipher's collections. Required when updating
|
||||
* ciphers an Admin does not normally have access to or for Unassigned ciphers.
|
||||
*
|
||||
* The bulk method already handles admin actions internally.
|
||||
*/
|
||||
isSingleCipherAdmin?: boolean;
|
||||
}
|
||||
|
||||
export enum CollectionAssignmentResult {
|
||||
@@ -463,6 +472,10 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
const { collections } = this.formGroup.getRawValue();
|
||||
cipherView.collectionIds = collections.map((i) => i.id as CollectionId);
|
||||
const cipher = await this.cipherService.encrypt(cipherView, this.activeUserId);
|
||||
await this.cipherService.saveCollectionsWithServer(cipher);
|
||||
if (this.params.isSingleCipherAdmin) {
|
||||
await this.cipherService.saveCollectionsWithServerAdmin(cipher);
|
||||
} else {
|
||||
await this.cipherService.saveCollectionsWithServer(cipher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"@angular/platform-browser": "16.2.12",
|
||||
"@angular/platform-browser-dynamic": "16.2.12",
|
||||
"@angular/router": "16.2.12",
|
||||
"@bitwarden/sdk-internal": "0.1.3",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "12.0.1",
|
||||
@@ -4625,6 +4626,11 @@
|
||||
"resolved": "libs/platform",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/sdk-internal": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.1.3.tgz",
|
||||
"integrity": "sha512-zk9DyYMjylVLdljeLn3OLBcD939Hg/qMNJ2FxbyjiSKtcOcgglXgYmbcS01NRFFfM9REbn+j+2fWbQo6N+8SHw=="
|
||||
},
|
||||
"node_modules/@bitwarden/vault": {
|
||||
"resolved": "libs/vault",
|
||||
"link": true
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
"@angular/platform-browser": "16.2.12",
|
||||
"@angular/platform-browser-dynamic": "16.2.12",
|
||||
"@angular/router": "16.2.12",
|
||||
"@bitwarden/sdk-internal": "0.1.3",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "12.0.1",
|
||||
|
||||
Reference in New Issue
Block a user