1
0
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:
Victoria League
2024-10-07 10:34:26 -04:00
committed by GitHub
59 changed files with 1371 additions and 91 deletions

View File

@@ -2,6 +2,7 @@
"devFlags": {},
"flags": {
"showPasswordless": true,
"accountSwitching": false
"accountSwitching": false,
"sdk": true
}
}

View File

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

View File

@@ -39,8 +39,7 @@
}
],
"background": {
"service_worker": "background.js",
"type": "module"
"service_worker": "background.js"
},
"action": {
"default_icon": {

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
{
"flags": {
"sdk": true
}
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
"pretty": true,
"moduleResolution": "node",
"target": "ES2016",
"module": "es6",
"module": "ES2020",
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,

View File

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

View File

@@ -1,4 +1,6 @@
{
"devFlags": {},
"flags": {}
"flags": {
"sdk": true
},
"devFlags": {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,8 @@
"allowedHosts": "auto"
},
"flags": {
"showPasswordless": false
}
"showPasswordless": false,
"sdk": true
},
"devFlags": {}
}

View File

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

View File

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

View File

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

View 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");

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
"pretty": true,
"moduleResolution": "node",
"target": "ES2016",
"module": "es6",
"module": "ES2020",
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,

View File

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

View File

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

View File

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

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

View File

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

View 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>;
}

View File

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

View File

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

View File

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

View 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";
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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