From b14bb92d78526d417e083e233d48cc784e3792c8 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 16 May 2024 00:09:24 +1000 Subject: [PATCH 1/4] [AC-2579] Set up bit-cli folder (#9092) * Create bit-cli folder with configs * Add bit-cli to workspace * Refactor CLI app structure * services are managed by the ServiceContainer * programs are registered by register(Oss|Bit)Program * the app is bootstrapped by Main * Reapply changes from #9099 * Reapply changes from #8604 * Reapply changes from #9115 --- apps/cli/package.json | 12 + apps/cli/src/bw.ts | 789 +----------------- apps/cli/src/commands/serve.command.ts | 177 ++-- apps/cli/src/program.ts | 153 ++-- apps/cli/src/register-oss-programs.ts | 22 + apps/cli/src/service-container.spec.ts | 7 + apps/cli/src/service-container.ts | 772 +++++++++++++++++ apps/cli/src/tools/send/send.program.ts | 88 +- apps/cli/src/vault.program.ts | 99 ++- bitwarden_license/bit-cli/.eslintrc.json | 5 + bitwarden_license/bit-cli/jest.config.js | 16 + bitwarden_license/bit-cli/src/bw.spec.ts | 5 + bitwarden_license/bit-cli/src/bw.ts | 20 + .../bit-cli/src/register-bit-programs.ts | 10 + .../bit-cli/src/service-container.spec.ts | 7 + .../bit-cli/src/service-container.ts | 7 + bitwarden_license/bit-cli/tsconfig.json | 28 + bitwarden_license/bit-cli/tsconfig.spec.json | 4 + bitwarden_license/bit-cli/webpack.config.js | 12 + clients.code-workspace | 4 + jest.config.js | 1 + 21 files changed, 1200 insertions(+), 1038 deletions(-) create mode 100644 apps/cli/src/register-oss-programs.ts create mode 100644 apps/cli/src/service-container.spec.ts create mode 100644 apps/cli/src/service-container.ts create mode 100644 bitwarden_license/bit-cli/.eslintrc.json create mode 100644 bitwarden_license/bit-cli/jest.config.js create mode 100644 bitwarden_license/bit-cli/src/bw.spec.ts create mode 100644 bitwarden_license/bit-cli/src/bw.ts create mode 100644 bitwarden_license/bit-cli/src/register-bit-programs.ts create mode 100644 bitwarden_license/bit-cli/src/service-container.spec.ts create mode 100644 bitwarden_license/bit-cli/src/service-container.ts create mode 100644 bitwarden_license/bit-cli/tsconfig.json create mode 100644 bitwarden_license/bit-cli/tsconfig.spec.json create mode 100644 bitwarden_license/bit-cli/webpack.config.js diff --git a/apps/cli/package.json b/apps/cli/package.json index c427947bd3..4d1636c51a 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -33,6 +33,18 @@ "dist:mac": "npm run build:prod && npm run clean && npm run package:mac", "dist:lin": "npm run build:prod && npm run clean && npm run package:lin", "publish:npm": "npm run build:prod && npm publish --access public", + "build:bit": "webpack -c ../../bitwarden_license/bit-cli/webpack.config.js", + "build:bit:debug": "npm run build:bit && node --inspect ./build/bw.js", + "build:bit:watch": "webpack --watch -c ../../bitwarden_license/bit-cli/webpack.config.js", + "build:bit:prod": "cross-env NODE_ENV=production npm run build:bit", + "build:bit:prod:watch": "cross-env NODE_ENV=production npm run build:bit:watch", + "dist:bit": "npm run build:bit:prod && npm run clean && npm run package", + "dist:bit:win": "npm run build:bit:prod && npm run clean && npm run package:bit:win", + "dist:bit:mac": "npm run build:bit:prod && npm run clean && npm run package:bit:mac", + "dist:bit:lin": "npm run build:bit:prod && npm run clean && npm run package:bit:lin", + "package:bit:win": "pkg . --targets win-x64 --output ./dist/bit/windows/bw.exe", + "package:bit:mac": "pkg . --targets macos-x64 --output ./dist/bit/macos/bw", + "package:bit:lin": "pkg . --targets linux-x64 --output ./dist/bit/linux/bw", "test": "jest", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll" diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 31de198194..03ebaa7368 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -1,788 +1,17 @@ -import * as fs from "fs"; -import * as path from "path"; - import { program } from "commander"; -import * as jsdom from "jsdom"; -import { firstValueFrom } from "rxjs"; -import { - InternalUserDecryptionOptionsServiceAbstraction, - AuthRequestService, - LoginStrategyService, - LoginStrategyServiceAbstraction, - PinService, - PinServiceAbstraction, - UserDecryptionOptionsService, -} from "@bitwarden/auth/common"; -import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; -import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; -import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; -import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; -import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; -import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; -import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; -import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; -import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; -import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; -import { TokenService } from "@bitwarden/common/auth/services/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; -import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; -import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { - DefaultDomainSettingsService, - DomainSettingsService, -} from "@bitwarden/common/autofill/services/domain-settings.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; -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 { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; -import { - BiometricStateService, - DefaultBiometricStateService, -} from "@bitwarden/common/platform/biometrics/biometric-state.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"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; -import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; -import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; -import { ContainerService } from "@bitwarden/common/platform/services/container.service"; -import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; -import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; -import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; -import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; -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 { 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"; -import { - ActiveUserStateProvider, - DerivedStateProvider, - GlobalStateProvider, - SingleUserStateProvider, - StateEventRunnerService, - StateProvider, -} from "@bitwarden/common/platform/state"; -/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */ -import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; -import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; -import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; -import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; -import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; -import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; -import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; -/* eslint-enable import/no-restricted-paths */ -import { AuditService } from "@bitwarden/common/services/audit.service"; -import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; -import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { SearchService } from "@bitwarden/common/services/search.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; -import { - PasswordGenerationService, - PasswordGenerationServiceAbstraction, -} from "@bitwarden/common/tools/generator/password"; -import { - PasswordStrengthService, - PasswordStrengthServiceAbstraction, -} from "@bitwarden/common/tools/password-strength"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; -import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; -import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; -import { CollectionService } from "@bitwarden/common/vault/services/collection.service"; -import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; -import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; -import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; -import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service"; -import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; -import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { - ImportApiService, - ImportApiServiceAbstraction, - ImportService, - ImportServiceAbstraction, -} from "@bitwarden/importer/core"; -import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; -import { - IndividualVaultExportService, - IndividualVaultExportServiceAbstraction, - OrganizationVaultExportService, - OrganizationVaultExportServiceAbstraction, - VaultExportService, - VaultExportServiceAbstraction, -} from "@bitwarden/vault-export-core"; +import { registerOssPrograms } from "./register-oss-programs"; +import { ServiceContainer } from "./service-container"; -import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; -import { ConsoleLogService } from "./platform/services/console-log.service"; -import { I18nService } from "./platform/services/i18n.service"; -import { LowdbStorageService } from "./platform/services/lowdb-storage.service"; -import { NodeApiService } from "./platform/services/node-api.service"; -import { NodeEnvSecureStorageService } from "./platform/services/node-env-secure-storage.service"; -import { Program } from "./program"; -import { SendProgram } from "./tools/send/send.program"; -import { VaultProgram } from "./vault.program"; +async function main() { + const serviceContainer = new ServiceContainer(); + await serviceContainer.init(); -// Polyfills -global.DOMParser = new jsdom.JSDOM().window.DOMParser; + await registerOssPrograms(serviceContainer); -// eslint-disable-next-line -const packageJson = require("../package.json"); - -export class Main { - messagingService: MessageSender; - storageService: LowdbStorageService; - secureStorageService: NodeEnvSecureStorageService; - memoryStorageService: MemoryStorageService; - memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; - i18nService: I18nService; - platformUtilsService: CliPlatformUtilsService; - cryptoService: CryptoService; - tokenService: TokenService; - appIdService: AppIdService; - apiService: NodeApiService; - environmentService: EnvironmentService; - cipherService: CipherService; - folderService: InternalFolderService; - organizationUserService: OrganizationUserService; - collectionService: CollectionService; - vaultTimeoutService: VaultTimeoutService; - masterPasswordService: InternalMasterPasswordServiceAbstraction; - vaultTimeoutSettingsService: VaultTimeoutSettingsService; - syncService: SyncService; - eventCollectionService: EventCollectionServiceAbstraction; - eventUploadService: EventUploadServiceAbstraction; - passwordGenerationService: PasswordGenerationServiceAbstraction; - passwordStrengthService: PasswordStrengthServiceAbstraction; - userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; - totpService: TotpService; - containerService: ContainerService; - auditService: AuditService; - importService: ImportServiceAbstraction; - importApiService: ImportApiServiceAbstraction; - exportService: VaultExportServiceAbstraction; - individualExportService: IndividualVaultExportServiceAbstraction; - organizationExportService: OrganizationVaultExportServiceAbstraction; - searchService: SearchService; - keyGenerationService: KeyGenerationServiceAbstraction; - cryptoFunctionService: NodeCryptoFunctionService; - encryptService: EncryptServiceImplementation; - authService: AuthService; - policyService: PolicyService; - policyApiService: PolicyApiServiceAbstraction; - program: Program; - vaultProgram: VaultProgram; - sendProgram: SendProgram; - logService: ConsoleLogService; - sendService: SendService; - sendStateProvider: SendStateProvider; - fileUploadService: FileUploadService; - cipherFileUploadService: CipherFileUploadService; - keyConnectorService: KeyConnectorService; - userVerificationService: UserVerificationService; - pinService: PinServiceAbstraction; - stateService: StateService; - autofillSettingsService: AutofillSettingsServiceAbstraction; - domainSettingsService: DomainSettingsService; - organizationService: OrganizationService; - providerService: ProviderService; - twoFactorService: TwoFactorService; - folderApiService: FolderApiService; - userVerificationApiService: UserVerificationApiService; - organizationApiService: OrganizationApiServiceAbstraction; - syncNotifierService: SyncNotifierService; - sendApiService: SendApiService; - devicesApiService: DevicesApiServiceAbstraction; - deviceTrustService: DeviceTrustServiceAbstraction; - authRequestService: AuthRequestService; - configApiService: ConfigApiServiceAbstraction; - configService: ConfigService; - accountService: AccountService; - globalStateProvider: GlobalStateProvider; - singleUserStateProvider: SingleUserStateProvider; - activeUserStateProvider: ActiveUserStateProvider; - derivedStateProvider: DerivedStateProvider; - stateProvider: StateProvider; - loginStrategyService: LoginStrategyServiceAbstraction; - avatarService: AvatarServiceAbstraction; - stateEventRunnerService: StateEventRunnerService; - biometricStateService: BiometricStateService; - billingAccountProfileStateService: BillingAccountProfileStateService; - providerApiService: ProviderApiServiceAbstraction; - userAutoUnlockKeyService: UserAutoUnlockKeyService; - kdfConfigService: KdfConfigServiceAbstraction; - - constructor() { - let p = null; - const relativeDataDir = path.join(path.dirname(process.execPath), "bw-data"); - if (fs.existsSync(relativeDataDir)) { - p = relativeDataDir; - } else if (process.env.BITWARDENCLI_APPDATA_DIR) { - p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR); - } else if (process.platform === "darwin") { - p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI"); - } else if (process.platform === "win32") { - p = path.join(process.env.APPDATA, "Bitwarden CLI"); - } else if (process.env.XDG_CONFIG_HOME) { - p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI"); - } else { - p = path.join(process.env.HOME, ".config/Bitwarden CLI"); - } - - this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson); - this.logService = new ConsoleLogService( - this.platformUtilsService.isDev(), - (level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info, - ); - this.cryptoFunctionService = new NodeCryptoFunctionService(); - this.encryptService = new EncryptServiceImplementation( - this.cryptoFunctionService, - this.logService, - true, - ); - this.storageService = new LowdbStorageService(this.logService, null, p, false, true); - this.secureStorageService = new NodeEnvSecureStorageService( - this.storageService, - this.logService, - this.encryptService, - ); - - this.memoryStorageService = new MemoryStorageService(); - this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders(); - - const storageServiceProvider = new StorageServiceProvider( - this.storageService, - this.memoryStorageForStateProviders, - ); - - this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); - - const stateEventRegistrarService = new StateEventRegistrarService( - this.globalStateProvider, - storageServiceProvider, - ); - - this.stateEventRunnerService = new StateEventRunnerService( - this.globalStateProvider, - storageServiceProvider, - ); - - this.i18nService = new I18nService("en", "./locales", this.globalStateProvider); - - this.singleUserStateProvider = new DefaultSingleUserStateProvider( - storageServiceProvider, - stateEventRegistrarService, - ); - - this.messagingService = MessageSender.EMPTY; - - this.accountService = new AccountServiceImplementation( - this.messagingService, - this.logService, - this.globalStateProvider, - ); - - this.activeUserStateProvider = new DefaultActiveUserStateProvider( - this.accountService, - this.singleUserStateProvider, - ); - - this.derivedStateProvider = new DefaultDerivedStateProvider(); - - this.stateProvider = new DefaultStateProvider( - this.activeUserStateProvider, - this.singleUserStateProvider, - this.globalStateProvider, - this.derivedStateProvider, - ); - - this.environmentService = new DefaultEnvironmentService( - this.stateProvider, - this.accountService, - ); - - this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); - - this.tokenService = new TokenService( - this.singleUserStateProvider, - this.globalStateProvider, - this.platformUtilsService.supportsSecureStorage(), - this.secureStorageService, - this.keyGenerationService, - this.encryptService, - this.logService, - ); - - const migrationRunner = new MigrationRunner( - this.storageService, - this.logService, - new MigrationBuilderService(), - ClientType.Cli, - ); - - this.stateService = new StateService( - this.storageService, - this.secureStorageService, - this.memoryStorageService, - this.logService, - new StateFactory(GlobalState, Account), - this.accountService, - this.environmentService, - this.tokenService, - migrationRunner, - ); - - this.masterPasswordService = new MasterPasswordService( - this.stateProvider, - this.stateService, - this.keyGenerationService, - this.encryptService, - ); - - this.kdfConfigService = new KdfConfigService(this.stateProvider); - - this.pinService = new PinService( - this.accountService, - this.cryptoFunctionService, - this.encryptService, - this.kdfConfigService, - this.keyGenerationService, - this.logService, - this.masterPasswordService, - this.stateProvider, - this.stateService, - ); - - this.cryptoService = new CryptoService( - this.pinService, - this.masterPasswordService, - this.keyGenerationService, - this.cryptoFunctionService, - this.encryptService, - this.platformUtilsService, - this.logService, - this.stateService, - this.accountService, - this.stateProvider, - this.kdfConfigService, - ); - - this.appIdService = new AppIdService(this.globalStateProvider); - - const customUserAgent = - "Bitwarden_CLI/" + - this.platformUtilsService.getApplicationVersionSync() + - " (" + - this.platformUtilsService.getDeviceString().toUpperCase() + - ")"; - - this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - - this.organizationService = new OrganizationService(this.stateProvider); - this.policyService = new PolicyService(this.stateProvider, this.organizationService); - - this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( - this.accountService, - this.pinService, - this.userDecryptionOptionsService, - this.cryptoService, - this.tokenService, - this.policyService, - this.biometricStateService, - this.stateProvider, - this.logService, - VaultTimeoutStringType.Never, // default vault timeout - ); - - this.apiService = new NodeApiService( - this.tokenService, - this.platformUtilsService, - this.environmentService, - this.appIdService, - this.vaultTimeoutSettingsService, - async (expired: boolean) => await this.logout(), - customUserAgent, - ); - - this.syncNotifierService = new SyncNotifierService(); - - this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); - - this.containerService = new ContainerService(this.cryptoService, this.encryptService); - - this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); - - this.fileUploadService = new FileUploadService(this.logService); - - this.sendStateProvider = new SendStateProvider(this.stateProvider); - - this.sendService = new SendService( - this.cryptoService, - this.i18nService, - this.keyGenerationService, - this.sendStateProvider, - this.encryptService, - ); - - this.cipherFileUploadService = new CipherFileUploadService( - this.apiService, - this.fileUploadService, - ); - - this.sendApiService = this.sendApiService = new SendApiService( - this.apiService, - this.fileUploadService, - this.sendService, - ); - - this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); - - this.collectionService = new CollectionService( - this.cryptoService, - this.i18nService, - this.stateProvider, - ); - - this.providerService = new ProviderService(this.stateProvider); - - this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); - - this.policyApiService = new PolicyApiService(this.policyService, this.apiService); - - this.keyConnectorService = new KeyConnectorService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.logService, - this.organizationService, - this.keyGenerationService, - async (expired: boolean) => await this.logout(), - this.stateProvider, - ); - - this.twoFactorService = new TwoFactorService( - this.i18nService, - this.platformUtilsService, - this.globalStateProvider, - ); - - this.passwordStrengthService = new PasswordStrengthService(); - - this.passwordGenerationService = new PasswordGenerationService( - this.cryptoService, - this.policyService, - this.stateService, - ); - - this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustService = new DeviceTrustService( - this.keyGenerationService, - this.cryptoFunctionService, - this.cryptoService, - this.encryptService, - this.appIdService, - this.devicesApiService, - this.i18nService, - this.platformUtilsService, - this.stateProvider, - this.secureStorageService, - this.userDecryptionOptionsService, - this.logService, - ); - - this.authRequestService = new AuthRequestService( - this.appIdService, - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.stateProvider, - ); - - this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.stateProvider, - ); - - this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.keyConnectorService, - this.environmentService, - this.stateService, - this.twoFactorService, - this.i18nService, - this.encryptService, - this.passwordStrengthService, - this.policyService, - this.deviceTrustService, - this.authRequestService, - this.userDecryptionOptionsService, - this.globalStateProvider, - this.billingAccountProfileStateService, - this.vaultTimeoutSettingsService, - this.kdfConfigService, - ); - - this.authService = new AuthService( - this.accountService, - this.messagingService, - this.cryptoService, - this.apiService, - this.stateService, - this.tokenService, - ); - - this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - - this.configService = new DefaultConfigService( - this.configApiService, - this.environmentService, - this.logService, - this.stateProvider, - ); - - this.cipherService = new CipherService( - this.cryptoService, - this.domainSettingsService, - this.apiService, - this.i18nService, - this.searchService, - this.stateService, - this.autofillSettingsService, - this.encryptService, - this.cipherFileUploadService, - this.configService, - this.stateProvider, - ); - - this.folderService = new FolderService( - this.cryptoService, - this.i18nService, - this.cipherService, - this.stateProvider, - ); - - this.folderApiService = new FolderApiService(this.folderService, this.apiService); - - const lockedCallback = async (userId?: string) => - await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); - - this.userVerificationService = new UserVerificationService( - this.stateService, - this.cryptoService, - this.accountService, - this.masterPasswordService, - this.i18nService, - this.userVerificationApiService, - this.userDecryptionOptionsService, - this.pinService, - this.logService, - this.vaultTimeoutSettingsService, - this.platformUtilsService, - this.kdfConfigService, - ); - - this.vaultTimeoutService = new VaultTimeoutService( - this.accountService, - this.masterPasswordService, - this.cipherService, - this.folderService, - this.collectionService, - this.platformUtilsService, - this.messagingService, - this.searchService, - this.stateService, - this.authService, - this.vaultTimeoutSettingsService, - this.stateEventRunnerService, - lockedCallback, - null, - ); - - this.avatarService = new AvatarService(this.apiService, this.stateProvider); - - this.syncService = new SyncService( - this.masterPasswordService, - this.accountService, - this.apiService, - this.domainSettingsService, - this.folderService, - this.cipherService, - this.cryptoService, - this.collectionService, - this.messagingService, - this.policyService, - this.sendService, - this.logService, - this.keyConnectorService, - this.stateService, - this.providerService, - this.folderApiService, - this.organizationService, - this.sendApiService, - this.userDecryptionOptionsService, - this.avatarService, - async (expired: boolean) => await this.logout(), - this.billingAccountProfileStateService, - this.tokenService, - this.authService, - ); - - this.totpService = new TotpService(this.cryptoFunctionService, this.logService); - - this.importApiService = new ImportApiService(this.apiService); - - this.importService = new ImportService( - this.cipherService, - this.folderService, - this.importApiService, - this.i18nService, - this.collectionService, - this.cryptoService, - this.pinService, - ); - - this.individualExportService = new IndividualVaultExportService( - this.folderService, - this.cipherService, - this.pinService, - this.cryptoService, - this.cryptoFunctionService, - this.kdfConfigService, - ); - - this.organizationExportService = new OrganizationVaultExportService( - this.cipherService, - this.apiService, - this.pinService, - this.cryptoService, - this.cryptoFunctionService, - this.collectionService, - this.kdfConfigService, - ); - - this.exportService = new VaultExportService( - this.individualExportService, - this.organizationExportService, - ); - - this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); - - this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); - this.program = new Program(this); - this.vaultProgram = new VaultProgram(this); - this.sendProgram = new SendProgram(this); - - this.userVerificationApiService = new UserVerificationApiService(this.apiService); - - this.eventUploadService = new EventUploadService( - this.apiService, - this.stateProvider, - this.logService, - this.authService, - ); - - this.eventCollectionService = new EventCollectionService( - this.cipherService, - this.stateProvider, - this.organizationService, - this.eventUploadService, - this.authService, - ); - - this.providerApiService = new ProviderApiService(this.apiService); - } - - async run() { - await this.init(); - - await this.program.register(); - await this.vaultProgram.register(); - await this.sendProgram.register(); - - program.parse(process.argv); - - if (process.argv.slice(2).length === 0) { - program.outputHelp(); - } - } - - async logout() { - this.authService.logOut(() => { - /* Do nothing */ - }); - const userId = (await this.stateService.getUserId()) as UserId; - await Promise.all([ - this.eventUploadService.uploadEvents(userId as UserId), - this.syncService.setLastSync(new Date(0)), - this.cryptoService.clearKeys(), - this.cipherService.clear(userId), - this.folderService.clear(userId), - this.collectionService.clear(userId as UserId), - this.passwordGenerationService.clear(), - ]); - - await this.stateEventRunnerService.handleEvent("logout", userId); - - await this.stateService.clean(); - await this.accountService.clean(userId); - process.env.BW_SESSION = null; - } - - private async init() { - await this.storageService.init(); - await this.stateService.init(); - this.containerService.attachToGlobal(global); - await this.i18nService.init(); - this.twoFactorService.init(); - - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - if (activeAccount) { - await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); - } - } + program.parse(process.argv); } -const main = new Main(); -// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. +// Node does not support top-level await statements until ES2022, esnext, etc which we don't use yet // eslint-disable-next-line @typescript-eslint/no-floating-promises -main.run(); +main(); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 7a11dc4b4a..aad205998f 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -11,9 +11,9 @@ import { ConfirmCommand } from "../admin-console/commands/confirm.command"; import { ShareCommand } from "../admin-console/commands/share.command"; import { LockCommand } from "../auth/commands/lock.command"; import { UnlockCommand } from "../auth/commands/unlock.command"; -import { Main } from "../bw"; import { Response } from "../models/response"; import { FileResponse } from "../models/response/file.response"; +import { ServiceContainer } from "../service-container"; import { GenerateCommand } from "../tools/generate.command"; import { SendEditCommand, @@ -55,116 +55,119 @@ export class ServeCommand { private sendListCommand: SendListCommand; private sendRemovePasswordCommand: SendRemovePasswordCommand; - constructor(protected main: Main) { + constructor(protected serviceContainer: ServiceContainer) { this.getCommand = new GetCommand( - this.main.cipherService, - this.main.folderService, - this.main.collectionService, - this.main.totpService, - this.main.auditService, - this.main.cryptoService, - this.main.stateService, - this.main.searchService, - this.main.apiService, - this.main.organizationService, - this.main.eventCollectionService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.totpService, + this.serviceContainer.auditService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.searchService, + this.serviceContainer.apiService, + this.serviceContainer.organizationService, + this.serviceContainer.eventCollectionService, + this.serviceContainer.billingAccountProfileStateService, ); this.listCommand = new ListCommand( - this.main.cipherService, - this.main.folderService, - this.main.collectionService, - this.main.organizationService, - this.main.searchService, - this.main.organizationUserService, - this.main.apiService, - this.main.eventCollectionService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.organizationService, + this.serviceContainer.searchService, + this.serviceContainer.organizationUserService, + this.serviceContainer.apiService, + this.serviceContainer.eventCollectionService, ); this.createCommand = new CreateCommand( - this.main.cipherService, - this.main.folderService, - this.main.cryptoService, - this.main.apiService, - this.main.folderApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.cryptoService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + this.serviceContainer.billingAccountProfileStateService, ); this.editCommand = new EditCommand( - this.main.cipherService, - this.main.folderService, - this.main.cryptoService, - this.main.apiService, - this.main.folderApiService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.cryptoService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, ); this.generateCommand = new GenerateCommand( - this.main.passwordGenerationService, - this.main.stateService, + this.serviceContainer.passwordGenerationService, + this.serviceContainer.stateService, ); - this.syncCommand = new SyncCommand(this.main.syncService); + this.syncCommand = new SyncCommand(this.serviceContainer.syncService); this.statusCommand = new StatusCommand( - this.main.environmentService, - this.main.syncService, - this.main.stateService, - this.main.authService, + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.stateService, + this.serviceContainer.authService, ); this.deleteCommand = new DeleteCommand( - this.main.cipherService, - this.main.folderService, - this.main.apiService, - this.main.folderApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + this.serviceContainer.billingAccountProfileStateService, ); this.confirmCommand = new ConfirmCommand( - this.main.apiService, - this.main.cryptoService, - this.main.organizationUserService, + this.serviceContainer.apiService, + this.serviceContainer.cryptoService, + this.serviceContainer.organizationUserService, ); - this.restoreCommand = new RestoreCommand(this.main.cipherService); - this.shareCommand = new ShareCommand(this.main.cipherService); - this.lockCommand = new LockCommand(this.main.vaultTimeoutService); + this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService); + this.shareCommand = new ShareCommand(this.serviceContainer.cipherService); + this.lockCommand = new LockCommand(this.serviceContainer.vaultTimeoutService); this.unlockCommand = new UnlockCommand( - this.main.accountService, - this.main.masterPasswordService, - this.main.cryptoService, - this.main.stateService, - this.main.cryptoFunctionService, - this.main.apiService, - this.main.logService, - this.main.keyConnectorService, - this.main.environmentService, - this.main.syncService, - this.main.organizationApiService, - async () => await this.main.logout(), - this.main.kdfConfigService, + this.serviceContainer.accountService, + this.serviceContainer.masterPasswordService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.cryptoFunctionService, + this.serviceContainer.apiService, + this.serviceContainer.logService, + this.serviceContainer.keyConnectorService, + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.organizationApiService, + async () => await this.serviceContainer.logout(), + this.serviceContainer.kdfConfigService, ); this.sendCreateCommand = new SendCreateCommand( - this.main.sendService, - this.main.environmentService, - this.main.sendApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.sendApiService, + this.serviceContainer.billingAccountProfileStateService, + ); + this.sendDeleteCommand = new SendDeleteCommand( + this.serviceContainer.sendService, + this.serviceContainer.sendApiService, ); - this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService, this.main.sendApiService); this.sendGetCommand = new SendGetCommand( - this.main.sendService, - this.main.environmentService, - this.main.searchService, - this.main.cryptoService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, + this.serviceContainer.cryptoService, ); this.sendEditCommand = new SendEditCommand( - this.main.sendService, + this.serviceContainer.sendService, this.sendGetCommand, - this.main.sendApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.sendApiService, + this.serviceContainer.billingAccountProfileStateService, ); this.sendListCommand = new SendListCommand( - this.main.sendService, - this.main.environmentService, - this.main.searchService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, ); this.sendRemovePasswordCommand = new SendRemovePasswordCommand( - this.main.sendService, - this.main.sendApiService, - this.main.environmentService, + this.serviceContainer.sendService, + this.serviceContainer.sendApiService, + this.serviceContainer.environmentService, ); } @@ -172,7 +175,7 @@ export class ServeCommand { const protectOrigin = !options.disableOriginProtection; const port = options.port || 8087; const hostname = options.hostname || "localhost"; - this.main.logService.info( + this.serviceContainer.logService.info( `Starting server on ${hostname}:${port} with ${ protectOrigin ? "origin protection" : "no origin protection" }`, @@ -187,7 +190,7 @@ export class ServeCommand { .use(async (ctx, next) => { if (protectOrigin && ctx.headers.origin != undefined) { ctx.status = 403; - this.main.logService.warning( + this.serviceContainer.logService.warning( `Blocking request from "${ Utils.isNullOrEmpty(ctx.headers.origin) ? "(Origin header value missing)" @@ -407,7 +410,7 @@ export class ServeCommand { .use(router.routes()) .use(router.allowedMethods()) .listen(port, hostname === "all" ? null : hostname, () => { - this.main.logService.info("Listening on " + hostname + ":" + port); + this.serviceContainer.logService.info("Listening on " + hostname + ":" + port); }); } @@ -426,12 +429,12 @@ export class ServeCommand { } private async errorIfLocked(res: koa.Response) { - const authed = await this.main.stateService.getIsAuthenticated(); + const authed = await this.serviceContainer.stateService.getIsAuthenticated(); if (!authed) { this.processResponse(res, Response.error("You are not logged in.")); return true; } - if (await this.main.cryptoService.hasUserKey()) { + if (await this.serviceContainer.cryptoService.hasUserKey()) { return false; } this.processResponse(res, Response.error("Vault is locked.")); diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 9664b75776..3a2858aa81 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -8,7 +8,6 @@ import { LockCommand } from "./auth/commands/lock.command"; import { LoginCommand } from "./auth/commands/login.command"; import { LogoutCommand } from "./auth/commands/logout.command"; import { UnlockCommand } from "./auth/commands/unlock.command"; -import { Main } from "./bw"; import { CompletionCommand } from "./commands/completion.command"; import { ConfigCommand } from "./commands/config.command"; import { EncodeCommand } from "./commands/encode.command"; @@ -20,6 +19,7 @@ import { ListResponse } from "./models/response/list.response"; import { MessageResponse } from "./models/response/message.response"; import { StringResponse } from "./models/response/string.response"; import { TemplateResponse } from "./models/response/template.response"; +import { ServiceContainer } from "./service-container"; import { GenerateCommand } from "./tools/generate.command"; import { CliUtils } from "./utils"; import { SyncCommand } from "./vault/sync.command"; @@ -27,7 +27,7 @@ import { SyncCommand } from "./vault/sync.command"; const writeLn = CliUtils.writeLn; export class Program { - constructor(protected main: Main) {} + constructor(protected serviceContainer: ServiceContainer) {} async register() { program @@ -38,7 +38,10 @@ export class Program { .option("--quiet", "Don't return anything to stdout.") .option("--nointeraction", "Do not prompt for interactive user input.") .option("--session ", "Pass session key instead of reading from env.") - .version(await this.main.platformUtilsService.getApplicationVersion(), "-v, --version"); + .version( + await this.serviceContainer.platformUtilsService.getApplicationVersion(), + "-v, --version", + ); program.on("option:pretty", () => { process.env.BW_PRETTY = "true"; @@ -68,9 +71,11 @@ export class Program { process.env.BW_SESSION = key; // once we have the session key, we can set the user key in memory - const activeAccount = await firstValueFrom(this.main.accountService.activeAccount$); + const activeAccount = await firstValueFrom( + this.serviceContainer.accountService.activeAccount$, + ); if (activeAccount) { - await this.main.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet( + await this.serviceContainer.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet( activeAccount.id, ); } @@ -122,7 +127,7 @@ export class Program { "Path to a file containing your password as its first line", ) .option("--check", "Check login status.", async () => { - const authed = await this.main.stateService.getIsAuthenticated(); + const authed = await this.serviceContainer.stateService.getIsAuthenticated(); if (authed) { const res = new MessageResponse("You are logged in!", null); this.processResponse(Response.success(res), true); @@ -148,24 +153,24 @@ export class Program { if (!options.check) { await this.exitIfAuthed(); const command = new LoginCommand( - this.main.loginStrategyService, - this.main.authService, - this.main.apiService, - this.main.cryptoFunctionService, - this.main.environmentService, - this.main.passwordGenerationService, - this.main.passwordStrengthService, - this.main.platformUtilsService, - this.main.stateService, - this.main.cryptoService, - this.main.policyService, - this.main.twoFactorService, - this.main.syncService, - this.main.keyConnectorService, - this.main.policyApiService, - this.main.organizationService, - async () => await this.main.logout(), - this.main.kdfConfigService, + this.serviceContainer.loginStrategyService, + this.serviceContainer.authService, + this.serviceContainer.apiService, + this.serviceContainer.cryptoFunctionService, + this.serviceContainer.environmentService, + this.serviceContainer.passwordGenerationService, + this.serviceContainer.passwordStrengthService, + this.serviceContainer.platformUtilsService, + this.serviceContainer.stateService, + this.serviceContainer.cryptoService, + this.serviceContainer.policyService, + this.serviceContainer.twoFactorService, + this.serviceContainer.syncService, + this.serviceContainer.keyConnectorService, + this.serviceContainer.policyApiService, + this.serviceContainer.organizationService, + async () => await this.serviceContainer.logout(), + this.serviceContainer.kdfConfigService, ); const response = await command.run(email, password, options); this.processResponse(response, true); @@ -184,9 +189,9 @@ export class Program { .action(async (cmd) => { await this.exitIfNotAuthed(); const command = new LogoutCommand( - this.main.authService, - this.main.i18nService, - async () => await this.main.logout(), + this.serviceContainer.authService, + this.serviceContainer.i18nService, + async () => await this.serviceContainer.logout(), ); const response = await command.run(); this.processResponse(response); @@ -204,11 +209,11 @@ export class Program { .action(async (cmd) => { await this.exitIfNotAuthed(); - if (await this.main.keyConnectorService.getUsesKeyConnector()) { + if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) { const logoutCommand = new LogoutCommand( - this.main.authService, - this.main.i18nService, - async () => await this.main.logout(), + this.serviceContainer.authService, + this.serviceContainer.i18nService, + async () => await this.serviceContainer.logout(), ); await logoutCommand.run(); this.processResponse( @@ -221,7 +226,7 @@ export class Program { return; } - const command = new LockCommand(this.main.vaultTimeoutService); + const command = new LockCommand(this.serviceContainer.vaultTimeoutService); const response = await command.run(); this.processResponse(response); }); @@ -246,7 +251,7 @@ export class Program { .option("--check", "Check lock status.", async () => { await this.exitIfNotAuthed(); - const authStatus = await this.main.authService.getAuthStatus(); + const authStatus = await this.serviceContainer.authService.getAuthStatus(); if (authStatus === AuthenticationStatus.Unlocked) { const res = new MessageResponse("Vault is unlocked!", null); this.processResponse(Response.success(res), true); @@ -263,19 +268,19 @@ export class Program { if (!cmd.check) { await this.exitIfNotAuthed(); const command = new UnlockCommand( - this.main.accountService, - this.main.masterPasswordService, - this.main.cryptoService, - this.main.stateService, - this.main.cryptoFunctionService, - this.main.apiService, - this.main.logService, - this.main.keyConnectorService, - this.main.environmentService, - this.main.syncService, - this.main.organizationApiService, - async () => await this.main.logout(), - this.main.kdfConfigService, + this.serviceContainer.accountService, + this.serviceContainer.masterPasswordService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.cryptoFunctionService, + this.serviceContainer.apiService, + this.serviceContainer.logService, + this.serviceContainer.keyConnectorService, + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.organizationApiService, + async () => await this.serviceContainer.logout(), + this.serviceContainer.kdfConfigService, ); const response = await command.run(password, cmd); this.processResponse(response); @@ -297,7 +302,7 @@ export class Program { }) .action(async (cmd) => { await this.exitIfNotAuthed(); - const command = new SyncCommand(this.main.syncService); + const command = new SyncCommand(this.serviceContainer.syncService); const response = await command.run(cmd); this.processResponse(response); }); @@ -340,8 +345,8 @@ export class Program { }) .action(async (options) => { const command = new GenerateCommand( - this.main.passwordGenerationService, - this.main.stateService, + this.serviceContainer.passwordGenerationService, + this.serviceContainer.stateService, ); const response = await command.run(options); this.processResponse(response); @@ -401,7 +406,7 @@ export class Program { writeLn("", true); }) .action(async (setting, value, options) => { - const command = new ConfigCommand(this.main.environmentService); + const command = new ConfigCommand(this.serviceContainer.environmentService); const response = await command.run(setting, value, options); this.processResponse(response); }); @@ -423,7 +428,7 @@ export class Program { writeLn("", true); }) .action(async () => { - const command = new UpdateCommand(this.main.platformUtilsService); + const command = new UpdateCommand(this.serviceContainer.platformUtilsService); const response = await command.run(); this.processResponse(response); }); @@ -474,10 +479,10 @@ export class Program { }) .action(async () => { const command = new StatusCommand( - this.main.environmentService, - this.main.syncService, - this.main.stateService, - this.main.authService, + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.stateService, + this.serviceContainer.authService, ); const response = await command.run(); this.processResponse(response); @@ -508,7 +513,7 @@ export class Program { }) .action(async (cmd) => { await this.exitIfNotAuthed(); - const command = new ServeCommand(this.main); + const command = new ServeCommand(this.serviceContainer); await command.run(cmd); }); } @@ -598,15 +603,15 @@ export class Program { } private async exitIfAuthed() { - const authed = await this.main.stateService.getIsAuthenticated(); + const authed = await this.serviceContainer.stateService.getIsAuthenticated(); if (authed) { - const email = await this.main.stateService.getEmail(); + const email = await this.serviceContainer.stateService.getEmail(); this.processResponse(Response.error("You are already logged in as " + email + "."), true); } } private async exitIfNotAuthed() { - const authed = await this.main.stateService.getIsAuthenticated(); + const authed = await this.serviceContainer.stateService.getIsAuthenticated(); if (!authed) { this.processResponse(Response.error("You are not logged in."), true); } @@ -614,11 +619,11 @@ export class Program { protected async exitIfLocked() { await this.exitIfNotAuthed(); - if (await this.main.cryptoService.hasUserKey()) { + if (await this.serviceContainer.cryptoService.hasUserKey()) { return; } else if (process.env.BW_NOINTERACTION !== "true") { // must unlock - if (await this.main.keyConnectorService.getUsesKeyConnector()) { + if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) { const response = Response.error( "Your vault is locked. You must unlock your vault using your session key.\n" + "If you do not have your session key, you can get a new one by logging out and logging in again.", @@ -626,19 +631,19 @@ export class Program { this.processResponse(response, true); } else { const command = new UnlockCommand( - this.main.accountService, - this.main.masterPasswordService, - this.main.cryptoService, - this.main.stateService, - this.main.cryptoFunctionService, - this.main.apiService, - this.main.logService, - this.main.keyConnectorService, - this.main.environmentService, - this.main.syncService, - this.main.organizationApiService, - this.main.logout, - this.main.kdfConfigService, + this.serviceContainer.accountService, + this.serviceContainer.masterPasswordService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.cryptoFunctionService, + this.serviceContainer.apiService, + this.serviceContainer.logService, + this.serviceContainer.keyConnectorService, + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.organizationApiService, + this.serviceContainer.logout, + this.serviceContainer.kdfConfigService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/cli/src/register-oss-programs.ts b/apps/cli/src/register-oss-programs.ts new file mode 100644 index 0000000000..f47aa52854 --- /dev/null +++ b/apps/cli/src/register-oss-programs.ts @@ -0,0 +1,22 @@ +import { Program } from "./program"; +import { ServiceContainer } from "./service-container"; +import { SendProgram } from "./tools/send/send.program"; +import { VaultProgram } from "./vault.program"; + +/** + * All OSS licensed programs should be registered here. + * @example + * const myProgram = new myProgram(serviceContainer); + * myProgram.register(); + * @param serviceContainer A class that instantiates services and makes them available for dependency injection + */ +export async function registerOssPrograms(serviceContainer: ServiceContainer) { + const program = new Program(serviceContainer); + await program.register(); + + const vaultProgram = new VaultProgram(serviceContainer); + await vaultProgram.register(); + + const sendProgram = new SendProgram(serviceContainer); + await sendProgram.register(); +} diff --git a/apps/cli/src/service-container.spec.ts b/apps/cli/src/service-container.spec.ts new file mode 100644 index 0000000000..26258c367c --- /dev/null +++ b/apps/cli/src/service-container.spec.ts @@ -0,0 +1,7 @@ +import { ServiceContainer } from "./service-container"; + +describe("ServiceContainer", () => { + it("instantiates", async () => { + expect(() => new ServiceContainer()).not.toThrow(); + }); +}); diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts new file mode 100644 index 0000000000..cffdc53444 --- /dev/null +++ b/apps/cli/src/service-container.ts @@ -0,0 +1,772 @@ +import * as fs from "fs"; +import * as path from "path"; + +import * as jsdom from "jsdom"; +import { firstValueFrom } from "rxjs"; + +import { + InternalUserDecryptionOptionsServiceAbstraction, + AuthRequestService, + LoginStrategyService, + LoginStrategyServiceAbstraction, + PinService, + PinServiceAbstraction, + UserDecryptionOptionsService, +} from "@bitwarden/auth/common"; +import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; +import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; +import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; +import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; +import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; +import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; +import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; +import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; +import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; +import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; +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 { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { + BiometricStateService, + DefaultBiometricStateService, +} from "@bitwarden/common/platform/biometrics/biometric-state.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"; +import { Account } from "@bitwarden/common/platform/models/domain/account"; +import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; +import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; +import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; +import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; +import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; +import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; +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 { 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"; +import { + ActiveUserStateProvider, + DerivedStateProvider, + GlobalStateProvider, + SingleUserStateProvider, + StateEventRunnerService, + StateProvider, +} from "@bitwarden/common/platform/state"; +/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */ +import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; +import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; +import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider"; +import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider"; +import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; +import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; +import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; +/* eslint-enable import/no-restricted-paths */ +import { AuditService } from "@bitwarden/common/services/audit.service"; +import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; +import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; +import { SearchService } from "@bitwarden/common/services/search.service"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; +import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; +import { + PasswordGenerationService, + PasswordGenerationServiceAbstraction, +} from "@bitwarden/common/tools/generator/password"; +import { + PasswordStrengthService, + PasswordStrengthServiceAbstraction, +} from "@bitwarden/common/tools/password-strength"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; +import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; +import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/services/collection.service"; +import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; +import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; +import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; +import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service"; +import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; +import { TotpService } from "@bitwarden/common/vault/services/totp.service"; +import { + ImportApiService, + ImportApiServiceAbstraction, + ImportService, + ImportServiceAbstraction, +} from "@bitwarden/importer/core"; +import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; +import { + IndividualVaultExportService, + IndividualVaultExportServiceAbstraction, + OrganizationVaultExportService, + OrganizationVaultExportServiceAbstraction, + VaultExportService, + VaultExportServiceAbstraction, +} from "@bitwarden/vault-export-core"; + +import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; +import { ConsoleLogService } from "./platform/services/console-log.service"; +import { I18nService } from "./platform/services/i18n.service"; +import { LowdbStorageService } from "./platform/services/lowdb-storage.service"; +import { NodeApiService } from "./platform/services/node-api.service"; +import { NodeEnvSecureStorageService } from "./platform/services/node-env-secure-storage.service"; + +// Polyfills +global.DOMParser = new jsdom.JSDOM().window.DOMParser; + +// eslint-disable-next-line +const packageJson = require("../package.json"); + +/** + * Instantiates services and makes them available for dependency injection. + * Any Bitwarden-licensed services should be registered here. + */ +export class ServiceContainer { + private inited = false; + + messagingService: MessageSender; + storageService: LowdbStorageService; + secureStorageService: NodeEnvSecureStorageService; + memoryStorageService: MemoryStorageService; + memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; + i18nService: I18nService; + platformUtilsService: CliPlatformUtilsService; + cryptoService: CryptoService; + tokenService: TokenService; + appIdService: AppIdService; + apiService: NodeApiService; + environmentService: EnvironmentService; + cipherService: CipherService; + folderService: InternalFolderService; + organizationUserService: OrganizationUserService; + collectionService: CollectionService; + vaultTimeoutService: VaultTimeoutService; + masterPasswordService: InternalMasterPasswordServiceAbstraction; + vaultTimeoutSettingsService: VaultTimeoutSettingsService; + syncService: SyncService; + eventCollectionService: EventCollectionServiceAbstraction; + eventUploadService: EventUploadServiceAbstraction; + passwordGenerationService: PasswordGenerationServiceAbstraction; + passwordStrengthService: PasswordStrengthServiceAbstraction; + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; + totpService: TotpService; + containerService: ContainerService; + auditService: AuditService; + importService: ImportServiceAbstraction; + importApiService: ImportApiServiceAbstraction; + exportService: VaultExportServiceAbstraction; + individualExportService: IndividualVaultExportServiceAbstraction; + organizationExportService: OrganizationVaultExportServiceAbstraction; + searchService: SearchService; + keyGenerationService: KeyGenerationServiceAbstraction; + cryptoFunctionService: NodeCryptoFunctionService; + encryptService: EncryptServiceImplementation; + authService: AuthService; + policyService: PolicyService; + policyApiService: PolicyApiServiceAbstraction; + logService: ConsoleLogService; + sendService: SendService; + sendStateProvider: SendStateProvider; + fileUploadService: FileUploadService; + cipherFileUploadService: CipherFileUploadService; + keyConnectorService: KeyConnectorService; + userVerificationService: UserVerificationService; + pinService: PinServiceAbstraction; + stateService: StateService; + autofillSettingsService: AutofillSettingsServiceAbstraction; + domainSettingsService: DomainSettingsService; + organizationService: OrganizationService; + providerService: ProviderService; + twoFactorService: TwoFactorService; + folderApiService: FolderApiService; + userVerificationApiService: UserVerificationApiService; + organizationApiService: OrganizationApiServiceAbstraction; + syncNotifierService: SyncNotifierService; + sendApiService: SendApiService; + devicesApiService: DevicesApiServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; + authRequestService: AuthRequestService; + configApiService: ConfigApiServiceAbstraction; + configService: ConfigService; + accountService: AccountService; + globalStateProvider: GlobalStateProvider; + singleUserStateProvider: SingleUserStateProvider; + activeUserStateProvider: ActiveUserStateProvider; + derivedStateProvider: DerivedStateProvider; + stateProvider: StateProvider; + loginStrategyService: LoginStrategyServiceAbstraction; + avatarService: AvatarServiceAbstraction; + stateEventRunnerService: StateEventRunnerService; + biometricStateService: BiometricStateService; + billingAccountProfileStateService: BillingAccountProfileStateService; + providerApiService: ProviderApiServiceAbstraction; + userAutoUnlockKeyService: UserAutoUnlockKeyService; + kdfConfigService: KdfConfigServiceAbstraction; + + constructor() { + let p = null; + const relativeDataDir = path.join(path.dirname(process.execPath), "bw-data"); + if (fs.existsSync(relativeDataDir)) { + p = relativeDataDir; + } else if (process.env.BITWARDENCLI_APPDATA_DIR) { + p = path.resolve(process.env.BITWARDENCLI_APPDATA_DIR); + } else if (process.platform === "darwin") { + p = path.join(process.env.HOME, "Library/Application Support/Bitwarden CLI"); + } else if (process.platform === "win32") { + p = path.join(process.env.APPDATA, "Bitwarden CLI"); + } else if (process.env.XDG_CONFIG_HOME) { + p = path.join(process.env.XDG_CONFIG_HOME, "Bitwarden CLI"); + } else { + p = path.join(process.env.HOME, ".config/Bitwarden CLI"); + } + + this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson); + this.logService = new ConsoleLogService( + this.platformUtilsService.isDev(), + (level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info, + ); + this.cryptoFunctionService = new NodeCryptoFunctionService(); + this.encryptService = new EncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + ); + this.storageService = new LowdbStorageService(this.logService, null, p, false, true); + this.secureStorageService = new NodeEnvSecureStorageService( + this.storageService, + this.logService, + this.encryptService, + ); + + this.memoryStorageService = new MemoryStorageService(); + this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders(); + + const storageServiceProvider = new StorageServiceProvider( + this.storageService, + this.memoryStorageForStateProviders, + ); + + this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); + + const stateEventRegistrarService = new StateEventRegistrarService( + this.globalStateProvider, + storageServiceProvider, + ); + + this.stateEventRunnerService = new StateEventRunnerService( + this.globalStateProvider, + storageServiceProvider, + ); + + this.i18nService = new I18nService("en", "./locales", this.globalStateProvider); + + this.singleUserStateProvider = new DefaultSingleUserStateProvider( + storageServiceProvider, + stateEventRegistrarService, + ); + + this.messagingService = MessageSender.EMPTY; + + this.accountService = new AccountServiceImplementation( + this.messagingService, + this.logService, + this.globalStateProvider, + ); + + this.activeUserStateProvider = new DefaultActiveUserStateProvider( + this.accountService, + this.singleUserStateProvider, + ); + + this.derivedStateProvider = new DefaultDerivedStateProvider(); + + this.stateProvider = new DefaultStateProvider( + this.activeUserStateProvider, + this.singleUserStateProvider, + this.globalStateProvider, + this.derivedStateProvider, + ); + + this.environmentService = new DefaultEnvironmentService( + this.stateProvider, + this.accountService, + ); + + this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + + this.tokenService = new TokenService( + this.singleUserStateProvider, + this.globalStateProvider, + this.platformUtilsService.supportsSecureStorage(), + this.secureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, + ); + + const migrationRunner = new MigrationRunner( + this.storageService, + this.logService, + new MigrationBuilderService(), + ClientType.Cli, + ); + + this.stateService = new StateService( + this.storageService, + this.secureStorageService, + this.memoryStorageService, + this.logService, + new StateFactory(GlobalState, Account), + this.accountService, + this.environmentService, + this.tokenService, + migrationRunner, + ); + + this.masterPasswordService = new MasterPasswordService( + this.stateProvider, + this.stateService, + this.keyGenerationService, + this.encryptService, + ); + + this.kdfConfigService = new KdfConfigService(this.stateProvider); + + this.pinService = new PinService( + this.accountService, + this.cryptoFunctionService, + this.encryptService, + this.kdfConfigService, + this.keyGenerationService, + this.logService, + this.masterPasswordService, + this.stateProvider, + this.stateService, + ); + + this.cryptoService = new CryptoService( + this.pinService, + this.masterPasswordService, + this.keyGenerationService, + this.cryptoFunctionService, + this.encryptService, + this.platformUtilsService, + this.logService, + this.stateService, + this.accountService, + this.stateProvider, + this.kdfConfigService, + ); + + this.appIdService = new AppIdService(this.globalStateProvider); + + const customUserAgent = + "Bitwarden_CLI/" + + this.platformUtilsService.getApplicationVersionSync() + + " (" + + this.platformUtilsService.getDeviceString().toUpperCase() + + ")"; + + this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + + this.organizationService = new OrganizationService(this.stateProvider); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); + + this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.accountService, + this.pinService, + this.userDecryptionOptionsService, + this.cryptoService, + this.tokenService, + this.policyService, + this.biometricStateService, + this.stateProvider, + this.logService, + VaultTimeoutStringType.Never, // default vault timeout + ); + + this.apiService = new NodeApiService( + this.tokenService, + this.platformUtilsService, + this.environmentService, + this.appIdService, + this.vaultTimeoutSettingsService, + async (expired: boolean) => await this.logout(), + customUserAgent, + ); + + this.syncNotifierService = new SyncNotifierService(); + + this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); + + this.containerService = new ContainerService(this.cryptoService, this.encryptService); + + this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); + + this.fileUploadService = new FileUploadService(this.logService); + + this.sendStateProvider = new SendStateProvider(this.stateProvider); + + this.sendService = new SendService( + this.cryptoService, + this.i18nService, + this.keyGenerationService, + this.sendStateProvider, + this.encryptService, + ); + + this.cipherFileUploadService = new CipherFileUploadService( + this.apiService, + this.fileUploadService, + ); + + this.sendApiService = this.sendApiService = new SendApiService( + this.apiService, + this.fileUploadService, + this.sendService, + ); + + this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); + + this.collectionService = new CollectionService( + this.cryptoService, + this.i18nService, + this.stateProvider, + ); + + this.providerService = new ProviderService(this.stateProvider); + + this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); + + this.policyApiService = new PolicyApiService(this.policyService, this.apiService); + + this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, + this.cryptoService, + this.apiService, + this.tokenService, + this.logService, + this.organizationService, + this.keyGenerationService, + async (expired: boolean) => await this.logout(), + this.stateProvider, + ); + + this.twoFactorService = new TwoFactorService( + this.i18nService, + this.platformUtilsService, + this.globalStateProvider, + ); + + this.passwordStrengthService = new PasswordStrengthService(); + + this.passwordGenerationService = new PasswordGenerationService( + this.cryptoService, + this.policyService, + this.stateService, + ); + + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); + this.deviceTrustService = new DeviceTrustService( + this.keyGenerationService, + this.cryptoFunctionService, + this.cryptoService, + this.encryptService, + this.appIdService, + this.devicesApiService, + this.i18nService, + this.platformUtilsService, + this.stateProvider, + this.secureStorageService, + this.userDecryptionOptionsService, + this.logService, + ); + + this.authRequestService = new AuthRequestService( + this.appIdService, + this.accountService, + this.masterPasswordService, + this.cryptoService, + this.apiService, + this.stateProvider, + ); + + this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( + this.stateProvider, + ); + + this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, + this.cryptoService, + this.apiService, + this.tokenService, + this.appIdService, + this.platformUtilsService, + this.messagingService, + this.logService, + this.keyConnectorService, + this.environmentService, + this.stateService, + this.twoFactorService, + this.i18nService, + this.encryptService, + this.passwordStrengthService, + this.policyService, + this.deviceTrustService, + this.authRequestService, + this.userDecryptionOptionsService, + this.globalStateProvider, + this.billingAccountProfileStateService, + this.vaultTimeoutSettingsService, + this.kdfConfigService, + ); + + this.authService = new AuthService( + this.accountService, + this.messagingService, + this.cryptoService, + this.apiService, + this.stateService, + this.tokenService, + ); + + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); + + this.configService = new DefaultConfigService( + this.configApiService, + this.environmentService, + this.logService, + this.stateProvider, + ); + + this.cipherService = new CipherService( + this.cryptoService, + this.domainSettingsService, + this.apiService, + this.i18nService, + this.searchService, + this.stateService, + this.autofillSettingsService, + this.encryptService, + this.cipherFileUploadService, + this.configService, + this.stateProvider, + ); + + this.folderService = new FolderService( + this.cryptoService, + this.i18nService, + this.cipherService, + this.stateProvider, + ); + + this.folderApiService = new FolderApiService(this.folderService, this.apiService); + + const lockedCallback = async (userId?: string) => + await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); + + this.userVerificationService = new UserVerificationService( + this.stateService, + this.cryptoService, + this.accountService, + this.masterPasswordService, + this.i18nService, + this.userVerificationApiService, + this.userDecryptionOptionsService, + this.pinService, + this.logService, + this.vaultTimeoutSettingsService, + this.platformUtilsService, + this.kdfConfigService, + ); + + this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, + this.cipherService, + this.folderService, + this.collectionService, + this.platformUtilsService, + this.messagingService, + this.searchService, + this.stateService, + this.authService, + this.vaultTimeoutSettingsService, + this.stateEventRunnerService, + lockedCallback, + null, + ); + + this.avatarService = new AvatarService(this.apiService, this.stateProvider); + + this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, + this.apiService, + this.domainSettingsService, + this.folderService, + this.cipherService, + this.cryptoService, + this.collectionService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.keyConnectorService, + this.stateService, + this.providerService, + this.folderApiService, + this.organizationService, + this.sendApiService, + this.userDecryptionOptionsService, + this.avatarService, + async (expired: boolean) => await this.logout(), + this.billingAccountProfileStateService, + this.tokenService, + this.authService, + ); + + this.totpService = new TotpService(this.cryptoFunctionService, this.logService); + + this.importApiService = new ImportApiService(this.apiService); + + this.importService = new ImportService( + this.cipherService, + this.folderService, + this.importApiService, + this.i18nService, + this.collectionService, + this.cryptoService, + this.pinService, + ); + + this.individualExportService = new IndividualVaultExportService( + this.folderService, + this.cipherService, + this.pinService, + this.cryptoService, + this.cryptoFunctionService, + this.kdfConfigService, + ); + + this.organizationExportService = new OrganizationVaultExportService( + this.cipherService, + this.apiService, + this.pinService, + this.cryptoService, + this.cryptoFunctionService, + this.collectionService, + this.kdfConfigService, + ); + + this.exportService = new VaultExportService( + this.individualExportService, + this.organizationExportService, + ); + + this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); + + this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); + + this.userVerificationApiService = new UserVerificationApiService(this.apiService); + + this.eventUploadService = new EventUploadService( + this.apiService, + this.stateProvider, + this.logService, + this.authService, + ); + + this.eventCollectionService = new EventCollectionService( + this.cipherService, + this.stateProvider, + this.organizationService, + this.eventUploadService, + this.authService, + ); + + this.providerApiService = new ProviderApiService(this.apiService); + } + + async logout() { + this.authService.logOut(() => { + /* Do nothing */ + }); + const userId = (await this.stateService.getUserId()) as UserId; + await Promise.all([ + this.eventUploadService.uploadEvents(userId as UserId), + this.syncService.setLastSync(new Date(0)), + this.cryptoService.clearKeys(), + this.cipherService.clear(userId), + this.folderService.clear(userId), + this.collectionService.clear(userId as UserId), + this.passwordGenerationService.clear(), + ]); + + await this.stateEventRunnerService.handleEvent("logout", userId); + + await this.stateService.clean(); + await this.accountService.clean(userId); + process.env.BW_SESSION = null; + } + + async init() { + if (this.inited) { + this.logService.warning("ServiceContainer.init called more than once"); + return; + } + + await this.storageService.init(); + await this.stateService.init(); + this.containerService.attachToGlobal(global); + await this.i18nService.init(); + this.twoFactorService.init(); + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount) { + await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); + } + + this.inited = true; + } +} diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 269719f887..86edd28f09 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -7,7 +7,6 @@ import { program, Command, OptionValues } from "commander"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { Main } from "../../bw"; import { GetCommand } from "../../commands/get.command"; import { Response } from "../../models/response"; import { Program } from "../../program"; @@ -29,10 +28,6 @@ import { SendResponse } from "./models/send.response"; const writeLn = CliUtils.writeLn; export class SendProgram extends Program { - constructor(main: Main) { - super(main); - } - async register() { program.addCommand(this.sendCommand()); // receive is accessible both at `bw receive` and `bw send receive` @@ -105,12 +100,12 @@ export class SendProgram extends Program { }) .action(async (url: string, options: OptionValues) => { const cmd = new SendReceiveCommand( - this.main.apiService, - this.main.cryptoService, - this.main.cryptoFunctionService, - this.main.platformUtilsService, - this.main.environmentService, - this.main.sendApiService, + this.serviceContainer.apiService, + this.serviceContainer.cryptoService, + this.serviceContainer.cryptoFunctionService, + this.serviceContainer.platformUtilsService, + this.serviceContainer.environmentService, + this.serviceContainer.sendApiService, ); const response = await cmd.run(url, options); this.processResponse(response); @@ -127,9 +122,9 @@ export class SendProgram extends Program { .action(async (options: OptionValues) => { await this.exitIfLocked(); const cmd = new SendListCommand( - this.main.sendService, - this.main.environmentService, - this.main.searchService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, ); const response = await cmd.run(options); this.processResponse(response); @@ -142,18 +137,18 @@ export class SendProgram extends Program { .description("Get json templates for send objects") .action(async (object) => { const cmd = new GetCommand( - this.main.cipherService, - this.main.folderService, - this.main.collectionService, - this.main.totpService, - this.main.auditService, - this.main.cryptoService, - this.main.stateService, - this.main.searchService, - this.main.apiService, - this.main.organizationService, - this.main.eventCollectionService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.totpService, + this.serviceContainer.auditService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.searchService, + this.serviceContainer.apiService, + this.serviceContainer.organizationService, + this.serviceContainer.eventCollectionService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await cmd.run("template", object, null); this.processResponse(response); @@ -188,10 +183,10 @@ export class SendProgram extends Program { .action(async (id: string, options: OptionValues) => { await this.exitIfLocked(); const cmd = new SendGetCommand( - this.main.sendService, - this.main.environmentService, - this.main.searchService, - this.main.cryptoService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, + this.serviceContainer.cryptoService, ); const response = await cmd.run(id, options); this.processResponse(response); @@ -247,16 +242,16 @@ export class SendProgram extends Program { .action(async (encodedJson: string, options: OptionValues) => { await this.exitIfLocked(); const getCmd = new SendGetCommand( - this.main.sendService, - this.main.environmentService, - this.main.searchService, - this.main.cryptoService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, + this.serviceContainer.cryptoService, ); const cmd = new SendEditCommand( - this.main.sendService, + this.serviceContainer.sendService, getCmd, - this.main.sendApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.sendApiService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await cmd.run(encodedJson, options); this.processResponse(response); @@ -269,7 +264,10 @@ export class SendProgram extends Program { .description("delete a Send") .action(async (id: string) => { await this.exitIfLocked(); - const cmd = new SendDeleteCommand(this.main.sendService, this.main.sendApiService); + const cmd = new SendDeleteCommand( + this.serviceContainer.sendService, + this.serviceContainer.sendApiService, + ); const response = await cmd.run(id); this.processResponse(response); }); @@ -282,9 +280,9 @@ export class SendProgram extends Program { .action(async (id: string) => { await this.exitIfLocked(); const cmd = new SendRemovePasswordCommand( - this.main.sendService, - this.main.sendApiService, - this.main.environmentService, + this.serviceContainer.sendService, + this.serviceContainer.sendApiService, + this.serviceContainer.environmentService, ); const response = await cmd.run(id); this.processResponse(response); @@ -323,10 +321,10 @@ export class SendProgram extends Program { private async runCreate(encodedJson: string, options: OptionValues) { await this.exitIfLocked(); const cmd = new SendCreateCommand( - this.main.sendService, - this.main.environmentService, - this.main.sendApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.sendApiService, + this.serviceContainer.billingAccountProfileStateService, ); return await cmd.run(encodedJson, options); } diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 1107c9aba0..52857ed542 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -2,7 +2,6 @@ import { program, Command } from "commander"; import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ShareCommand } from "./admin-console/commands/share.command"; -import { Main } from "./bw"; import { EditCommand } from "./commands/edit.command"; import { GetCommand } from "./commands/get.command"; import { ListCommand } from "./commands/list.command"; @@ -18,10 +17,6 @@ import { DeleteCommand } from "./vault/delete.command"; const writeLn = CliUtils.writeLn; export class VaultProgram extends Program { - constructor(protected main: Main) { - super(main); - } - async register() { program .addCommand(this.listCommand()) @@ -108,14 +103,14 @@ export class VaultProgram extends Program { await this.exitIfLocked(); const command = new ListCommand( - this.main.cipherService, - this.main.folderService, - this.main.collectionService, - this.main.organizationService, - this.main.searchService, - this.main.organizationUserService, - this.main.apiService, - this.main.eventCollectionService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.organizationService, + this.serviceContainer.searchService, + this.serviceContainer.organizationUserService, + this.serviceContainer.apiService, + this.serviceContainer.eventCollectionService, ); const response = await command.run(object, cmd); @@ -177,18 +172,18 @@ export class VaultProgram extends Program { await this.exitIfLocked(); const command = new GetCommand( - this.main.cipherService, - this.main.folderService, - this.main.collectionService, - this.main.totpService, - this.main.auditService, - this.main.cryptoService, - this.main.stateService, - this.main.searchService, - this.main.apiService, - this.main.organizationService, - this.main.eventCollectionService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.totpService, + this.serviceContainer.auditService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.searchService, + this.serviceContainer.apiService, + this.serviceContainer.organizationService, + this.serviceContainer.eventCollectionService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await command.run(object, id, cmd); this.processResponse(response); @@ -225,12 +220,12 @@ export class VaultProgram extends Program { await this.exitIfLocked(); const command = new CreateCommand( - this.main.cipherService, - this.main.folderService, - this.main.cryptoService, - this.main.apiService, - this.main.folderApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.cryptoService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await command.run(object, encodedJson, cmd); this.processResponse(response); @@ -271,11 +266,11 @@ export class VaultProgram extends Program { await this.exitIfLocked(); const command = new EditCommand( - this.main.cipherService, - this.main.folderService, - this.main.cryptoService, - this.main.apiService, - this.main.folderApiService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.cryptoService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, ); const response = await command.run(object, id, encodedJson, cmd); this.processResponse(response); @@ -312,11 +307,11 @@ export class VaultProgram extends Program { await this.exitIfLocked(); const command = new DeleteCommand( - this.main.cipherService, - this.main.folderService, - this.main.apiService, - this.main.folderApiService, - this.main.billingAccountProfileStateService, + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + this.serviceContainer.billingAccountProfileStateService, ); const response = await command.run(object, id, cmd); this.processResponse(response); @@ -341,7 +336,7 @@ export class VaultProgram extends Program { } await this.exitIfLocked(); - const command = new RestoreCommand(this.main.cipherService); + const command = new RestoreCommand(this.serviceContainer.cipherService); const response = await command.run(object, id); this.processResponse(response); }); @@ -379,7 +374,7 @@ export class VaultProgram extends Program { }) .action(async (id, organizationId, encodedJson, cmd) => { await this.exitIfLocked(); - const command = new ShareCommand(this.main.cipherService); + const command = new ShareCommand(this.serviceContainer.cipherService); const response = await command.run(id, organizationId, encodedJson); this.processResponse(response); }); @@ -408,9 +403,9 @@ export class VaultProgram extends Program { await this.exitIfLocked(); const command = new ConfirmCommand( - this.main.apiService, - this.main.cryptoService, - this.main.organizationUserService, + this.serviceContainer.apiService, + this.serviceContainer.cryptoService, + this.serviceContainer.organizationUserService, ); const response = await command.run(object, id, cmd); this.processResponse(response); @@ -437,9 +432,9 @@ export class VaultProgram extends Program { .action(async (format, filepath, options) => { await this.exitIfLocked(); const command = new ImportCommand( - this.main.importService, - this.main.organizationService, - this.main.syncService, + this.serviceContainer.importService, + this.serviceContainer.organizationService, + this.serviceContainer.syncService, ); const response = await command.run(format, filepath, options); this.processResponse(response); @@ -484,9 +479,9 @@ export class VaultProgram extends Program { .action(async (options) => { await this.exitIfLocked(); const command = new ExportCommand( - this.main.exportService, - this.main.policyService, - this.main.eventCollectionService, + this.serviceContainer.exportService, + this.serviceContainer.policyService, + this.serviceContainer.eventCollectionService, ); const response = await command.run(options); this.processResponse(response); diff --git a/bitwarden_license/bit-cli/.eslintrc.json b/bitwarden_license/bit-cli/.eslintrc.json new file mode 100644 index 0000000000..10d2238837 --- /dev/null +++ b/bitwarden_license/bit-cli/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "node": true + } +} diff --git a/bitwarden_license/bit-cli/jest.config.js b/bitwarden_license/bit-cli/jest.config.js new file mode 100644 index 0000000000..92be98cc56 --- /dev/null +++ b/bitwarden_license/bit-cli/jest.config.js @@ -0,0 +1,16 @@ +const { pathsToModuleNameMapper } = require("ts-jest"); + +const { compilerOptions } = require("./tsconfig"); + +const sharedConfig = require("../../libs/shared/jest.config.ts"); + +/** @type {import('jest').Config} */ +module.exports = { + ...sharedConfig, + preset: "ts-jest", + testEnvironment: "node", + setupFilesAfterEnv: ["/../../apps/cli/test.setup.ts"], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/", + }), +}; diff --git a/bitwarden_license/bit-cli/src/bw.spec.ts b/bitwarden_license/bit-cli/src/bw.spec.ts new file mode 100644 index 0000000000..317f9733b1 --- /dev/null +++ b/bitwarden_license/bit-cli/src/bw.spec.ts @@ -0,0 +1,5 @@ +describe("Jest", () => { + it("is set up", () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/bitwarden_license/bit-cli/src/bw.ts b/bitwarden_license/bit-cli/src/bw.ts new file mode 100644 index 0000000000..d6ebcaf041 --- /dev/null +++ b/bitwarden_license/bit-cli/src/bw.ts @@ -0,0 +1,20 @@ +import { program } from "commander"; + +import { registerOssPrograms } from "@bitwarden/cli/register-oss-programs"; + +import { registerBitPrograms } from "./register-bit-programs"; +import { ServiceContainer } from "./service-container"; + +async function main() { + const serviceContainer = new ServiceContainer(); + await serviceContainer.init(); + + await registerOssPrograms(serviceContainer); + await registerBitPrograms(serviceContainer); + + program.parse(process.argv); +} + +// Node does not support top-level await statements until ES2022, esnext, etc which we don't use yet +// eslint-disable-next-line @typescript-eslint/no-floating-promises +main(); diff --git a/bitwarden_license/bit-cli/src/register-bit-programs.ts b/bitwarden_license/bit-cli/src/register-bit-programs.ts new file mode 100644 index 0000000000..859574644a --- /dev/null +++ b/bitwarden_license/bit-cli/src/register-bit-programs.ts @@ -0,0 +1,10 @@ +import { ServiceContainer } from "./service-container"; + +/** + * All Bitwarden-licensed programs should be registered here. + * @example + * const myProgram = new myProgram(serviceContainer); + * myProgram.register(); + * @param serviceContainer A class that instantiates services and makes them available for dependency injection + */ +export async function registerBitPrograms(serviceContainer: ServiceContainer) {} diff --git a/bitwarden_license/bit-cli/src/service-container.spec.ts b/bitwarden_license/bit-cli/src/service-container.spec.ts new file mode 100644 index 0000000000..26258c367c --- /dev/null +++ b/bitwarden_license/bit-cli/src/service-container.spec.ts @@ -0,0 +1,7 @@ +import { ServiceContainer } from "./service-container"; + +describe("ServiceContainer", () => { + it("instantiates", async () => { + expect(() => new ServiceContainer()).not.toThrow(); + }); +}); diff --git a/bitwarden_license/bit-cli/src/service-container.ts b/bitwarden_license/bit-cli/src/service-container.ts new file mode 100644 index 0000000000..369d54113d --- /dev/null +++ b/bitwarden_license/bit-cli/src/service-container.ts @@ -0,0 +1,7 @@ +import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container"; + +/** + * Instantiates services and makes them available for dependency injection. + * Any Bitwarden-licensed services should be registered here. + */ +export class ServiceContainer extends OssServiceContainer {} diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json new file mode 100644 index 0000000000..1989aa08f9 --- /dev/null +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "pretty": true, + "moduleResolution": "node", + "target": "ES2016", + "module": "es6", + "noImplicitAny": true, + "allowSyntheticDefaultImports": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowJs": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@bitwarden/cli/*": ["../../apps/cli/src/*"], + "@bitwarden/common/spec": ["../../libs/common/spec"], + "@bitwarden/auth/common": ["../../libs/auth/src/common"], + "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], + "@bitwarden/common/*": ["../../libs/common/src/*"], + "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/vault-export-core": [ + "../../libs/tools/export/vault-export/vault-export-core/src" + ], + "@bitwarden/node/*": ["../../libs/node/src/*"] + } + }, + "include": ["src", "src/**/*.spec.ts"] +} diff --git a/bitwarden_license/bit-cli/tsconfig.spec.json b/bitwarden_license/bit-cli/tsconfig.spec.json new file mode 100644 index 0000000000..ef128c808c --- /dev/null +++ b/bitwarden_license/bit-cli/tsconfig.spec.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "files": ["../../apps/cli/test.setup.ts"] +} diff --git a/bitwarden_license/bit-cli/webpack.config.js b/bitwarden_license/bit-cli/webpack.config.js new file mode 100644 index 0000000000..3e991f7971 --- /dev/null +++ b/bitwarden_license/bit-cli/webpack.config.js @@ -0,0 +1,12 @@ +const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); + +// Re-use the OSS CLI webpack config +const webpackConfig = require("../../apps/cli/webpack.config"); + +// Update paths to use the bit-cli entrypoint and tsconfig +webpackConfig.entry = { bw: "../../bitwarden_license/bit-cli/src/bw.ts" }; +webpackConfig.resolve.plugins = [ + new TsconfigPathsPlugin({ configFile: "../../bitwarden_license/bit-cli/tsconfig.json" }), +]; + +module.exports = webpackConfig; diff --git a/clients.code-workspace b/clients.code-workspace index 608f57096b..c7075c04ea 100644 --- a/clients.code-workspace +++ b/clients.code-workspace @@ -16,6 +16,10 @@ "name": "cli", "path": "apps/cli", }, + { + "name": "cli (bit)", + "path": "bitwarden_license/bit-cli", + }, { "name": "desktop", "path": "apps/desktop", diff --git a/jest.config.js b/jest.config.js index a5876f3dc0..1ff25c3beb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,6 +21,7 @@ module.exports = { "/apps/desktop/jest.config.js", "/apps/web/jest.config.js", "/bitwarden_license/bit-web/jest.config.js", + "/bitwarden_license/bit-cli/jest.config.js", "/libs/admin-console/jest.config.js", "/libs/angular/jest.config.js", From 426bacfd67dc47957ff1365303a7a66b61f26797 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 15 May 2024 10:45:40 -0400 Subject: [PATCH 2/4] Ps/pm-8003/handle-dekstop-invalidated-message-encryption (#9181) * Do not initialize symmetric crypto keys with null * Require new message on invalid native message encryption Handling of this error is to require the user to retry, so the promise needs to resolve. --- .../browser/src/background/nativeMessaging.background.ts | 5 +++++ .../src/platform/services/electron-crypto.service.ts | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 5ba2b6b34a..51ab301fd1 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -167,6 +167,11 @@ export class NativeMessagingBackground { cancelButtonText: null, type: "danger", }); + + if (this.resolver) { + this.resolver(message); + } + break; case "verifyFingerprint": { if (this.sharedSecret == null) { diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 7397990cb4..1bbd02ab8b 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -92,7 +92,9 @@ export class ElectronCryptoService extends CryptoService { if (keySuffix === KeySuffixOptions.Biometric) { await this.migrateBiometricKeyIfNeeded(userId); const userKey = await this.stateService.getUserKeyBiometric({ userId: userId }); - return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey; + return userKey == null + ? null + : (new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey); } return await super.getKeyFromStorage(keySuffix, userId); } @@ -169,7 +171,9 @@ export class ElectronCryptoService extends CryptoService { // decrypt const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; - const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey({ + userId: userId, + }); const encUserKey = encUserKeyPrim != null ? new EncString(encUserKeyPrim) @@ -180,6 +184,7 @@ export class ElectronCryptoService extends CryptoService { const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( masterKey, encUserKey, + userId, ); // migrate await this.storeBiometricKey(userKey, userId); From 25f55e13683e6fd198402f26afd25f10ed44c86f Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 15 May 2024 12:11:06 -0400 Subject: [PATCH 3/4] [PM-7978] Create ForegroundSyncService For Delegating `fullSync` Calls (#9192) * Create ForegroundSyncService For Delegating `fullSync` calls to the background * Relax `isExternalMessage` to Allow For Typed Payload * Null Coalesce The `startListening` Method * Filter To Only External Messages --- .../browser/src/background/main.background.ts | 76 ++++-- .../platform/sync/foreground-sync.service.ts | 79 ++++++ .../platform/sync/sync-service.listener.ts | 25 ++ libs/common/src/platform/messaging/helpers.ts | 4 +- .../src/platform/sync/core-sync.service.ts | 230 +++++++++++++++++ libs/common/src/platform/sync/internal.ts | 1 + .../src/vault/services/sync/sync.service.ts | 243 +++--------------- 7 files changed, 422 insertions(+), 236 deletions(-) create mode 100644 apps/browser/src/platform/sync/foreground-sync.service.ts create mode 100644 apps/browser/src/platform/sync/sync-service.listener.ts create mode 100644 libs/common/src/platform/sync/core-sync.service.ts create mode 100644 libs/common/src/platform/sync/internal.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c3722f2a48..d5e8fe1da7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -230,6 +230,8 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; +import { ForegroundSyncService } from "../platform/sync/foreground-sync.service"; +import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; @@ -339,6 +341,7 @@ export default class MainBackground { scriptInjectorService: BrowserScriptInjectorService; kdfConfigService: kdfConfigServiceAbstraction; offscreenDocumentService: OffscreenDocumentService; + syncServiceListener: SyncServiceListener; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -792,32 +795,52 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); - this.syncService = new SyncService( - this.masterPasswordService, - this.accountService, - this.apiService, - this.domainSettingsService, - this.folderService, - this.cipherService, - this.cryptoService, - this.collectionService, - this.messagingService, - this.policyService, - this.sendService, - this.logService, - this.keyConnectorService, - this.stateService, - this.providerService, - this.folderApiService, - this.organizationService, - this.sendApiService, - this.userDecryptionOptionsService, - this.avatarService, - logoutCallback, - this.billingAccountProfileStateService, - this.tokenService, - this.authService, - ); + if (this.popupOnlyContext) { + this.syncService = new ForegroundSyncService( + this.stateService, + this.folderService, + this.folderApiService, + this.messagingService, + this.logService, + this.cipherService, + this.collectionService, + this.apiService, + this.accountService, + this.authService, + this.sendService, + this.sendApiService, + messageListener, + ); + } else { + this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, + this.apiService, + this.domainSettingsService, + this.folderService, + this.cipherService, + this.cryptoService, + this.collectionService, + this.messagingService, + this.policyService, + this.sendService, + this.logService, + this.keyConnectorService, + this.stateService, + this.providerService, + this.folderApiService, + this.organizationService, + this.sendApiService, + this.userDecryptionOptionsService, + this.avatarService, + logoutCallback, + this.billingAccountProfileStateService, + this.tokenService, + this.authService, + ); + + this.syncServiceListener = new SyncServiceListener(this.syncService, messageListener); + } this.eventUploadService = new EventUploadService( this.apiService, this.stateProvider, @@ -1141,6 +1164,7 @@ export default class MainBackground { this.contextMenusBackground?.init(); await this.idleBackground.init(); this.webRequestBackground?.startListening(); + this.syncServiceListener?.startListening(); return new Promise((resolve) => { setTimeout(async () => { diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts new file mode 100644 index 0000000000..3c14431672 --- /dev/null +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -0,0 +1,79 @@ +import { firstValueFrom, timeout } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { + CommandDefinition, + MessageListener, + MessageSender, +} from "@bitwarden/common/platform/messaging"; +import { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; + +const SYNC_COMPLETED = new CommandDefinition<{ successfully: boolean }>("syncCompleted"); +export const DO_FULL_SYNC = new CommandDefinition<{ + forceSync: boolean; + allowThrowOnError: boolean; +}>("doFullSync"); + +export class ForegroundSyncService extends CoreSyncService { + constructor( + stateService: StateService, + folderService: InternalFolderService, + folderApiService: FolderApiServiceAbstraction, + messageSender: MessageSender, + logService: LogService, + cipherService: CipherService, + collectionService: CollectionService, + apiService: ApiService, + accountService: AccountService, + authService: AuthService, + sendService: InternalSendService, + sendApiService: SendApiService, + private readonly messageListener: MessageListener, + ) { + super( + stateService, + folderService, + folderApiService, + messageSender, + logService, + cipherService, + collectionService, + apiService, + accountService, + authService, + sendService, + sendApiService, + ); + } + + async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise { + this.syncInProgress = true; + try { + const syncCompletedPromise = firstValueFrom( + this.messageListener.messages$(SYNC_COMPLETED).pipe( + timeout({ + first: 10_000, + with: () => { + throw new Error("Timeout while doing a fullSync call."); + }, + }), + ), + ); + this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError }); + const result = await syncCompletedPromise; + return result.successfully; + } finally { + this.syncInProgress = false; + } + } +} diff --git a/apps/browser/src/platform/sync/sync-service.listener.ts b/apps/browser/src/platform/sync/sync-service.listener.ts new file mode 100644 index 0000000000..b9e18accac --- /dev/null +++ b/apps/browser/src/platform/sync/sync-service.listener.ts @@ -0,0 +1,25 @@ +import { Subscription, concatMap, filter } from "rxjs"; + +import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; + +import { DO_FULL_SYNC } from "./foreground-sync.service"; + +export class SyncServiceListener { + constructor( + private readonly syncService: SyncService, + private readonly messageListener: MessageListener, + ) {} + + startListening(): Subscription { + return this.messageListener + .messages$(DO_FULL_SYNC) + .pipe( + filter((message) => isExternalMessage(message)), + concatMap(async ({ forceSync, allowThrowOnError }) => { + await this.syncService.fullSync(forceSync, allowThrowOnError); + }), + ) + .subscribe(); + } +} diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/common/src/platform/messaging/helpers.ts index bf119432e0..ba772e517b 100644 --- a/libs/common/src/platform/messaging/helpers.ts +++ b/libs/common/src/platform/messaging/helpers.ts @@ -12,8 +12,8 @@ export const getCommand = (commandDefinition: CommandDefinition | string export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); -export const isExternalMessage = (message: Message) => { - return (message as Record)?.[EXTERNAL_SOURCE_TAG] === true; +export const isExternalMessage = (message: Record) => { + return message?.[EXTERNAL_SOURCE_TAG] === true; }; export const tagAsExternal: MonoTypeOperatorFunction> = map( diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts new file mode 100644 index 0000000000..52c1a51cb8 --- /dev/null +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -0,0 +1,230 @@ +import { firstValueFrom, map, of, switchMap } from "rxjs"; + +import { ApiService } from "../../abstractions/api.service"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { + SyncCipherNotification, + SyncFolderNotification, + SyncSendNotification, +} from "../../models/response/notification.response"; +import { SendData } from "../../tools/send/models/data/send.data"; +import { SendApiService } from "../../tools/send/services/send-api.service.abstraction"; +import { InternalSendService } from "../../tools/send/services/send.service.abstraction"; +import { CipherService } from "../../vault/abstractions/cipher.service"; +import { CollectionService } from "../../vault/abstractions/collection.service"; +import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction"; +import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; +import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; +import { CipherData } from "../../vault/models/data/cipher.data"; +import { FolderData } from "../../vault/models/data/folder.data"; +import { LogService } from "../abstractions/log.service"; +import { StateService } from "../abstractions/state.service"; +import { MessageSender } from "../messaging"; + +/** + * Core SyncService Logic EXCEPT for fullSync so that implementations can differ. + */ +export abstract class CoreSyncService implements SyncService { + syncInProgress = false; + + constructor( + protected readonly stateService: StateService, + protected readonly folderService: InternalFolderService, + protected readonly folderApiService: FolderApiServiceAbstraction, + protected readonly messageSender: MessageSender, + protected readonly logService: LogService, + protected readonly cipherService: CipherService, + protected readonly collectionService: CollectionService, + protected readonly apiService: ApiService, + protected readonly accountService: AccountService, + protected readonly authService: AuthService, + protected readonly sendService: InternalSendService, + protected readonly sendApiService: SendApiService, + ) {} + + abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise; + + async getLastSync(): Promise { + if ((await this.stateService.getUserId()) == null) { + return null; + } + + const lastSync = await this.stateService.getLastSync(); + if (lastSync) { + return new Date(lastSync); + } + + return null; + } + + async setLastSync(date: Date, userId?: string): Promise { + await this.stateService.setLastSync(date.toJSON(), { userId: userId }); + } + + async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + const localFolder = await this.folderService.get(notification.id); + if ( + (!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate) + ) { + const remoteFolder = await this.folderApiService.get(notification.id); + if (remoteFolder != null) { + await this.folderService.upsert(new FolderData(remoteFolder)); + this.messageSender.send("syncedUpsertedFolder", { folderId: notification.id }); + return this.syncCompleted(true); + } + } + } catch (e) { + this.logService.error(e); + } + } + return this.syncCompleted(false); + } + + async syncDeleteFolder(notification: SyncFolderNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.folderService.delete(notification.id); + this.messageSender.send("syncedDeletedFolder", { folderId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + try { + let shouldUpdate = true; + const localCipher = await this.cipherService.get(notification.id); + if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { + shouldUpdate = false; + } + + let checkCollections = false; + if (shouldUpdate) { + if (isEdit) { + shouldUpdate = localCipher != null; + checkCollections = true; + } else { + if (notification.collectionIds == null || notification.organizationId == null) { + shouldUpdate = localCipher == null; + } else { + shouldUpdate = false; + checkCollections = true; + } + } + } + + if ( + !shouldUpdate && + checkCollections && + notification.organizationId != null && + notification.collectionIds != null && + notification.collectionIds.length > 0 + ) { + const collections = await this.collectionService.getAll(); + if (collections != null) { + for (let i = 0; i < collections.length; i++) { + if (notification.collectionIds.indexOf(collections[i].id) > -1) { + shouldUpdate = true; + break; + } + } + } + } + + if (shouldUpdate) { + const remoteCipher = await this.apiService.getFullCipherDetails(notification.id); + if (remoteCipher != null) { + await this.cipherService.upsert(new CipherData(remoteCipher)); + this.messageSender.send("syncedUpsertedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + } + } catch (e) { + if (e != null && e.statusCode === 404 && isEdit) { + await this.cipherService.delete(notification.id); + this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + } + } + return this.syncCompleted(false); + } + + async syncDeleteCipher(notification: SyncCipherNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.cipherService.delete(notification.id); + this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id }); + return this.syncCompleted(true); + } + return this.syncCompleted(false); + } + + async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { + this.syncStarted(); + const [activeUserId, status] = await firstValueFrom( + this.accountService.activeAccount$.pipe( + switchMap((a) => { + if (a == null) { + of([null, AuthenticationStatus.LoggedOut]); + } + return this.authService.authStatusFor$(a.id).pipe(map((s) => [a.id, s])); + }), + ), + ); + // Process only notifications for currently active user when user is not logged out + // TODO: once send service allows data manipulation of non-active users, this should process any received notification + if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) { + try { + const localSend = await firstValueFrom(this.sendService.get$(notification.id)); + if ( + (!isEdit && localSend == null) || + (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) + ) { + const remoteSend = await this.sendApiService.getSend(notification.id); + if (remoteSend != null) { + await this.sendService.upsert(new SendData(remoteSend)); + this.messageSender.send("syncedUpsertedSend", { sendId: notification.id }); + return this.syncCompleted(true); + } + } + } catch (e) { + this.logService.error(e); + } + } + return this.syncCompleted(false); + } + + async syncDeleteSend(notification: SyncSendNotification): Promise { + this.syncStarted(); + if (await this.stateService.getIsAuthenticated()) { + await this.sendService.delete(notification.id); + this.messageSender.send("syncedDeletedSend", { sendId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + // Helpers + + protected syncStarted() { + this.syncInProgress = true; + this.messageSender.send("syncStarted"); + } + + protected syncCompleted(successfully: boolean): boolean { + this.syncInProgress = false; + this.messageSender.send("syncCompleted", { successfully: successfully }); + return successfully; + } +} diff --git a/libs/common/src/platform/sync/internal.ts b/libs/common/src/platform/sync/internal.ts new file mode 100644 index 0000000000..f515e90a07 --- /dev/null +++ b/libs/common/src/platform/sync/internal.ts @@ -0,0 +1 @@ +export { CoreSyncService } from "./core-sync.service"; diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index b591a61e86..172891b08d 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, of, switchMap } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -17,22 +17,17 @@ import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; import { TokenService } from "../../../auth/abstractions/token.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; import { DomainsResponse } from "../../../models/response/domains.response"; -import { - SyncCipherNotification, - SyncFolderNotification, - SyncSendNotification, -} from "../../../models/response/notification.response"; import { ProfileResponse } from "../../../models/response/profile.response"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { LogService } from "../../../platform/abstractions/log.service"; -import { MessagingService } from "../../../platform/abstractions/messaging.service"; import { StateService } from "../../../platform/abstractions/state.service"; +import { MessageSender } from "../../../platform/messaging"; import { sequentialize } from "../../../platform/misc/sequentialize"; +import { CoreSyncService } from "../../../platform/sync/core-sync.service"; import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; @@ -40,7 +35,6 @@ import { InternalSendService } from "../../../tools/send/services/send.service.a import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; -import { SyncService as SyncServiceAbstraction } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { CipherData } from "../../../vault/models/data/cipher.data"; import { FolderData } from "../../../vault/models/data/folder.data"; import { CipherResponse } from "../../../vault/models/response/cipher.response"; @@ -49,55 +43,51 @@ import { CollectionService } from "../../abstractions/collection.service"; import { CollectionData } from "../../models/data/collection.data"; import { CollectionDetailsResponse } from "../../models/response/collection.response"; -export class SyncService implements SyncServiceAbstraction { - syncInProgress = false; - +export class SyncService extends CoreSyncService { constructor( private masterPasswordService: InternalMasterPasswordServiceAbstraction, - private accountService: AccountService, - private apiService: ApiService, + accountService: AccountService, + apiService: ApiService, private domainSettingsService: DomainSettingsService, - private folderService: InternalFolderService, - private cipherService: CipherService, + folderService: InternalFolderService, + cipherService: CipherService, private cryptoService: CryptoService, - private collectionService: CollectionService, - private messagingService: MessagingService, + collectionService: CollectionService, + messageSender: MessageSender, private policyService: InternalPolicyService, - private sendService: InternalSendService, - private logService: LogService, + sendService: InternalSendService, + logService: LogService, private keyConnectorService: KeyConnectorService, - private stateService: StateService, + stateService: StateService, private providerService: ProviderService, - private folderApiService: FolderApiServiceAbstraction, + folderApiService: FolderApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, - private sendApiService: SendApiService, + sendApiService: SendApiService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, private tokenService: TokenService, - private authService: AuthService, - ) {} - - async getLastSync(): Promise { - if ((await this.stateService.getUserId()) == null) { - return null; - } - - const lastSync = await this.stateService.getLastSync(); - if (lastSync) { - return new Date(lastSync); - } - - return null; - } - - async setLastSync(date: Date, userId?: string): Promise { - await this.stateService.setLastSync(date.toJSON(), { userId: userId }); + authService: AuthService, + ) { + super( + stateService, + folderService, + folderApiService, + messageSender, + logService, + cipherService, + collectionService, + apiService, + accountService, + authService, + sendService, + sendApiService, + ); } @sequentialize(() => "fullSync") - async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { + override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { this.syncStarted(); const isAuthenticated = await this.stateService.getIsAuthenticated(); if (!isAuthenticated) { @@ -110,6 +100,7 @@ export class SyncService implements SyncServiceAbstraction { needsSync = await this.needsSyncing(forceSync); } catch (e) { if (allowThrowOnError) { + this.syncCompleted(false); throw e; } } @@ -135,6 +126,7 @@ export class SyncService implements SyncServiceAbstraction { return this.syncCompleted(true); } catch (e) { if (allowThrowOnError) { + this.syncCompleted(false); throw e; } else { return this.syncCompleted(false); @@ -142,171 +134,6 @@ export class SyncService implements SyncServiceAbstraction { } } - async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - try { - const localFolder = await this.folderService.get(notification.id); - if ( - (!isEdit && localFolder == null) || - (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate) - ) { - const remoteFolder = await this.folderApiService.get(notification.id); - if (remoteFolder != null) { - await this.folderService.upsert(new FolderData(remoteFolder)); - this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - this.logService.error(e); - } - } - return this.syncCompleted(false); - } - - async syncDeleteFolder(notification: SyncFolderNotification): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.folderService.delete(notification.id); - this.messagingService.send("syncedDeletedFolder", { folderId: notification.id }); - this.syncCompleted(true); - return true; - } - return this.syncCompleted(false); - } - - async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - try { - let shouldUpdate = true; - const localCipher = await this.cipherService.get(notification.id); - if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { - shouldUpdate = false; - } - - let checkCollections = false; - if (shouldUpdate) { - if (isEdit) { - shouldUpdate = localCipher != null; - checkCollections = true; - } else { - if (notification.collectionIds == null || notification.organizationId == null) { - shouldUpdate = localCipher == null; - } else { - shouldUpdate = false; - checkCollections = true; - } - } - } - - if ( - !shouldUpdate && - checkCollections && - notification.organizationId != null && - notification.collectionIds != null && - notification.collectionIds.length > 0 - ) { - const collections = await this.collectionService.getAll(); - if (collections != null) { - for (let i = 0; i < collections.length; i++) { - if (notification.collectionIds.indexOf(collections[i].id) > -1) { - shouldUpdate = true; - break; - } - } - } - } - - if (shouldUpdate) { - const remoteCipher = await this.apiService.getFullCipherDetails(notification.id); - if (remoteCipher != null) { - await this.cipherService.upsert(new CipherData(remoteCipher)); - this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - if (e != null && e.statusCode === 404 && isEdit) { - await this.cipherService.delete(notification.id); - this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id }); - return this.syncCompleted(true); - } - } - } - return this.syncCompleted(false); - } - - async syncDeleteCipher(notification: SyncCipherNotification): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.cipherService.delete(notification.id); - this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id }); - return this.syncCompleted(true); - } - return this.syncCompleted(false); - } - - async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { - this.syncStarted(); - const [activeUserId, status] = await firstValueFrom( - this.accountService.activeAccount$.pipe( - switchMap((a) => { - if (a == null) { - of([null, AuthenticationStatus.LoggedOut]); - } - return this.authService.authStatusFor$(a.id).pipe(map((s) => [a.id, s])); - }), - ), - ); - // Process only notifications for currently active user when user is not logged out - // TODO: once send service allows data manipulation of non-active users, this should process any received notification - if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) { - try { - const localSend = await firstValueFrom(this.sendService.get$(notification.id)); - if ( - (!isEdit && localSend == null) || - (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) - ) { - const remoteSend = await this.sendApiService.getSend(notification.id); - if (remoteSend != null) { - await this.sendService.upsert(new SendData(remoteSend)); - this.messagingService.send("syncedUpsertedSend", { sendId: notification.id }); - return this.syncCompleted(true); - } - } - } catch (e) { - this.logService.error(e); - } - } - return this.syncCompleted(false); - } - - async syncDeleteSend(notification: SyncSendNotification): Promise { - this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.sendService.delete(notification.id); - this.messagingService.send("syncedDeletedSend", { sendId: notification.id }); - this.syncCompleted(true); - return true; - } - return this.syncCompleted(false); - } - - // Helpers - - private syncStarted() { - this.syncInProgress = true; - this.messagingService.send("syncStarted"); - } - - private syncCompleted(successfully: boolean): boolean { - this.syncInProgress = false; - this.messagingService.send("syncCompleted", { successfully: successfully }); - return successfully; - } - private async needsSyncing(forceSync: boolean) { if (forceSync) { return true; @@ -365,7 +192,7 @@ export class SyncService implements SyncServiceAbstraction { if (await this.keyConnectorService.userNeedsMigration()) { await this.keyConnectorService.setConvertAccountRequired(true); - this.messagingService.send("convertAccountToKeyConnector"); + this.messageSender.send("convertAccountToKeyConnector"); } else { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises From db2f60b6849833e9a4586d025074ed2064badf16 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 15 May 2024 14:27:15 -0400 Subject: [PATCH 4/4] [AC-2483] Added new Add Access UI to the collection dialog for AC (#9090) * added new Add Access UI to the collection dialog for AC --- .../collection-dialog.component.html | 12 +++++++++++- .../collection-dialog.component.ts | 15 +++++++++++++++ .../src/app/vault/org-vault/vault.component.ts | 1 + apps/web/src/locales/en/messages.json | 6 ++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index b64ce5bb00..6adf6bcf8f 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -69,6 +69,13 @@ {{ "readOnlyCollectionAccess" | i18n }} + + {{ "grantAddAccessCollectionWarning" | i18n }} + {{ "grantCollectionAccess" | i18n }} {{ "grantCollectionAccessMembersOnly" | i18n @@ -84,7 +91,10 @@
{{ "managePermissionRequired" | i18n }}
diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index f386665186..8040cf13cd 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -59,6 +59,7 @@ export interface CollectionDialogParams { */ limitNestedCollections?: boolean; readonly?: boolean; + isAddAccessCollection?: boolean; } export interface CollectionDialogResult { @@ -100,6 +101,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { }); protected PermissionMode = PermissionMode; protected showDeleteButton = false; + protected showAddAccessWarning = false; constructor( @Inject(DIALOG_DATA) private params: CollectionDialogParams, @@ -251,6 +253,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.handleFormGroupReadonly(this.dialogReadonly); this.loading = false; + this.showAddAccessWarning = this.handleAddAccessWarning(flexibleCollectionsV1); }, ); } @@ -362,6 +365,18 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.destroy$.complete(); } + private handleAddAccessWarning(flexibleCollectionsV1: boolean): boolean { + if ( + flexibleCollectionsV1 && + !this.organization?.allowAdminAccessToAllCollectionItems && + this.params.isAddAccessCollection + ) { + return true; + } + + return false; + } + private handleFormGroupReadonly(readonly: boolean) { if (readonly) { this.formGroup.controls.name.disable(); diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 95dd9cab16..c8549e0b88 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -1222,6 +1222,7 @@ export class VaultComponent implements OnInit, OnDestroy { organizationId: this.organization?.id, initialTab: tab, readonly: readonly, + isAddAccessCollection: c.addAccess, limitNestedCollections: !this.organization.canEditAnyCollection( this.flexibleCollectionsV1Enabled, ), diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9e5ad73a03..3c96dd5df7 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7635,6 +7635,12 @@ "readOnlyCollectionAccess": { "message": "You do not have access to manage this collection." }, + "grantAddAccessCollectionWarningTitle": { + "message": "Missing Can Manage Permissions" + }, + "grantAddAccessCollectionWarning": { + "message": "Grant Can manage permissions to allow full collection management including deletion of collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." },