From 09ff12fc028545d149a74f4c432ed729d511d7e7 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Mon, 6 May 2024 11:15:33 -0400 Subject: [PATCH 01/18] [PM-7919] Add more tde logging (#9035) * adds additional logging to TDE service * remove base catch swallowing errors * add dependency to cli * fix comment --- .../device-trust-service.factory.ts | 2 + .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + ...base-login-decryption-options.component.ts | 3 +- .../src/services/jslib-services.module.ts | 1 + .../login-strategies/sso-login.strategy.ts | 2 +- .../device-trust.service.implementation.ts | 49 ++++++++++++------- .../services/device-trust.service.spec.ts | 3 ++ 8 files changed, 40 insertions(+), 22 deletions(-) diff --git a/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts index 106bcbcf72..42a8232c3e 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts @@ -34,6 +34,7 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "../../../platform/background/service-factories/key-generation-service.factory"; +import { logServiceFactory } from "../../../platform/background/service-factories/log-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -88,6 +89,7 @@ export function deviceTrustServiceFactory( await stateProviderFactory(cache, opts), await secureStorageServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5ce7b0a408..5cd4113bae 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -631,6 +631,7 @@ export default class MainBackground { this.stateProvider, this.secureStorageService, this.userDecryptionOptionsService, + this.logService, ); this.devicesService = new DevicesServiceImplementation(this.devicesApiService); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 114765a789..a038f3aa90 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -486,6 +486,7 @@ export class Main { this.stateProvider, this.secureStorageService, this.userDecryptionOptionsService, + this.logService, ); this.authRequestService = new AuthRequestService( diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 0e58c03a54..b5cc50d847 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -272,6 +272,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { // this.loading to support clients without async-actions-support this.loading = true; + // errors must be caught in child components to prevent navigation try { const { publicKey, privateKey } = await this.cryptoService.initAccount(); const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); @@ -288,8 +289,6 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { if (this.rememberDeviceForm.value.rememberDevice) { await this.deviceTrustService.trustDevice(this.activeAccountId); } - } catch (error) { - this.validationService.showError(error); } finally { this.loading = false; } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 80024bb0b6..fdbf5e9ecb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -967,6 +967,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, SECURE_STORAGE, UserDecryptionOptionsServiceAbstraction, + LogService, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index f328547772..c37ef683ed 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -241,7 +241,7 @@ export class SsoLoginStrategy extends LoginStrategy { if (userDecryptionOptions?.trustedDeviceOption) { await this.trySetUserKeyWithApprovedAdminRequestIfExists(userId); - const hasUserKey = await this.cryptoService.hasUserKey(); + const hasUserKey = await this.cryptoService.hasUserKey(userId); // Only try to set user key with device key if admin approval request was not successful if (!hasUserKey) { diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index ccf87acaf8..dd98ce2b44 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -8,6 +8,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; @@ -61,6 +62,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { private stateProvider: StateProvider, private secureStorageService: AbstractStorageService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + private logService: LogService, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( map((options) => options?.trustedDeviceOption != null ?? false), @@ -110,7 +112,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } // Attempt to get user key - const userKey: UserKey = await this.cryptoService.getUserKey(); + const userKey: UserKey = await this.cryptoService.getUserKey(userId); // If user key is not found, throw error if (!userKey) { @@ -223,19 +225,23 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { throw new Error("UserId is required. Cannot get device key."); } - if (this.platformSupportsSecureStorage) { - const deviceKeyB64 = await this.secureStorageService.get< - ReturnType - >(`${userId}${this.deviceKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); + try { + if (this.platformSupportsSecureStorage) { + const deviceKeyB64 = await this.secureStorageService.get< + ReturnType + >(`${userId}${this.deviceKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); - const deviceKey = SymmetricCryptoKey.fromJSON(deviceKeyB64) as DeviceKey; + const deviceKey = SymmetricCryptoKey.fromJSON(deviceKeyB64) as DeviceKey; + + return deviceKey; + } + + const deviceKey = await firstValueFrom(this.stateProvider.getUserState$(DEVICE_KEY, userId)); return deviceKey; + } catch (e) { + this.logService.error("Failed to get device key", e); } - - const deviceKey = await firstValueFrom(this.stateProvider.getUserState$(DEVICE_KEY, userId)); - - return deviceKey; } private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise { @@ -243,16 +249,20 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { throw new Error("UserId is required. Cannot set device key."); } - if (this.platformSupportsSecureStorage) { - await this.secureStorageService.save( - `${userId}${this.deviceKeySecureStorageKey}`, - deviceKey, - this.getSecureStorageOptions(userId), - ); - return; - } + try { + if (this.platformSupportsSecureStorage) { + await this.secureStorageService.save( + `${userId}${this.deviceKeySecureStorageKey}`, + deviceKey, + this.getSecureStorageOptions(userId), + ); + return; + } - await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId); + await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId); + } catch (e) { + this.logService.error("Failed to set device key", e); + } } private async makeDeviceKey(): Promise { @@ -293,6 +303,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { return new SymmetricCryptoKey(userKey) as UserKey; } catch (e) { // If either decryption effort fails, we want to remove the device key + this.logService.error("Failed to decrypt using device key. Removing device key."); await this.setDeviceKey(userId, null); return null; diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 12b8cf2eaa..f61bce563f 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -14,6 +14,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; @@ -48,6 +49,7 @@ describe("deviceTrustService", () => { const i18nService = mock(); const platformUtilsService = mock(); const secureStorageService = mock(); + const logService = mock(); const userDecryptionOptionsService = mock(); const decryptionOptions = new BehaviorSubject(null); @@ -726,6 +728,7 @@ describe("deviceTrustService", () => { stateProvider, secureStorageService, userDecryptionOptionsService, + logService, ); } }); From ff3021129e82f888a7d12e8e42e1f9913a107683 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 6 May 2024 09:14:47 -0700 Subject: [PATCH 02/18] [PM-6827] Browser Extension Refresh - Tabs Routing (#9004) * [PM-6827] Add componentRouteSwap util function * [PM-6827] Add extension-refresh feature flag * [PM-6827] Add extension-refresh route swap utils * [PM-6827] Add the TabsV2 component * [PM-6827] Add the TabsV2 to routing module * [PM-6827] Fix route prefixes in popup-tab-navigation component --- .../layout/popup-tab-navigation.component.ts | 8 +-- apps/browser/src/popup/app-routing.module.ts | 10 ++-- apps/browser/src/popup/app.module.ts | 2 + .../popup/extension-refresh-route-utils.ts | 45 +++++++++++++++ apps/browser/src/popup/tabs-v2.component.ts | 11 ++++ .../angular/src/utils/component-route-swap.ts | 55 +++++++++++++++++++ libs/common/src/enums/feature-flag.enum.ts | 2 + 7 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 apps/browser/src/popup/extension-refresh-route-utils.ts create mode 100644 apps/browser/src/popup/tabs-v2.component.ts create mode 100644 libs/angular/src/utils/component-route-swap.ts diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index 3a275454d9..ced3f6462e 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -17,25 +17,25 @@ export class PopupTabNavigationComponent { navButtons = [ { label: "Vault", - page: "/vault", + page: "/tabs/vault", iconKey: "lock", iconKeyActive: "lock-f", }, { label: "Generator", - page: "/generator", + page: "/tabs/generator", iconKey: "generate", iconKeyActive: "generate-f", }, { label: "Send", - page: "/send", + page: "/tabs/send", iconKey: "send", iconKeyActive: "send-f", }, { label: "Settings", - page: "/settings", + page: "/tabs/settings", iconKey: "cog", iconKeyActive: "cog-f", }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 14659cb4df..0dcf496457 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -2,9 +2,9 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; import { - redirectGuard, AuthGuard, lockGuard, + redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; @@ -47,6 +47,7 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; import { FoldersComponent } from "./settings/folders.component"; @@ -54,6 +55,7 @@ import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component import { OptionsComponent } from "./settings/options.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; const unauthRouteOverrides = { @@ -322,9 +324,8 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "help-and-feedback" }, }, - { + ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", - component: TabsComponent, data: { state: "tabs" }, children: [ { @@ -336,6 +337,7 @@ const routes: Routes = [ path: "current", component: CurrentTabComponent, canActivate: [AuthGuard], + canMatch: [extensionRefreshRedirect("/tabs/vault")], data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, @@ -364,7 +366,7 @@ const routes: Routes = [ data: { state: "tabs_send" }, }, ], - }, + }), { path: "account-switcher", component: AccountSwitcherComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index a6e953ad1d..bed40dfddc 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -80,6 +80,7 @@ import { OptionsComponent } from "./settings/options.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; // Register the locales for the application @@ -160,6 +161,7 @@ import "../platform/popup/locales"; SsoComponent, SyncComponent, TabsComponent, + TabsV2Component, TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, diff --git a/apps/browser/src/popup/extension-refresh-route-utils.ts b/apps/browser/src/popup/extension-refresh-route-utils.ts new file mode 100644 index 0000000000..3c2ca33f86 --- /dev/null +++ b/apps/browser/src/popup/extension-refresh-route-utils.ts @@ -0,0 +1,45 @@ +import { inject, Type } from "@angular/core"; +import { Route, Router, Routes, UrlTree } from "@angular/router"; + +import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +/** + * Helper function to swap between two components based on the ExtensionRefresh feature flag. + * @param defaultComponent - The current non-refreshed component to render. + * @param refreshedComponent - The new refreshed component to render. + * @param options - The shared route options to apply to both components. + */ +export function extensionRefreshSwap( + defaultComponent: Type, + refreshedComponent: Type, + options: Route, +): Routes { + return componentRouteSwap( + defaultComponent, + refreshedComponent, + async () => { + const configService = inject(ConfigService); + return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + }, + options, + ); +} + +/** + * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag. + * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled. + */ +export function extensionRefreshRedirect(redirectUrl: string): () => Promise { + return async () => { + const configService = inject(ConfigService); + const router = inject(Router); + const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + if (shouldRedirect) { + return router.parseUrl(redirectUrl); + } else { + return true; + } + }; +} diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts new file mode 100644 index 0000000000..4cdb8fc029 --- /dev/null +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-tabs-v2", + template: ` + + + + `, +}) +export class TabsV2Component {} diff --git a/libs/angular/src/utils/component-route-swap.ts b/libs/angular/src/utils/component-route-swap.ts new file mode 100644 index 0000000000..1a2db317d6 --- /dev/null +++ b/libs/angular/src/utils/component-route-swap.ts @@ -0,0 +1,55 @@ +import { Type } from "@angular/core"; +import { Route, Routes } from "@angular/router"; + +/** + * Helper function to swap between two components based on an async condition. The async condition is evaluated + * as an `CanMatchFn` and supports Angular dependency injection via `inject()`. + * + * @example + * ```ts + * const routes = [ + * ...componentRouteSwap( + * defaultComponent, + * altComponent, + * async () => { + * const configService = inject(ConfigService); + * return configService.getFeatureFlag(FeatureFlag.SomeFlag); + * }, + * { + * path: 'some-path' + * } + * ), + * // Other routes... + * ]; + * ``` + * + * @param defaultComponent - The default component to render. + * @param altComponent - The alternate component to render when the condition is met. + * @param shouldSwapFn - The async function to determine if the alternate component should be rendered. + * @param options - The shared route options to apply to both components. + */ +export function componentRouteSwap( + defaultComponent: Type, + altComponent: Type, + shouldSwapFn: () => Promise, + options: Route, +): Routes { + const defaultRoute = { + ...options, + component: defaultComponent, + }; + + const altRoute: Route = { + ...options, + component: altComponent, + canMatch: [ + async () => { + return await shouldSwapFn(); + }, + ...(options.canMatch ?? []), + ], + }; + + // Return the alternate route first, so it is evaluated first. + return [altRoute, defaultRoute]; +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d84494362e..5ed3724f2f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -16,6 +16,7 @@ export enum FeatureFlag { AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", + ExtensionRefresh = "extension-refresh", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -42,6 +43,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.UnassignedItemsBanner]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, + [FeatureFlag.ExtensionRefresh]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 425c7914b0673d33e69ef8cce2c78e42915acba5 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 6 May 2024 20:21:11 +0100 Subject: [PATCH 03/18] [AC-2558] Provider Admin still sees manage billing options - not the provided image (#9048) * Fix the issue of provider admin not seeing the image * Resolve the case and ternary operator comment --- ...ganization-subscription-cloud.component.html | 2 +- ...organization-subscription-cloud.component.ts | 17 +++++++++-------- .../response/provider/provider.response.ts | 3 +++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 38903bab19..306c8dbc26 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -256,7 +256,7 @@ - +
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 7acb108808..198332db0c 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -5,6 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; @@ -49,7 +50,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy locale: string; showUpdatedSubscriptionStatusSection$: Observable; manageBillingFromProviderPortal = ManageBilling; - IsProviderManaged = false; + isProviderManaged = false; protected readonly teamsStarter = ProductType.TeamsStarter; @@ -69,6 +70,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, + private providerService: ProviderApiServiceAbstraction, ) {} async ngOnInit() { @@ -106,13 +108,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - this.IsProviderManaged = - this.userOrg.hasProvider && - this.userOrg.providerType == ProviderType.Msp && - enableConsolidatedBilling - ? true - : false; + if (this.userOrg.hasProvider) { + const provider = await this.providerService.getProvider(this.userOrg.providerId); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + this.isProviderManaged = provider.type == ProviderType.Msp && enableConsolidatedBilling; + } + if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; diff --git a/libs/common/src/admin-console/models/response/provider/provider.response.ts b/libs/common/src/admin-console/models/response/provider/provider.response.ts index 0ea925cd33..369499595c 100644 --- a/libs/common/src/admin-console/models/response/provider/provider.response.ts +++ b/libs/common/src/admin-console/models/response/provider/provider.response.ts @@ -1,4 +1,5 @@ import { BaseResponse } from "../../../../models/response/base.response"; +import { ProviderType } from "../../../enums"; export class ProviderResponse extends BaseResponse { id: string; @@ -6,6 +7,7 @@ export class ProviderResponse extends BaseResponse { businessName: string; billingEmail: string; creationDate: Date; + type: ProviderType; constructor(response: any) { super(response); @@ -14,5 +16,6 @@ export class ProviderResponse extends BaseResponse { this.businessName = this.getResponseProperty("BusinessName"); this.billingEmail = this.getResponseProperty("BillingEmail"); this.creationDate = this.getResponseProperty("CreationDate"); + this.type = this.getResponseProperty("Type"); } } From 6c21223466a88659ba5041fdbe8ede1b96b832d2 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Mon, 6 May 2024 15:25:41 -0400 Subject: [PATCH 04/18] [PM-7662] Suppress welcome window on install when extension is in dev mode (#8864) * suppress welcome window on install when extension is in dev mode * use platformUtilsService.isDev instead of process.env.ENV * use devFlags.skipWelcomeOnInstall instead of platformUtilsService.isDev * update old dev_flags casing in base configs --- apps/browser/config/base.json | 2 +- apps/browser/config/development.json | 3 ++- apps/browser/src/background/runtime.background.ts | 8 +++++--- apps/desktop/config/base.json | 2 +- libs/common/src/platform/misc/flags.ts | 1 + 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index 8a3ccc14d3..6c428c43d2 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -1,5 +1,5 @@ { - "dev_flags": {}, + "devFlags": {}, "flags": { "showPasswordless": true, "enableCipherKeyEncryption": false, diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index aba10eb25b..e0925ebecc 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -2,7 +2,8 @@ "devFlags": { "managedEnvironment": { "base": "https://localhost:8080" - } + }, + "skipWelcomeOnInstall": true }, "flags": { "showPasswordless": true, diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index d8f3cf840f..1db32659d2 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -8,6 +8,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; +import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -324,9 +325,10 @@ export default class RuntimeBackground { if (this.onInstalledReason != null) { if (this.onInstalledReason === "install") { - // 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 - BrowserApi.createNewTab("https://bitwarden.com/browser-start/"); + if (!devFlagEnabled("skipWelcomeOnInstall")) { + void BrowserApi.createNewTab("https://bitwarden.com/browser-start/"); + } + await this.autofillSettingsService.setInlineMenuVisibility( AutofillOverlayVisibility.OnFieldFocus, ); diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index cb408a87d8..1f4f624dc8 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,5 +1,5 @@ { - "dev_flags": {}, + "devFlags": {}, "flags": { "multithreadDecryption": false, "enableCipherKeyEncryption": false diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index cc463b1060..e0089a5451 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -10,6 +10,7 @@ export type SharedFlags = { // eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; + skipWelcomeOnInstall: boolean; }; function getFlags(envFlags: string | T): T { From cf74870779c50c535b1e5b22ff05760d445518b8 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 6 May 2024 14:56:24 -0500 Subject: [PATCH 05/18] [SM-1030] Remove access selector (#8909) * remove access selector --- .../access-policy.view.ts | 29 --- .../potential-grantee.view.ts | 0 .../project-people-access-policies.view.ts | 6 + ...t-service-accounts-access-policies.view.ts | 5 + .../service-account-granted-policies.view.ts | 10 + ...ice-account-people-access-policies.view.ts | 9 + .../project-service-accounts.component.ts | 2 +- .../projects/projects/projects.component.ts | 3 - .../service-account-projects.component.ts | 2 +- .../models/ap-item-value.type.ts | 12 +- .../models/ap-item-view.type.ts | 12 +- .../access-policies/access-policy.service.ts | 237 +++++------------- .../access-selector.component.html | 80 ------ .../access-selector.component.ts | 194 -------------- .../access-policies-create.request.ts | 7 - .../requests/access-policy-update.request.ts | 4 - .../project-access-policies.response.ts | 29 --- .../shared/sm-shared.module.ts | 3 - 18 files changed, 103 insertions(+), 541 deletions(-) rename bitwarden_license/bit-web/src/app/secrets-manager/models/view/{ => access-policies}/access-policy.view.ts (53%) rename bitwarden_license/bit-web/src/app/secrets-manager/models/view/{ => access-policies}/potential-grantee.view.ts (100%) create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts similarity index 53% rename from bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts rename to bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts index 6c005a1225..76be88b610 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts @@ -42,32 +42,3 @@ export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView grantedProjectId: string; grantedProjectName: string; } - -export class ProjectAccessPoliciesView { - userAccessPolicies: UserProjectAccessPolicyView[]; - groupAccessPolicies: GroupProjectAccessPolicyView[]; - serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; -} - -export class ProjectPeopleAccessPoliciesView { - userAccessPolicies: UserProjectAccessPolicyView[]; - groupAccessPolicies: GroupProjectAccessPolicyView[]; -} - -export class ServiceAccountPeopleAccessPoliciesView { - userAccessPolicies: UserServiceAccountAccessPolicyView[]; - groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; -} - -export class ServiceAccountProjectPolicyPermissionDetailsView { - accessPolicy: ServiceAccountProjectAccessPolicyView; - hasPermission: boolean; -} - -export class ServiceAccountGrantedPoliciesView { - grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; -} - -export class ProjectServiceAccountsAccessPoliciesView { - serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/potential-grantee.view.ts similarity index 100% rename from bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts rename to bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/potential-grantee.view.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts new file mode 100644 index 0000000000..c85b289928 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts @@ -0,0 +1,6 @@ +import { GroupProjectAccessPolicyView, UserProjectAccessPolicyView } from "./access-policy.view"; + +export class ProjectPeopleAccessPoliciesView { + userAccessPolicies: UserProjectAccessPolicyView[]; + groupAccessPolicies: GroupProjectAccessPolicyView[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts new file mode 100644 index 0000000000..28636c4df1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts @@ -0,0 +1,5 @@ +import { ServiceAccountProjectAccessPolicyView } from "./access-policy.view"; + +export class ProjectServiceAccountsAccessPoliciesView { + serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts new file mode 100644 index 0000000000..7d53e38263 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts @@ -0,0 +1,10 @@ +import { ServiceAccountProjectAccessPolicyView } from "./access-policy.view"; + +export class ServiceAccountGrantedPoliciesView { + grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; +} + +export class ServiceAccountProjectPolicyPermissionDetailsView { + accessPolicy: ServiceAccountProjectAccessPolicyView; + hasPermission: boolean; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts new file mode 100644 index 0000000000..58dcf6d470 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts @@ -0,0 +1,9 @@ +import { + GroupServiceAccountAccessPolicyView, + UserServiceAccountAccessPolicyView, +} from "./access-policy.view"; + +export class ServiceAccountPeopleAccessPoliciesView { + userAccessPolicies: UserServiceAccountAccessPolicyView[]; + groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts index 668bdbae43..7ac111ef62 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts @@ -7,7 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policy.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view"; import { ApItemValueType, convertToProjectServiceAccountsAccessPoliciesView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts index 831ee4df9b..fd59014642 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.ts @@ -6,7 +6,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { DialogService } from "@bitwarden/components"; import { ProjectListView } from "../../models/view/project-list.view"; -import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; import { BulkConfirmationDetails, BulkConfirmationDialogComponent, @@ -38,7 +37,6 @@ export class ProjectsComponent implements OnInit { constructor( private route: ActivatedRoute, private projectService: ProjectService, - private accessPolicyService: AccessPolicyService, private dialogService: DialogService, private organizationService: OrganizationService, ) {} @@ -47,7 +45,6 @@ export class ProjectsComponent implements OnInit { this.projects$ = combineLatest([ this.route.params, this.projectService.project$.pipe(startWith(null)), - this.accessPolicyService.projectAccessPolicyChanges$.pipe(startWith(null)), ]).pipe( switchMap(async ([params]) => { this.organizationId = params.organizationId; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts index a6f3d720b7..49ddfe331f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts @@ -7,7 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policy.view"; +import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policies/service-account-granted-policies.view"; import { ApItemValueType, convertToServiceAccountGrantedPoliciesView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts index 237fa2f323..935c77f1b3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts @@ -1,15 +1,17 @@ import { - ProjectPeopleAccessPoliciesView, UserProjectAccessPolicyView, GroupProjectAccessPolicyView, - ServiceAccountPeopleAccessPoliciesView, UserServiceAccountAccessPolicyView, GroupServiceAccountAccessPolicyView, + ServiceAccountProjectAccessPolicyView, +} from "../../../../models/view/access-policies/access-policy.view"; +import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { ServiceAccountGrantedPoliciesView, ServiceAccountProjectPolicyPermissionDetailsView, - ServiceAccountProjectAccessPolicyView, - ProjectServiceAccountsAccessPoliciesView, -} from "../../../../models/view/access-policy.view"; +} from "../../../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; import { ApItemEnum } from "./enums/ap-item.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts index 07e08afcf9..1a023659c1 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts @@ -1,13 +1,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SelectItemView } from "@bitwarden/components"; -import { - ProjectPeopleAccessPoliciesView, - ServiceAccountGrantedPoliciesView, - ProjectServiceAccountsAccessPoliciesView, - ServiceAccountPeopleAccessPoliciesView, -} from "../../../../models/view/access-policy.view"; -import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; +import { PotentialGranteeView } from "../../../../models/view/access-policies/potential-grantee.view"; +import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { ServiceAccountGrantedPoliciesView } from "../../../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts index 98684e3a60..32c130647a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -1,5 +1,4 @@ import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; @@ -9,26 +8,23 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { - BaseAccessPolicyView, - GroupProjectAccessPolicyView, - GroupServiceAccountAccessPolicyView, - ProjectAccessPoliciesView, - ProjectPeopleAccessPoliciesView, - ServiceAccountProjectAccessPolicyView, UserProjectAccessPolicyView, + GroupProjectAccessPolicyView, UserServiceAccountAccessPolicyView, - ServiceAccountPeopleAccessPoliciesView, + GroupServiceAccountAccessPolicyView, + ServiceAccountProjectAccessPolicyView, +} from "../../models/view/access-policies/access-policy.view"; +import { PotentialGranteeView } from "../../models/view/access-policies/potential-grantee.view"; +import { ProjectPeopleAccessPoliciesView } from "../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { ServiceAccountGrantedPoliciesView, - ProjectServiceAccountsAccessPoliciesView, ServiceAccountProjectPolicyPermissionDetailsView, -} from "../../models/view/access-policy.view"; -import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; -import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request"; +} from "../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view"; import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request"; -import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response"; import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/requests/service-account-granted-policies.request"; -import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request"; import { @@ -49,39 +45,12 @@ import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/r providedIn: "root", }) export class AccessPolicyService { - private _projectAccessPolicyChanges$ = new Subject(); - - /** - * Emits when a project access policy is created or deleted. - */ - readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable(); - constructor( private cryptoService: CryptoService, protected apiService: ApiService, protected encryptService: EncryptService, ) {} - refreshProjectAccessPolicyChanges() { - this._projectAccessPolicyChanges$.next(null); - } - - async getProjectAccessPolicies( - organizationId: string, - projectId: string, - ): Promise { - const r = await this.apiService.send( - "GET", - "/projects/" + projectId + "/access-policies", - null, - true, - true, - ); - - const results = new ProjectAccessPoliciesResponse(r); - return await this.createProjectAccessPoliciesView(organizationId, results); - } - async getProjectPeopleAccessPolicies( projectId: string, ): Promise { @@ -212,43 +181,6 @@ export class AccessPolicyService { return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); } - async createProjectAccessPolicies( - organizationId: string, - projectId: string, - projectAccessPoliciesView: ProjectAccessPoliciesView, - ): Promise { - const request = this.getAccessPoliciesCreateRequest(projectAccessPoliciesView); - const r = await this.apiService.send( - "POST", - "/projects/" + projectId + "/access-policies", - request, - true, - true, - ); - const results = new ProjectAccessPoliciesResponse(r); - const view = await this.createProjectAccessPoliciesView(organizationId, results); - this._projectAccessPolicyChanges$.next(view); - return view; - } - - async deleteAccessPolicy(accessPolicyId: string): Promise { - await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); - this._projectAccessPolicyChanges$.next(null); - } - - async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise { - const payload = new AccessPolicyUpdateRequest(); - payload.read = baseAccessPolicyView.read; - payload.write = baseAccessPolicyView.write; - await this.apiService.send( - "PUT", - "/access-policies/" + baseAccessPolicyView.id, - payload, - true, - true, - ); - } - async getPeoplePotentialGrantees(organizationId: string) { const r = await this.apiService.send( "GET", @@ -285,11 +217,11 @@ export class AccessPolicyService { return await this.createPotentialGranteeViews(organizationId, results.data); } - protected async getOrganizationKey(organizationId: string): Promise { + private async getOrganizationKey(organizationId: string): Promise { return await this.cryptoService.getOrgKey(organizationId); } - protected getAccessPolicyRequest( + private getAccessPolicyRequest( granteeId: string, view: | UserProjectAccessPolicyView @@ -305,7 +237,53 @@ export class AccessPolicyService { return request; } - protected createBaseAccessPolicyView( + private getServiceAccountGrantedPoliciesRequest( + policies: ServiceAccountGrantedPoliciesView, + ): ServiceAccountGrantedPoliciesRequest { + const request = new ServiceAccountGrantedPoliciesRequest(); + + request.projectGrantedPolicyRequests = policies.grantedProjectPolicies.map((detailView) => ({ + grantedId: detailView.accessPolicy.grantedProjectId, + read: detailView.accessPolicy.read, + write: detailView.accessPolicy.write, + })); + + return request; + } + + private getProjectServiceAccountsAccessPoliciesRequest( + policies: ProjectServiceAccountsAccessPoliciesView, + ): ProjectServiceAccountsAccessPoliciesRequest { + const request = new ProjectServiceAccountsAccessPoliciesRequest(); + + request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.serviceAccountId, ap); + }); + + return request; + } + + private getPeopleAccessPoliciesRequest( + view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, + ): PeopleAccessPoliciesRequest { + const request = new PeopleAccessPoliciesRequest(); + + if (view.userAccessPolicies?.length > 0) { + request.userAccessPolicyRequests = view.userAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.organizationUserId, ap); + }); + } + + if (view.groupAccessPolicies?.length > 0) { + request.groupAccessPolicyRequests = view.groupAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.groupId, ap); + }); + } + + return request; + } + + private createBaseAccessPolicyView( response: | UserProjectAccessPolicyResponse | UserServiceAccountAccessPolicyResponse @@ -348,32 +326,6 @@ export class AccessPolicyService { ); } - private getServiceAccountGrantedPoliciesRequest( - policies: ServiceAccountGrantedPoliciesView, - ): ServiceAccountGrantedPoliciesRequest { - const request = new ServiceAccountGrantedPoliciesRequest(); - - request.projectGrantedPolicyRequests = policies.grantedProjectPolicies.map((detailView) => ({ - grantedId: detailView.accessPolicy.grantedProjectId, - read: detailView.accessPolicy.read, - write: detailView.accessPolicy.write, - })); - - return request; - } - - private getProjectServiceAccountsAccessPoliciesRequest( - policies: ProjectServiceAccountsAccessPoliciesView, - ): ProjectServiceAccountsAccessPoliciesRequest { - const request = new ProjectServiceAccountsAccessPoliciesRequest(); - - request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.serviceAccountId, ap); - }); - - return request; - } - private async createServiceAccountGrantedPoliciesView( response: ServiceAccountGrantedPoliciesPermissionDetailsResponse, organizationId: string, @@ -413,27 +365,6 @@ export class AccessPolicyService { return view; } - private async createProjectAccessPoliciesView( - organizationId: string, - projectAccessPoliciesResponse: ProjectAccessPoliciesResponse, - ): Promise { - const orgKey = await this.getOrganizationKey(organizationId); - const view = new ProjectAccessPoliciesView(); - - view.userAccessPolicies = projectAccessPoliciesResponse.userAccessPolicies.map((ap) => { - return this.createUserProjectAccessPolicyView(ap); - }); - view.groupAccessPolicies = projectAccessPoliciesResponse.groupAccessPolicies.map((ap) => { - return this.createGroupProjectAccessPolicyView(ap); - }); - view.serviceAccountAccessPolicies = await Promise.all( - projectAccessPoliciesResponse.serviceAccountAccessPolicies.map(async (ap) => { - return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); - }), - ); - return view; - } - private createProjectPeopleAccessPoliciesView( peopleAccessPoliciesResponse: ProjectPeopleAccessPoliciesResponse, ): ProjectPeopleAccessPoliciesView { @@ -462,56 +393,6 @@ export class AccessPolicyService { return view; } - private getAccessPoliciesCreateRequest( - projectAccessPoliciesView: ProjectAccessPoliciesView, - ): AccessPoliciesCreateRequest { - const createRequest = new AccessPoliciesCreateRequest(); - - if (projectAccessPoliciesView.userAccessPolicies?.length > 0) { - createRequest.userAccessPolicyRequests = projectAccessPoliciesView.userAccessPolicies.map( - (ap) => { - return this.getAccessPolicyRequest(ap.organizationUserId, ap); - }, - ); - } - - if (projectAccessPoliciesView.groupAccessPolicies?.length > 0) { - createRequest.groupAccessPolicyRequests = projectAccessPoliciesView.groupAccessPolicies.map( - (ap) => { - return this.getAccessPolicyRequest(ap.groupId, ap); - }, - ); - } - - if (projectAccessPoliciesView.serviceAccountAccessPolicies?.length > 0) { - createRequest.serviceAccountAccessPolicyRequests = - projectAccessPoliciesView.serviceAccountAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.serviceAccountId, ap); - }); - } - return createRequest; - } - - private getPeopleAccessPoliciesRequest( - view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, - ): PeopleAccessPoliciesRequest { - const request = new PeopleAccessPoliciesRequest(); - - if (view.userAccessPolicies?.length > 0) { - request.userAccessPolicyRequests = view.userAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.organizationUserId, ap); - }); - } - - if (view.groupAccessPolicies?.length > 0) { - request.groupAccessPolicyRequests = view.groupAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.groupId, ap); - }); - } - - return request; - } - private createUserProjectAccessPolicyView( response: UserProjectAccessPolicyResponse, ): UserProjectAccessPolicyView { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html deleted file mode 100644 index 9da038a2d8..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
- - {{ label }} - - {{ hint }} - - -
- - - - - - {{ columnTitle }} - {{ "permissions" | i18n }} - - - - - - - - - - {{ row.name }} - - - - - - {{ "canRead" | i18n }} - {{ "canWrite" | i18n }} - {{ "canReadWrite" | i18n }} - - - - - - - - - - - - -
- {{ emptyMessage }} -
-
- - -
- -
-
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts deleted file mode 100644 index 8deed43cd3..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; -import { - combineLatest, - firstValueFrom, - map, - Observable, - share, - Subject, - switchMap, - tap, -} from "rxjs"; - -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; - -import { BaseAccessPolicyView } from "../../models/view/access-policy.view"; - -import { AccessPolicyService } from "./access-policy.service"; - -export type AccessSelectorRowView = { - type: "user" | "group" | "serviceAccount" | "project"; - name: string; - id: string; - accessPolicyId: string; - read: boolean; - write: boolean; - icon: string; - userId?: string; - currentUserInGroup?: boolean; - static?: boolean; -}; - -@Component({ - selector: "sm-access-selector", - templateUrl: "./access-selector.component.html", -}) -export class AccessSelectorComponent implements OnInit { - static readonly userIcon = "bwi-user"; - static readonly groupIcon = "bwi-family"; - static readonly serviceAccountIcon = "bwi-wrench"; - static readonly projectIcon = "bwi-collection"; - - /** - * Emits the selected items on submit. - */ - @Output() onCreateAccessPolicies = new EventEmitter(); - @Output() onDeleteAccessPolicy = new EventEmitter(); - @Output() onUpdateAccessPolicy = new EventEmitter(); - - @Input() label: string; - @Input() hint: string; - @Input() columnTitle: string; - @Input() emptyMessage: string; - @Input() granteeType: "people" | "serviceAccounts" | "projects"; - - protected rows$ = new Subject(); - @Input() private set rows(value: AccessSelectorRowView[]) { - const sorted = value.sort((a, b) => { - if (a.icon == b.icon) { - return a.name.localeCompare(b.name); - } - if (a.icon == AccessSelectorComponent.userIcon) { - return -1; - } - return 1; - }); - this.rows$.next(sorted); - } - - private maxLength = 15; - protected formGroup = new FormGroup({ - multiSelect: new FormControl([], [Validators.required, Validators.maxLength(this.maxLength)]), - }); - protected loading = true; - - protected selectItems$: Observable = combineLatest([ - this.rows$, - this.route.params, - ]).pipe( - switchMap(([rows, params]) => - this.getPotentialGrantees(params.organizationId).then((grantees) => - grantees - .filter((g) => !rows.some((row) => row.id === g.id)) - .map((granteeView) => { - let icon: string; - let listName = granteeView.name; - let labelName = granteeView.name; - if (granteeView.type === "user") { - icon = AccessSelectorComponent.userIcon; - if (Utils.isNullOrWhitespace(granteeView.name)) { - listName = granteeView.email; - labelName = granteeView.email; - } else { - listName = `${granteeView.name} (${granteeView.email})`; - } - } else if (granteeView.type === "group") { - icon = AccessSelectorComponent.groupIcon; - } else if (granteeView.type === "serviceAccount") { - icon = AccessSelectorComponent.serviceAccountIcon; - } else if (granteeView.type === "project") { - icon = AccessSelectorComponent.projectIcon; - } - return { - icon: icon, - id: granteeView.id, - labelName: labelName, - listName: listName, - }; - }), - ), - ), - map((selectItems) => selectItems.sort((a, b) => a.listName.localeCompare(b.listName))), - tap(() => { - this.loading = false; - this.formGroup.reset(); - this.formGroup.enable(); - }), - share(), - ); - - constructor( - private accessPolicyService: AccessPolicyService, - private route: ActivatedRoute, - ) {} - - ngOnInit(): void { - this.formGroup.disable(); - } - - submit = async () => { - this.formGroup.markAllAsTouched(); - if (this.formGroup.invalid) { - return; - } - this.formGroup.disable(); - this.loading = true; - - this.onCreateAccessPolicies.emit(this.formGroup.value.multiSelect); - - return firstValueFrom(this.selectItems$); - }; - - async update(target: any, row: AccessSelectorRowView): Promise { - if (target.value === "canRead") { - row.read = true; - row.write = false; - } else if (target.value === "canReadWrite") { - row.read = true; - row.write = true; - } - this.onUpdateAccessPolicy.emit(row); - } - - delete = (row: AccessSelectorRowView) => async () => { - this.loading = true; - this.formGroup.disable(); - this.onDeleteAccessPolicy.emit(row); - return firstValueFrom(this.selectItems$); - }; - - private getPotentialGrantees(organizationId: string) { - switch (this.granteeType) { - case "people": - return this.accessPolicyService.getPeoplePotentialGrantees(organizationId); - case "serviceAccounts": - return this.accessPolicyService.getServiceAccountsPotentialGrantees(organizationId); - case "projects": - return this.accessPolicyService.getProjectsPotentialGrantees(organizationId); - } - } - - static getAccessItemType(item: SelectItemView) { - switch (item.icon) { - case AccessSelectorComponent.userIcon: - return "user"; - case AccessSelectorComponent.groupIcon: - return "group"; - case AccessSelectorComponent.serviceAccountIcon: - return "serviceAccount"; - case AccessSelectorComponent.projectIcon: - return "project"; - } - } - - static getBaseAccessPolicyView(row: AccessSelectorRowView) { - const view = new BaseAccessPolicyView(); - view.id = row.accessPolicyId; - view.read = row.read; - view.write = row.write; - return view; - } -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts deleted file mode 100644 index ff391ecacd..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AccessPolicyRequest } from "./access-policy.request"; - -export class AccessPoliciesCreateRequest { - userAccessPolicyRequests?: AccessPolicyRequest[]; - groupAccessPolicyRequests?: AccessPolicyRequest[]; - serviceAccountAccessPolicyRequests?: AccessPolicyRequest[]; -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts deleted file mode 100644 index 5aff186e12..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class AccessPolicyUpdateRequest { - read: boolean; - write: boolean; -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts deleted file mode 100644 index 66d76c1493..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; - -import { - GroupProjectAccessPolicyResponse, - ServiceAccountProjectAccessPolicyResponse, - UserProjectAccessPolicyResponse, -} from "./access-policy.response"; - -export class ProjectAccessPoliciesResponse extends BaseResponse { - userAccessPolicies: UserProjectAccessPolicyResponse[]; - groupAccessPolicies: GroupProjectAccessPolicyResponse[]; - serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; - - constructor(response: any) { - super(response); - const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); - this.userAccessPolicies = userAccessPolicies.map( - (k: any) => new UserProjectAccessPolicyResponse(k), - ); - const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); - this.groupAccessPolicies = groupAccessPolicies.map( - (k: any) => new GroupProjectAccessPolicyResponse(k), - ); - const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); - this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( - (k: any) => new ServiceAccountProjectAccessPolicyResponse(k), - ); - } -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts index cb302fb7db..cb723af6d7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts @@ -13,7 +13,6 @@ import { ProductSwitcherModule } from "@bitwarden/web-vault/app/layouts/product- import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { AccessPolicySelectorComponent } from "./access-policies/access-policy-selector/access-policy-selector.component"; -import { AccessSelectorComponent } from "./access-policies/access-selector.component"; import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component"; import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { NewMenuComponent } from "./new-menu.component"; @@ -35,7 +34,6 @@ import { SecretsListComponent } from "./secrets-list.component"; ], exports: [ AccessPolicySelectorComponent, - AccessSelectorComponent, BulkConfirmationDialogComponent, BulkStatusDialogComponent, HeaderModule, @@ -49,7 +47,6 @@ import { SecretsListComponent } from "./secrets-list.component"; ], declarations: [ AccessPolicySelectorComponent, - AccessSelectorComponent, BulkConfirmationDialogComponent, BulkStatusDialogComponent, BulkStatusDialogComponent, From 2b9c4c6e34bde9aec2d42f87759784308c1a74b9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 6 May 2024 21:38:53 +0100 Subject: [PATCH 06/18] Rename the isProviderManaged variable (#9061) --- .../organization-subscription-cloud.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 306c8dbc26..5a71e353d7 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -1,6 +1,6 @@ - + {{ "loading" | i18n }} From af6a63c10b721a38ca11fb47b78e3279a0fa950f Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 6 May 2024 18:37:46 -0400 Subject: [PATCH 07/18] Bumped browser,cli,desktop,web version to (#9064) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 580acfc3d0..278a3b6c52 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.4.2", + "version": "2024.5.0", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 18bfaf8acb..fc0ff51230 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.4.2", + "version": "2024.5.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 93d798490f..0720b65a91 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.4.2", + "version": "2024.5.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index d6c449de48..b57b818c63 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.0", + "version": "2024.5.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5e098eb213..90d9841a61 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.3", + "version": "2024.5.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index d6945fd16e..508c42fa72 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.3", + "version": "2024.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.3", + "version": "2024.5.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index fa190af9a6..ea4b95491c 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.3", + "version": "2024.5.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 434712cdf4..6e5355c708 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.4.2", + "version": "2024.5.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index b7372ce27b..f853763efa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,11 +193,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.4.2" + "version": "2024.5.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.4.0", + "version": "2024.5.0", "license": "GPL-3.0-only", "dependencies": { "@koa/multer": "3.0.2", @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.3", + "version": "2024.5.0", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.4.2" + "version": "2024.5.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From 0fb352d8edfdef2b6fda2cda4a3e849a81081d3b Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 6 May 2024 18:34:40 -0700 Subject: [PATCH 08/18] [PM-7343] AnonLayoutComponent Implementation Groundwork (#8585) * test implementation * move files * adjust import and sample router comments * add storybook docs to anon-layout * rename to AnonLayoutWrapperComponent * update storybook docs * remove references to CL and replace with 'Auth-owned' * move AnonLayoutWrapperComponent to libs * add pageTitle input * add subTitle input * translate page title/subtitle, and refactor how icon is added * update tailwind.config and component styles * adjust spacing between primary and secondary content * move switch statement to wrapper * move icon to router file * update storybook documentation * fix storybook text color in normal code blocks * remove sample route * move wrapper component back to web * remove sample route * update storybook docs --- .storybook/main.ts | 1 + .../auth/anon-layout-wrapper.component.html | 4 + .../app/auth/anon-layout-wrapper.component.ts | 34 +++++ apps/web/tailwind.config.js | 1 + .../anon-layout/anon-layout.component.html | 8 +- .../anon-layout/anon-layout.component.ts | 2 +- .../src/angular/anon-layout/anon-layout.mdx | 118 ++++++++++++++++++ .../anon-layout/anon-layout.stories.ts | 38 +++--- .../icons/bitwarden-logo.icon.ts} | 0 libs/auth/src/angular/icons/index.ts | 2 + .../icons/lock.icon.ts} | 2 +- libs/auth/src/angular/index.ts | 1 + 12 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 apps/web/src/app/auth/anon-layout-wrapper.component.html create mode 100644 apps/web/src/app/auth/anon-layout-wrapper.component.ts create mode 100644 libs/auth/src/angular/anon-layout/anon-layout.mdx rename libs/auth/src/{icons/bitwarden-logo.ts => angular/icons/bitwarden-logo.icon.ts} (100%) rename libs/auth/src/{icons/icon-lock.ts => angular/icons/lock.icon.ts} (98%) diff --git a/.storybook/main.ts b/.storybook/main.ts index cb63ada550..26eee201f9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -4,6 +4,7 @@ import remarkGfm from "remark-gfm"; const config: StorybookConfig = { stories: [ + "../libs/auth/src/**/*.mdx", "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/components/src/**/*.mdx", "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", diff --git a/apps/web/src/app/auth/anon-layout-wrapper.component.html b/apps/web/src/app/auth/anon-layout-wrapper.component.html new file mode 100644 index 0000000000..26cab30809 --- /dev/null +++ b/apps/web/src/app/auth/anon-layout-wrapper.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/app/auth/anon-layout-wrapper.component.ts b/apps/web/src/app/auth/anon-layout-wrapper.component.ts new file mode 100644 index 0000000000..e39a8e11a9 --- /dev/null +++ b/apps/web/src/app/auth/anon-layout-wrapper.component.ts @@ -0,0 +1,34 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, RouterModule } from "@angular/router"; + +import { AnonLayoutComponent } from "@bitwarden/auth/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Icon } from "@bitwarden/components"; + +@Component({ + standalone: true, + templateUrl: "anon-layout-wrapper.component.html", + imports: [AnonLayoutComponent, RouterModule], +}) +export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { + protected pageTitle: string; + protected pageSubtitle: string; + protected pageIcon: Icon; + + constructor( + private route: ActivatedRoute, + private i18nService: I18nService, + ) { + this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]); + this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]); + this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; // don't translate + } + + ngOnInit() { + document.body.classList.add("layout_frontend"); + } + + ngOnDestroy() { + document.body.classList.remove("layout_frontend"); + } +} diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index a944f9dd67..e80bf6a834 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base"); config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/auth/src/**/*.{html,ts}", "../../bitwarden_license/bit-web/src/**/*.{html,ts}", ]; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 1f583edf20..55da36d9bf 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,5 +1,5 @@
@@ -13,8 +13,10 @@

{{ subtitle }}

-
-
+
+
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index d247a010bf..106844fb5a 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -5,7 +5,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { IconModule, Icon } from "../../../../components/src/icon"; import { TypographyModule } from "../../../../components/src/typography"; -import { BitwardenLogo } from "../../icons/bitwarden-logo"; +import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; @Component({ standalone: true, diff --git a/libs/auth/src/angular/anon-layout/anon-layout.mdx b/libs/auth/src/angular/anon-layout/anon-layout.mdx new file mode 100644 index 0000000000..c604c02f03 --- /dev/null +++ b/libs/auth/src/angular/anon-layout/anon-layout.mdx @@ -0,0 +1,118 @@ +import { Meta, Story, Controls } from "@storybook/addon-docs"; + +import * as stories from "./anon-layout.stories"; + + + +# AnonLayout Component + +The Auth-owned AnonLayoutComponent is to be used for unauthenticated pages, where we don't know who +the user is (this includes viewing a Send). + +--- + +### Incorrect Usage ❌ + +The AnonLayoutComponent is **not** to be implemented by every component that uses it in that +component's template directly. For example, if you have a component template called +`example.component.html`, and you want it to use the AnonLayoutComponent, you will **not** be +writing: + +```html + + + +
Example component content
+
+``` + +### Correct Usage ✅ + +Instead the AnonLayoutComponent is implemented solely in the router via routable composition, which +gives us the advantages of nested routes in Angular. + +To allow for routable composition, Auth will also provide a wrapper component in each client, called +AnonLayout**Wrapper**Component. + +For clarity: + +- AnonLayoutComponent = the Auth-owned library component - `` +- AnonLayout**Wrapper**Component = the client-specific wrapper component to be used in a client + routing module + +The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the router outlets: + +```html + + + + + + +``` + +To implement, the developer does not need to work with the base AnonLayoutComponent directly. The +devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for +example) to construct the page via routable composition: + +```javascript +// File: oss-routing.module.ts + +{ + path: "", + component: AnonLayoutWrapperComponent, // Wrapper component + children: [ + { + path: "sample-route", // replace with your route + children: [ + { + path: "", + component: MyPrimaryComponent, // replace with your component + }, + { + path: "", + component: MySecondaryComponent, // replace with your component (or remove this secondary outlet object entirely if not needed) + outlet: "secondary", + }, + ], + data: { + pageTitle: "logIn", // example of a translation key from messages.json + pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json + pageIcon: LockIcon, // example of an icon to pass in + }, + }, + ], + }, +``` + +And if the AnonLayout**Wrapper**Component is already being used in your client's routing module, +then your work will be as simple as just adding another child route under the `children` array. + +### Data Properties + +In the `oss-routing.module.ts` example above, notice the data properties being passed in: + +- For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`. +- For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon + directly. + +All 3 of these properties are optional. + +```javascript +import { LockIcon } from "@bitwarden/auth/angular"; + +// ... + +{ + // ... + data: { + pageTitle: "logIn", + pageSubtitle: "loginWithMasterPassword", + pageIcon: LockIcon, + }, +} +``` + +--- + + diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index daba5b5e53..61a395b155 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -3,12 +3,12 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../../../../components/src/button"; -import { IconLock } from "../../icons/icon-lock"; +import { LockIcon } from "../icons"; import { AnonLayoutComponent } from "./anon-layout.component"; class MockPlatformUtilsService implements Partial { - getApplicationVersion = () => Promise.resolve("Version 2023.1.1"); + getApplicationVersion = () => Promise.resolve("Version 2024.1.1"); } export default { @@ -28,7 +28,7 @@ export default { args: { title: "The Page Title", subtitle: "The subtitle (optional)", - icon: IconLock, + icon: LockIcon, }, } as Meta; @@ -38,14 +38,13 @@ export const WithPrimaryContent: Story = { render: (args) => ({ props: args, template: - /** - * The projected content (i.e. the
) and styling below is just a - * sample and could be replaced with any content and styling - */ + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. ` -
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
+
Primary Projected Content Area (customizable)
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
`, }), @@ -55,15 +54,16 @@ export const WithSecondaryContent: Story = { render: (args) => ({ props: args, template: - // Notice that slot="secondary" is requred to project any secondary content: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + // Notice that slot="secondary" is requred to project any secondary content. ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
+
Secondary Projected Content (optional)
@@ -75,14 +75,16 @@ export const WithSecondaryContent: Story = { export const WithLongContent: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
+
Secondary Projected Content (optional)

Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est?

@@ -95,9 +97,11 @@ export const WithLongContent: Story = { export const WithIcon: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
diff --git a/libs/auth/src/icons/bitwarden-logo.ts b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts similarity index 100% rename from libs/auth/src/icons/bitwarden-logo.ts rename to libs/auth/src/angular/icons/bitwarden-logo.icon.ts diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 7bb3f57579..d71e2e6efd 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1 +1,3 @@ +export * from "./bitwarden-logo.icon"; +export * from "./lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; diff --git a/libs/auth/src/icons/icon-lock.ts b/libs/auth/src/angular/icons/lock.icon.ts similarity index 98% rename from libs/auth/src/icons/icon-lock.ts rename to libs/auth/src/angular/icons/lock.icon.ts index 61330fe0df..b567c213f7 100644 --- a/libs/auth/src/icons/icon-lock.ts +++ b/libs/auth/src/angular/icons/lock.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const IconLock = svgIcon` +export const LockIcon = svgIcon` diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index c93bf1c1d3..067ed63b8e 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -5,6 +5,7 @@ // icons export * from "./icons"; +export * from "./anon-layout/anon-layout.component"; export * from "./fingerprint-dialog/fingerprint-dialog.component"; export * from "./password-callout/password-callout.component"; From c051412d41ebdaedfb9ee271c1c06495aab4b7af Mon Sep 17 00:00:00 2001 From: Anas Date: Tue, 7 May 2024 05:21:19 +0200 Subject: [PATCH 09/18] fix(8702): refreshing organization report page does not display the refreshed page (#8713) --- .../organizations/reporting/reports-home.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 69b9b78819..765637be39 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -23,8 +23,8 @@ export class ReportsHomeComponent implements OnInit { ngOnInit() { this.homepage$ = this.router.events.pipe( filter((event) => event instanceof NavigationEnd), - map((event) => (event as NavigationEnd).urlAfterRedirects.endsWith("/reports")), - startWith(true), + map((event) => this.isReportsHomepageRouteUrl((event as NavigationEnd).urlAfterRedirects)), + startWith(this.isReportsHomepageRouteUrl(this.router.url)), ); this.reports$ = this.route.params.pipe( @@ -61,4 +61,8 @@ export class ReportsHomeComponent implements OnInit { }, ]; } + + private isReportsHomepageRouteUrl(url: string): boolean { + return url.endsWith("/reports"); + } } From be51f1934a34360c5a54faa3d75c3f30ee1b64e9 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 7 May 2024 11:02:50 -0400 Subject: [PATCH 10/18] [AC-1121] Collections Add Access filter and badge (#8404) * added bit toggle group for add access filter to AC collections --- .../vault-collection-row.component.html | 12 ++- .../vault-collection-row.component.ts | 1 + .../vault-items/vault-items.component.html | 6 +- .../vault-items/vault-items.component.ts | 51 +++++++++ .../vault/core/views/collection-admin.view.ts | 29 +++++ .../app/vault/org-vault/vault.component.html | 16 +++ .../app/vault/org-vault/vault.component.ts | 100 ++++++++++++++++-- apps/web/src/locales/en/messages.json | 6 ++ .../vault/models/domain/collection.spec.ts | 1 + .../src/vault/models/view/collection.view.ts | 1 + 10 files changed, 214 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index d03b6dcc38..897d360b4b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -20,7 +20,7 @@ bitLink [disabled]="disabled" type="button" - class="tw-w-full tw-truncate tw-text-start tw-leading-snug" + class="tw-flex tw-w-full tw-text-start tw-leading-snug" linkType="secondary" title="{{ 'viewCollectionWithName' | i18n: collection.name }}" [routerLink]="[]" @@ -28,7 +28,15 @@ queryParamsHandling="merge" appStopProp > - {{ collection.name }} + {{ collection.name }} +
+ {{ "addAccess" | i18n }} +
diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 8bf7779f88..4a9667f8b8 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -21,6 +21,7 @@ import { RowHeightClass } from "./vault-items.component"; }) export class VaultCollectionRowComponent { protected RowHeightClass = RowHeightClass; + protected Unassigned = "unassigned"; @Input() disabled: boolean; @Input() collection: CollectionView; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index c63273fabd..ba69c038fb 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -99,8 +99,12 @@ (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" > + o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user without edit access should not see the Edit option unless that user has "Can Manage" access to a collection + if ( + !collection.manage && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.editAnyCollection + ) { + return false; + } + //Owner/Admin and Custom Users with Edit can see Edit and Access of Orphaned Collections + if ( + collection.addAccess && + collection.id !== Unassigned && + ((organization?.type === OrganizationUserType.Custom && + organization?.permissions.editAnyCollection) || + organization.isAdmin || + organization.isOwner) + ) { + return true; + } + } return collection.canEdit(organization, this.flexibleCollectionsV1Enabled); } @@ -111,6 +136,32 @@ export class VaultItemsComponent { } const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user with only edit access should not see the Delete button for orphaned collections + if ( + collection.addAccess && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.deleteAnyCollection && + organization?.permissions.editAnyCollection + ) { + return false; + } + + // Owner/Admin with no access to a collection will not see Delete + if ( + !collection.assigned && + !collection.addAccess && + (organization.isAdmin || organization.isOwner) && + !( + organization?.type === OrganizationUserType.Custom && + organization?.permissions.deleteAnyCollection + ) + ) { + return false; + } + } + return collection.canDelete(organization); } diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index 2be84b0d24..cc217fc9ce 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -1,3 +1,4 @@ +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -7,6 +8,7 @@ import { CollectionAccessSelectionView } from "../../../admin-console/organizati export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; users: CollectionAccessSelectionView[] = []; + addAccess: boolean; /** * Flag indicating the user has been explicitly assigned to this Collection @@ -31,6 +33,33 @@ export class CollectionAdminView extends CollectionView { this.assigned = response.assigned; } + groupsCanManage() { + if (this.groups.length === 0) { + return this.groups; + } + + const returnedGroups = this.groups.filter((group) => { + if (group.manage) { + return group; + } + }); + return returnedGroups; + } + + usersCanManage(revokedUsers: OrganizationUserUserDetailsResponse[]) { + if (this.users.length === 0) { + return this.users; + } + + const returnedUsers = this.users.filter((user) => { + const isRevoked = revokedUsers.some((revoked) => revoked.id === user.id); + if (user.manage && !isRevoked) { + return user; + } + }); + return returnedUsers; + } + /** * Whether the current user can edit the collection, including user and group access */ diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index f815fccb21..af7b5059e5 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -26,6 +26,20 @@
+ + + {{ "all" | i18n }} + + + + {{ "addAccess" | i18n }} + + {{ trashCleanupWarning }} @@ -54,6 +68,8 @@ [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" + [addAccessStatus]="addAccessStatus$ | async" + [addAccessToggle]="showAddAccessToggle" > 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 243dedef93..4e06f7668c 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -36,6 +36,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -102,6 +105,11 @@ import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; +enum AddAccessStatusType { + All = 0, + AddAccess = 1, +} + @Component({ selector: "app-org-vault", templateUrl: "vault.component.html", @@ -122,6 +130,7 @@ export class VaultComponent implements OnInit, OnDestroy { trashCleanupWarning: string = null; activeFilter: VaultFilter = new VaultFilter(); + protected showAddAccessToggle = false; protected noItemIcon = Icons.Search; protected performingInitialLoad = true; protected refreshing = false; @@ -149,10 +158,12 @@ export class VaultComponent implements OnInit, OnDestroy { protected get flexibleCollectionsV1Enabled(): boolean { return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections; } + protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); + protected addAccessStatus$ = new BehaviorSubject(0); constructor( private route: ActivatedRoute, @@ -181,6 +192,7 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, + private organizationUserService: OrganizationUserService, protected configService: ConfigService, ) {} @@ -241,6 +253,11 @@ export class VaultComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe((activeFilter) => { this.activeFilter = activeFilter; + + // watch the active filters. Only show toggle when viewing the collections filter + if (!this.activeFilter.collectionId) { + this.showAddAccessToggle = false; + } }); this.searchText$ @@ -309,6 +326,10 @@ export class VaultComponent implements OnInit, OnDestroy { const allCiphers$ = organization$.pipe( concatMap(async (organization) => { + // If user swaps organization reset the addAccessToggle + if (!this.showAddAccessToggle || organization) { + this.addAccessToggle(0); + } let ciphers; if (this.flexibleCollectionsV1Enabled) { @@ -348,9 +369,21 @@ export class VaultComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( + // This will be passed into the usersCanManage call + this.orgRevokedUsers = ( + await this.organizationUserService.getAllUsers(await firstValueFrom(organizationId$)) + ).data.filter((user: OrganizationUserUserDetailsResponse) => { + return user.status === -1; + }); + + const collections$ = combineLatest([ + nestedCollections$, + filter$, + this.currentSearchText$, + this.addAccessStatus$, + ]).pipe( filter(([collections, filter]) => collections != undefined && filter != undefined), - concatMap(async ([collections, filter, searchText]) => { + concatMap(async ([collections, filter, searchText, addAccessStatus]) => { if ( filter.collectionId === Unassigned || (filter.collectionId === undefined && filter.type !== undefined) @@ -358,26 +391,30 @@ export class VaultComponent implements OnInit, OnDestroy { return []; } + this.showAddAccessToggle = false; let collectionsToReturn = []; if (filter.collectionId === undefined || filter.collectionId === All) { - collectionsToReturn = collections.map((c) => c.node); + collectionsToReturn = await this.addAccessCollectionsMap(collections); } else { const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( collections, filter.collectionId, ); - collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; + collectionsToReturn = await this.addAccessCollectionsMap(selectedCollection?.children); } if (await this.searchService.isSearchable(searchText)) { collectionsToReturn = this.searchPipe.transform( collectionsToReturn, searchText, - (collection) => collection.name, - (collection) => collection.id, + (collection: CollectionAdminView) => collection.name, + (collection: CollectionAdminView) => collection.id, ); } + if (addAccessStatus === 1 && this.showAddAccessToggle) { + collectionsToReturn = collectionsToReturn.filter((c: any) => c.addAccess); + } return collectionsToReturn; }), takeUntil(this.destroy$), @@ -586,6 +623,57 @@ export class VaultComponent implements OnInit, OnDestroy { ); } + // Update the list of collections to see if any collection is orphaned + // and will receive the addAccess badge / be filterable by the user + async addAccessCollectionsMap(collections: TreeNode[]) { + let mappedCollections; + const { type, allowAdminAccessToAllCollectionItems, permissions } = this.organization; + + const canEditCiphersCheck = + this._flexibleCollectionsV1FlagEnabled && + !this.organization.canEditAllCiphers(this._flexibleCollectionsV1FlagEnabled); + + // This custom type check will show addAccess badge for + // Custom users with canEdit access AND owner/admin manage access setting is OFF + const customUserCheck = + this._flexibleCollectionsV1FlagEnabled && + !allowAdminAccessToAllCollectionItems && + type === OrganizationUserType.Custom && + permissions.editAnyCollection; + + // If Custom user has Delete Only access they will not see Add Access toggle + const customUserOnlyDelete = + this.flexibleCollectionsV1Enabled && + type === OrganizationUserType.Custom && + permissions.deleteAnyCollection && + !permissions.editAnyCollection; + + if (!customUserOnlyDelete && (canEditCiphersCheck || customUserCheck)) { + mappedCollections = collections.map((c: TreeNode) => { + const groupsCanManage = c.node.groupsCanManage(); + const usersCanManage = c.node.usersCanManage(this.orgRevokedUsers); + if ( + groupsCanManage.length === 0 && + usersCanManage.length === 0 && + c.node.id !== Unassigned + ) { + c.node.addAccess = true; + this.showAddAccessToggle = true; + } else { + c.node.addAccess = false; + } + return c.node; + }); + } else { + mappedCollections = collections.map((c: TreeNode) => c.node); + } + return mappedCollections; + } + + addAccessToggle(e: any) { + this.addAccessStatus$.next(e); + } + get loading() { return this.refreshing || this.processingEvent; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4840003abd..f032e822f8 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2788,6 +2788,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, diff --git a/libs/common/src/vault/models/domain/collection.spec.ts b/libs/common/src/vault/models/domain/collection.spec.ts index cd1cab8b42..4ee725be57 100644 --- a/libs/common/src/vault/models/domain/collection.spec.ts +++ b/libs/common/src/vault/models/domain/collection.spec.ts @@ -61,6 +61,7 @@ describe("Collection", () => { const view = await collection.decrypt(); expect(view).toEqual({ + addAccess: false, externalId: "extId", hidePasswords: false, id: "id", diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 86766bdeac..f742b283bd 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject { readOnly: boolean = null; hidePasswords: boolean = null; manage: boolean = null; + addAccess: boolean = false; assigned: boolean = null; constructor(c?: Collection | CollectionAccessDetailsResponse) { From c241aba025bfa75fe589767d766cd53c73f5acb0 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 7 May 2024 10:00:47 -0700 Subject: [PATCH 11/18] [AC-2555] Cipher collections dialog merge fixes (#9036) * [AC-2555] Fix missing feature flags in CollectionsComponent * [AC-2555] Do not filter collections when opening the cipher collections dialog in the org vault --- .../components/vault/collections.component.ts | 3 +++ .../vault/app/vault/collections.component.ts | 3 +++ .../individual-vault/collections.component.ts | 5 +++- .../vault/org-vault/collections.component.ts | 3 +++ .../app/vault/org-vault/vault.component.ts | 23 ++----------------- .../components/collections.component.ts | 6 +++++ 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts index c8f85a8b7a..cb37f0fdad 100644 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault/collections.component.ts @@ -5,6 +5,7 @@ import { first } from "rxjs/operators"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -26,6 +27,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { private route: ActivatedRoute, private location: Location, logService: LogService, + configService: ConfigService, ) { super( collectionService, @@ -34,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, ); } diff --git a/apps/desktop/src/vault/app/vault/collections.component.ts b/apps/desktop/src/vault/app/vault/collections.component.ts index cd08427016..4b6a88f325 100644 --- a/apps/desktop/src/vault/app/vault/collections.component.ts +++ b/apps/desktop/src/vault/app/vault/collections.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -20,6 +21,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { platformUtilsService: PlatformUtilsService, organizationService: OrganizationService, logService: LogService, + configService: ConfigService, ) { super( collectionService, @@ -28,6 +30,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, ); } } diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 6add775b4a..3bf9181905 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -1,8 +1,9 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, OnDestroy, Inject } from "@angular/core"; +import { Component, Inject, OnDestroy } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -23,6 +24,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService: CipherService, organizationSerivce: OrganizationService, logService: LogService, + configService: ConfigService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: CollectionsDialogParams, ) { @@ -33,6 +35,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService, organizationSerivce, logService, + configService, ); this.cipherId = params?.cipherId; } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 67eac2098f..89e4884559 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -4,6 +4,7 @@ import { Component, Inject } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -35,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { organizationService: OrganizationService, private apiService: ApiService, logService: LogService, + configService: ConfigService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams, ) { @@ -45,6 +47,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, dialogRef, params, ); 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 4e06f7668c..f037170dda 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -802,33 +802,14 @@ export class VaultComponent implements OnInit, OnDestroy { const dialog = openOrgVaultCollectionsDialog(this.dialogService, { data: { collectionIds: cipher.collectionIds, - collections: collections.filter((c) => !c.readOnly && c.id != Unassigned), + collections: collections, organization: this.organization, cipherId: cipher.id, }, }); - /** - - const [modal] = await this.modalService.openViewRef( - CollectionsComponent, - this.collectionsModalRef, - (comp) => { - comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled; - comp.collectionIds = cipher.collectionIds; - comp.collections = collections; - comp.organization = this.organization; - comp.cipherId = cipher.id; - comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }, - ); - - */ if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) { - await this.refresh(); + this.refresh(); } } diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 5f8c4145cb..d1f4f93072 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -2,6 +2,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -33,9 +35,13 @@ export class CollectionsComponent implements OnInit { protected cipherService: CipherService, protected organizationService: OrganizationService, private logService: LogService, + private configService: ConfigService, ) {} async ngOnInit() { + this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( + FeatureFlag.FlexibleCollectionsV1, + ); await this.load(); } From de0852431adf467e828da17b0d8038804c1c30dc Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 7 May 2024 13:25:49 -0400 Subject: [PATCH 12/18] [PM-7917] Remove session sync (#9024) * Remove session sync and MemoryStorageService * Fix merge --- .../browser/src/background/main.background.ts | 8 +- .../storage-service.factory.ts | 9 +- .../browser-session.decorator.spec.ts | 88 ----- .../browser-session.decorator.ts | 75 ----- .../session-sync-observable/index.ts | 2 - .../session-storable.ts | 7 - .../session-sync.decorator.spec.ts | 57 ---- .../session-sync.decorator.ts | 54 ---- .../session-syncer.spec.ts | 301 ------------------ .../session-sync-observable/session-syncer.ts | 125 -------- .../sync-item-metadata.ts | 25 -- .../synced-item-metadata.spec.ts | 42 --- .../browser-memory-storage.service.ts | 11 +- .../services/browser-state.service.spec.ts | 7 +- .../services/default-browser-state.service.ts | 7 +- ...cal-backed-session-storage.service.spec.ts | 26 +- .../local-backed-session-storage.service.ts | 19 +- .../background-memory-storage.service.ts | 1 - .../foreground-memory-storage.service.ts | 7 +- ...emory-storage-service-interactions.spec.ts | 4 +- .../src/platform/storage/port-messages.d.ts | 4 +- .../src/popup/services/services.module.ts | 5 +- apps/web/src/app/core/state/state.service.ts | 7 +- libs/angular/src/services/injection-tokens.ts | 7 +- .../platform/abstractions/storage.service.ts | 11 +- .../platform/models/domain/storage-options.ts | 4 - .../services/memory-storage.service.ts | 8 +- .../src/platform/services/state.service.ts | 14 +- .../state/storage/memory-storage.service.ts | 8 +- 29 files changed, 41 insertions(+), 902 deletions(-) delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/index.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts delete mode 100644 apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5cd4113bae..713dfe801c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -84,7 +84,6 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -246,10 +245,9 @@ export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; - memoryStorageService: AbstractMemoryStorageService; - memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; - largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService & - ObservableStorageService; + memoryStorageService: AbstractStorageService; + memoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; + largeObjectMemoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index e63e39944d..764842d751 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -1,5 +1,4 @@ import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -66,9 +65,9 @@ export function sessionStorageServiceFactory( } export function memoryStorageServiceFactory( - cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices, + cache: { memoryStorageService?: AbstractStorageService } & CachedServices, opts: MemoryStorageServiceInitOptions, -): Promise { +): Promise { return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( @@ -97,10 +96,10 @@ export function memoryStorageServiceFactory( export function observableMemoryStorageServiceFactory( cache: { - memoryStorageService?: AbstractMemoryStorageService & ObservableStorageService; + memoryStorageService?: AbstractStorageService & ObservableStorageService; } & CachedServices, opts: MemoryStorageServiceInitOptions, -): Promise { +): Promise { return factory(cache, "memoryStorageService", opts, async () => { return new BackgroundMemoryStorageService(); }); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts deleted file mode 100644 index 2092f6992b..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; - -import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; - -import { browserSession } from "./browser-session.decorator"; -import { SessionStorable } from "./session-storable"; -import { sessionSync } from "./session-sync.decorator"; - -// browserSession initializes SessionSyncers for each sessionSync decorated property -// We don't want to test SessionSyncers, so we'll mock them -jest.mock("./session-syncer"); - -describe("browserSession decorator", () => { - it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => { - @browserSession - class TestClass {} - expect(() => { - new TestClass(); - }).toThrowError( - "Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters", - ); - }); - - it("should create if StateService is a constructor argument", () => { - const stateService = Object.create(DefaultBrowserStateService.prototype, { - memoryStorageService: { - value: Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }), - }, - }); - - @browserSession - class TestClass { - constructor(private stateService: DefaultBrowserStateService) {} - } - - expect(new TestClass(stateService)).toBeDefined(); - }); - - it("should create if MemoryStorageService is a constructor argument", () => { - const memoryStorageService = Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }); - - @browserSession - class TestClass { - constructor(private memoryStorageService: AbstractMemoryStorageService) {} - } - - expect(new TestClass(memoryStorageService)).toBeDefined(); - }); - - describe("interaction with @sessionSync decorator", () => { - let memoryStorageService: MemoryStorageService; - - @browserSession - class TestClass { - @sessionSync({ initializer: (s: string) => s }) - private behaviorSubject = new BehaviorSubject(""); - - constructor(private memoryStorageService: MemoryStorageService) {} - - fromJSON(json: any) { - this.behaviorSubject.next(json); - } - } - - beforeEach(() => { - memoryStorageService = Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }); - }); - - it("should create a session syncer", () => { - const testClass = new TestClass(memoryStorageService) as any as SessionStorable; - expect(testClass.__sessionSyncers.length).toEqual(1); - }); - - it("should initialize the session syncer", () => { - const testClass = new TestClass(memoryStorageService) as any as SessionStorable; - expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts deleted file mode 100644 index 8cf84ef153..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Constructor } from "type-fest"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; - -import { SessionStorable } from "./session-storable"; -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -/** - * Mark the class as syncing state across the browser session. This decorator finds rxjs BehaviorSubject properties - * marked with @sessionSync and syncs these values across the browser session. - * - * @param constructor - * @returns A new constructor that extends the original one to add session syncing. - */ -export function browserSession>(constructor: TCtor) { - return class extends constructor implements SessionStorable { - __syncedItemMetadata: SyncedItemMetadata[]; - __sessionSyncers: SessionSyncer[]; - - constructor(...args: any[]) { - super(...args); - - // Require state service to be injected - const storageService: AbstractMemoryStorageService = this.findStorageService( - [this as any].concat(args), - ); - - if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) { - return; - } - - this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) => - this.buildSyncer(metadata, storageService), - ); - } - - buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) { - const syncer = new SessionSyncer( - (this as any)[metadata.propertyKey], - storageSerice, - metadata, - ); - // 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 - syncer.init(); - return syncer; - } - - findStorageService(args: any[]): AbstractMemoryStorageService { - const storageService = args.find(this.isMemoryStorageService); - - if (storageService) { - return storageService; - } - - const stateService = args.find( - (arg) => - arg?.memoryStorageService != null && - this.isMemoryStorageService(arg.memoryStorageService), - ); - if (stateService) { - return stateService.memoryStorageService; - } - - throw new Error( - `Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`, - ); - } - - isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService { - return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE; - } - }; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/index.ts b/apps/browser/src/platform/decorators/session-sync-observable/index.ts deleted file mode 100644 index c0c547192e..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { browserSession } from "./browser-session.decorator"; -export { sessionSync } from "./session-sync.decorator"; diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts deleted file mode 100644 index f5838b86ef..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -export interface SessionStorable { - __syncedItemMetadata: SyncedItemMetadata[]; - __sessionSyncers: SessionSyncer[]; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts deleted file mode 100644 index 7a6e726608..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { sessionSync } from "./session-sync.decorator"; - -describe("sessionSync decorator", () => { - const initializer = (s: string) => "test"; - class TestClass { - @sessionSync({ initializer: initializer }) - private testProperty = new BehaviorSubject(""); - @sessionSync({ initializer: initializer, initializeAs: "array" }) - private secondTestProperty = new BehaviorSubject(""); - - complete() { - this.testProperty.complete(); - this.secondTestProperty.complete(); - } - } - - it("should add __syncedItemKeys to prototype", () => { - const testClass = new TestClass(); - expect((testClass as any).__syncedItemMetadata).toEqual([ - expect.objectContaining({ - propertyKey: "testProperty", - sessionKey: "testProperty_0", - initializer: initializer, - }), - expect.objectContaining({ - propertyKey: "secondTestProperty", - sessionKey: "secondTestProperty_1", - initializer: initializer, - initializeAs: "array", - }), - ]); - testClass.complete(); - }); - - class TestClass2 { - @sessionSync({ initializer: initializer }) - private testProperty = new BehaviorSubject(""); - - complete() { - this.testProperty.complete(); - } - } - - it("should maintain sessionKey index count for other test classes", () => { - const testClass = new TestClass2(); - expect((testClass as any).__syncedItemMetadata).toEqual([ - expect.objectContaining({ - propertyKey: "testProperty", - sessionKey: "testProperty_2", - initializer: initializer, - }), - ]); - testClass.complete(); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts deleted file mode 100644 index e439cea45a..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { SessionStorable } from "./session-storable"; -import { InitializeOptions } from "./sync-item-metadata"; - -class BuildOptions> { - initializer?: (keyValuePair: TJson) => T; - initializeAs?: InitializeOptions; -} - -// Used to ensure uniqueness for each synced observable -let index = 0; - -/** - * A decorator used to indicate the BehaviorSubject should be synced for this browser session across all contexts. - * - * >**Note** This decorator does nothing if the enclosing class is not decorated with @browserSession. - * - * >**Note** The Behavior subject must be initialized with a default or in the constructor of the class. If it is not, an error will be thrown. - * - * >**!!Warning!!** If the property is overwritten at any time, the new value will not be synced across the browser session. - * - * @param buildOptions - * Builders for the value, requires either a constructor (ctor) for your BehaviorSubject type or an - * initializer function that takes a key value pair representation of the BehaviorSubject data - * and returns your instantiated BehaviorSubject value. `initializeAs can optionally be used to indicate - * the provided initializer function should be used to build an array of values. For example, - * ```ts - * \@sessionSync({ initializer: Foo.fromJSON, initializeAs: 'array' }) - * ``` - * is equivalent to - * ``` - * \@sessionSync({ initializer: (obj: any[]) => obj.map((f) => Foo.fromJSON }) - * ``` - * - * @returns decorator function - */ -export function sessionSync(buildOptions: BuildOptions) { - return (prototype: unknown, propertyKey: string) => { - // Force prototype into SessionStorable and implement it. - const p = prototype as SessionStorable; - - if (p.__syncedItemMetadata == null) { - p.__syncedItemMetadata = []; - } - - p.__syncedItemMetadata.push({ - propertyKey, - sessionKey: `${propertyKey}_${index++}`, - initializer: buildOptions.initializer, - initializeAs: buildOptions.initializeAs ?? "object", - }); - }; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts deleted file mode 100644 index 18f0ceac60..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { awaitAsync } from "@bitwarden/common/../spec/utils"; -import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, ReplaySubject } from "rxjs"; - -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; - -import { BrowserApi } from "../../browser/browser-api"; - -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -describe("session syncer", () => { - const propertyKey = "behaviorSubject"; - const sessionKey = "Test__" + propertyKey; - const metaData: SyncedItemMetadata = { - propertyKey, - sessionKey, - initializer: (s: string) => s, - initializeAs: "object", - }; - let storageService: MockProxy; - let sut: SessionSyncer; - let behaviorSubject: BehaviorSubject; - - beforeEach(() => { - behaviorSubject = new BehaviorSubject(""); - jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({ - name: "bitwarden-test", - version: "0.0.0", - manifest_version: 3, - }); - - storageService = mock(); - storageService.has.mockResolvedValue(false); - sut = new SessionSyncer(behaviorSubject, storageService, metaData); - }); - - afterEach(() => { - jest.resetAllMocks(); - - behaviorSubject.complete(); - }); - - describe("constructor", () => { - it("should throw if subject is not an instance of Subject", () => { - expect(() => { - new SessionSyncer({} as any, storageService, null); - }).toThrowError("subject must inherit from Subject"); - }); - - it("should create if either ctor or initializer is provided", () => { - expect( - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializeAs: "object", - initializer: () => null, - }), - ).toBeDefined(); - expect( - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializer: (s: any) => s, - initializeAs: "object", - }), - ).toBeDefined(); - }); - it("should throw if neither ctor or initializer is provided", () => { - expect(() => { - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializeAs: "object", - initializer: null, - }); - }).toThrowError("initializer must be provided"); - }); - }); - - describe("init", () => { - it("should ignore all updates currently in a ReplaySubject's buffer", () => { - const replaySubject = new ReplaySubject(Infinity); - replaySubject.next("1"); - replaySubject.next("2"); - replaySubject.next("3"); - sut = new SessionSyncer(replaySubject, storageService, metaData); - // block observing the subject - jest.spyOn(sut as any, "observe").mockImplementation(); - - // 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 - sut.init(); - - expect(sut["ignoreNUpdates"]).toBe(3); - }); - - it("should ignore BehaviorSubject's initial value", () => { - const behaviorSubject = new BehaviorSubject("initial"); - sut = new SessionSyncer(behaviorSubject, storageService, metaData); - // block observing the subject - jest.spyOn(sut as any, "observe").mockImplementation(); - - // 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 - sut.init(); - - expect(sut["ignoreNUpdates"]).toBe(1); - }); - - it("should grab an initial value from storage if it exists", async () => { - storageService.has.mockResolvedValue(true); - //Block a call to update - const updateSpy = jest.spyOn(sut as any, "updateFromMemory").mockImplementation(); - - // 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 - sut.init(); - await awaitAsync(); - - expect(updateSpy).toHaveBeenCalledWith(); - }); - - it("should not grab an initial value from storage if it does not exist", async () => { - storageService.has.mockResolvedValue(false); - //Block a call to update - const updateSpy = jest.spyOn(sut as any, "update").mockImplementation(); - - // 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 - sut.init(); - await awaitAsync(); - - expect(updateSpy).not.toHaveBeenCalled(); - }); - }); - - describe("a value is emitted on the observable", () => { - let sendMessageSpy: jest.SpyInstance; - const value = "test"; - const serializedValue = JSON.stringify(value); - - beforeEach(() => { - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); - - // 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 - sut.init(); - - behaviorSubject.next(value); - }); - - it("should update sessionSyncers in other contexts", async () => { - // await finishing of fire-and-forget operation - await new Promise((resolve) => setTimeout(resolve, 100)); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, { - id: sut.id, - serializedValue, - }); - }); - }); - - describe("A message is received", () => { - let nextSpy: jest.SpyInstance; - let sendMessageSpy: jest.SpyInstance; - - beforeEach(() => { - nextSpy = jest.spyOn(behaviorSubject, "next"); - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); - - // 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 - sut.init(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should ignore messages with the wrong command", async () => { - await sut.updateFromMessage({ command: "wrong_command", id: sut.id }); - - expect(storageService.getBypassCache).not.toHaveBeenCalled(); - expect(nextSpy).not.toHaveBeenCalled(); - }); - - it("should ignore messages from itself", async () => { - await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id }); - - expect(storageService.getBypassCache).not.toHaveBeenCalled(); - expect(nextSpy).not.toHaveBeenCalled(); - }); - - it("should update from message on emit from another instance", async () => { - const builder = jest.fn(); - jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); - const value = "test"; - const serializedValue = JSON.stringify(value); - builder.mockReturnValue(value); - - // Expect no circular messaging - await awaitAsync(); - expect(sendMessageSpy).toHaveBeenCalledTimes(0); - - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(storageService.getBypassCache).toHaveBeenCalledTimes(0); - - expect(nextSpy).toHaveBeenCalledTimes(1); - expect(nextSpy).toHaveBeenCalledWith(value); - expect(behaviorSubject.value).toBe(value); - - // Expect no circular messaging - expect(sendMessageSpy).toHaveBeenCalledTimes(0); - }); - }); - - describe("memory storage", () => { - const value = "test"; - const serializedValue = JSON.stringify(value); - let saveSpy: jest.SpyInstance; - const builder = jest.fn().mockReturnValue(value); - const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - const isBackgroundPageSpy = jest.spyOn(BrowserApi, "isBackgroundPage"); - - beforeEach(async () => { - jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); - saveSpy = jest.spyOn(storageService, "save"); - - // 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 - sut.init(); - await awaitAsync(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should always store on observed next for manifest version 3", async () => { - manifestVersionSpy.mockReturnValue(3); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - behaviorSubject.next(value); - await awaitAsync(); - behaviorSubject.next(value); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(2); - }); - - it("should not store on message receive for manifest version 3", async () => { - manifestVersionSpy.mockReturnValue(3); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(0); - }); - - it("should store on message receive for manifest version 2 for background page only", async () => { - manifestVersionSpy.mockReturnValue(2); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(1); - }); - - it("should store on observed next for manifest version 2 for background page only", async () => { - manifestVersionSpy.mockReturnValue(2); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - behaviorSubject.next(value); - await awaitAsync(); - behaviorSubject.next(value); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts deleted file mode 100644 index 6561d5074c..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BehaviorSubject, concatMap, ReplaySubject, skip, Subject, Subscription } from "rxjs"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { BrowserApi } from "../../browser/browser-api"; - -import { SyncedItemMetadata } from "./sync-item-metadata"; - -export class SessionSyncer { - subscription: Subscription; - id = Utils.newGuid(); - - // ignore initial values - private ignoreNUpdates = 0; - - constructor( - private subject: Subject, - private memoryStorageService: AbstractMemoryStorageService, - private metaData: SyncedItemMetadata, - ) { - if (!(subject instanceof Subject)) { - throw new Error("subject must inherit from Subject"); - } - - if (metaData.initializer == null) { - throw new Error("initializer must be provided"); - } - } - - async init() { - switch (this.subject.constructor) { - case ReplaySubject: - // ignore all updates currently in the buffer - this.ignoreNUpdates = (this.subject as any)._buffer.length; - break; - case BehaviorSubject: - this.ignoreNUpdates = 1; - break; - default: - break; - } - - await this.observe(); - // must be synchronous - const hasInSessionMemory = await this.memoryStorageService.has(this.metaData.sessionKey); - if (hasInSessionMemory) { - await this.updateFromMemory(); - } - - this.listenForUpdates(); - } - - private async observe() { - const stream = this.subject.pipe(skip(this.ignoreNUpdates)); - this.ignoreNUpdates = 0; - - // This may be a memory leak. - // There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary - // contexts. If so, this is handled by destruction of the context. - this.subscription = stream - .pipe( - concatMap(async (next) => { - if (this.ignoreNUpdates > 0) { - this.ignoreNUpdates -= 1; - return; - } - await this.updateSession(next); - }), - ) - .subscribe(); - } - - private listenForUpdates() { - // This is an unawaited promise, but it will be executed asynchronously in the background. - BrowserApi.messageListener(this.updateMessageCommand, (message) => { - // 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 - this.updateFromMessage(message); - }); - } - - async updateFromMessage(message: any) { - if (message.command != this.updateMessageCommand || message.id === this.id) { - return; - } - await this.update(message.serializedValue); - } - - async updateFromMemory() { - const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey); - await this.update(value); - } - - async update(serializedValue: any) { - if (!serializedValue) { - return; - } - - const unBuiltValue = JSON.parse(serializedValue); - if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) { - await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); - } - const builder = SyncedItemMetadata.builder(this.metaData); - const value = builder(unBuiltValue); - this.ignoreNUpdates = 1; - this.subject.next(value); - } - - private async updateSession(value: any) { - if (!value) { - return; - } - - const serializedValue = JSON.stringify(value); - if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) { - await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); - } - await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue }); - } - - private get updateMessageCommand() { - return `${this.metaData.sessionKey}_update`; - } -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts deleted file mode 100644 index fe2b393923..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type InitializeOptions = "array" | "record" | "object"; - -export class SyncedItemMetadata { - propertyKey: string; - sessionKey: string; - initializer: (keyValuePair: any) => any; - initializeAs: InitializeOptions; - - static builder(metadata: SyncedItemMetadata): (o: any) => any { - const itemBuilder = metadata.initializer; - if (metadata.initializeAs === "array") { - return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o)); - } else if (metadata.initializeAs === "record") { - return (keyValuePair: any) => { - const record: Record = {}; - for (const key in keyValuePair) { - record[key] = itemBuilder(keyValuePair[key]); - } - return record; - }; - } else { - return (keyValuePair: any) => itemBuilder(keyValuePair); - } - } -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts deleted file mode 100644 index 61eb63eaac..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SyncedItemMetadata } from "./sync-item-metadata"; - -describe("builder", () => { - const propertyKey = "propertyKey"; - const key = "key"; - const initializer = (s: any) => "used initializer"; - - it("should use initializer", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer, - initializeAs: "object", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder({})).toBe("used initializer"); - }); - - it("should honor initialize as array", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer: initializer, - initializeAs: "array", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder([{}])).toBeInstanceOf(Array); - expect(builder([{}])[0]).toBe("used initializer"); - }); - - it("should honor initialize as record", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer: initializer, - initializeAs: "record", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder({ key: "" })).toBeInstanceOf(Object); - expect(builder({ key: "" })).toStrictEqual({ key: "used initializer" }); - }); -}); diff --git a/apps/browser/src/platform/services/browser-memory-storage.service.ts b/apps/browser/src/platform/services/browser-memory-storage.service.ts index b067dc5a12..f824a1df0d 100644 --- a/apps/browser/src/platform/services/browser-memory-storage.service.ts +++ b/apps/browser/src/platform/services/browser-memory-storage.service.ts @@ -1,16 +1,7 @@ -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; - import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service"; -export default class BrowserMemoryStorageService - extends AbstractChromeStorageService - implements AbstractMemoryStorageService -{ +export default class BrowserMemoryStorageService extends AbstractChromeStorageService { constructor() { super(chrome.storage.session); } - type = "MemoryStorageService" as const; - getBypassCache(key: string): Promise { - return this.get(key); - } } diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index a0a52ff622..9077305f44 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -3,10 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { State } from "@bitwarden/common/platform/models/domain/state"; @@ -56,7 +53,7 @@ describe("Browser State Service", () => { }); describe("state methods", () => { - let memoryStorageService: MockProxy; + let memoryStorageService: MockProxy; beforeEach(() => { memoryStorageService = mock(); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index d7bc45bcc3..92da28efa2 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -2,10 +2,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractStorageService, - AbstractMemoryStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; @@ -25,7 +22,7 @@ export class DefaultBrowserStateService constructor( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, + memoryStorageService: AbstractStorageService, logService: LogService, stateFactory: StateFactory, accountService: AccountService, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 7114bda06e..8d43c8f2fe 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -59,24 +59,12 @@ describe("LocalBackedSessionStorage", () => { await sut.get("test"); expect(sut["cache"]["test"]).toEqual("decrypted"); }); - }); - - describe("getBypassCache", () => { - it("ignores cached values", async () => { - sut["cache"]["test"] = "cached"; - const encrypted = makeEncString("encrypted"); - localStorage.internalStore["session_test"] = encrypted.encryptedString; - encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - const result = await sut.getBypassCache("test"); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); - expect(result).toEqual("decrypted"); - }); it("returns a decrypted value when one is stored in local storage", async () => { const encrypted = makeEncString("encrypted"); localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - const result = await sut.getBypassCache("test"); + const result = await sut.get("test"); expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); expect(result).toEqual("decrypted"); }); @@ -85,19 +73,9 @@ describe("LocalBackedSessionStorage", () => { const encrypted = makeEncString("encrypted"); localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - await sut.getBypassCache("test"); + await sut.get("test"); expect(sut["cache"]["test"]).toEqual("decrypted"); }); - - it("deserializes when a deserializer is provided", async () => { - const encrypted = makeEncString("encrypted"); - localStorage.internalStore["session_test"] = encrypted.encryptedString; - encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); - const deserializer = jest.fn().mockReturnValue("deserialized"); - const result = await sut.getBypassCache("test", { deserializer }); - expect(deserializer).toHaveBeenCalledWith("decrypted"); - expect(result).toEqual("deserialized"); - }); }); describe("has", () => { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index c29b9c69dc..2c14ac2833 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,18 +1,16 @@ import { Subject } from "rxjs"; -import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; +import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BrowserApi } from "../browser/browser-api"; @@ -20,7 +18,7 @@ import { MemoryStoragePortMessage } from "../storage/port-messages"; import { portName } from "../storage/port-name"; export class LocalBackedSessionStorageService - extends AbstractMemoryStorageService + extends AbstractStorageService implements ObservableStorageService { private ports: Set = new Set([]); @@ -65,20 +63,12 @@ export class LocalBackedSessionStorageService }); } - async get(key: string, options?: MemoryStorageOptions): Promise { + async get(key: string, options?: StorageOptions): Promise { if (this.cache[key] !== undefined) { return this.cache[key] as T; } - return await this.getBypassCache(key, options); - } - - async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { - let value = await this.getLocalSessionValue(await this.sessionKey.get(), key); - - if (options?.deserializer != null) { - value = options.deserializer(value as Jsonify); - } + const value = await this.getLocalSessionValue(await this.sessionKey.get(), key); this.cache[key] = value; return value as T; @@ -159,7 +149,6 @@ export class LocalBackedSessionStorageService switch (message.action) { case "get": - case "getBypassCache": case "has": { result = await this[message.action](message.key); break; diff --git a/apps/browser/src/platform/storage/background-memory-storage.service.ts b/apps/browser/src/platform/storage/background-memory-storage.service.ts index 9203d2aacb..a1d333affa 100644 --- a/apps/browser/src/platform/storage/background-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/background-memory-storage.service.ts @@ -51,7 +51,6 @@ export class BackgroundMemoryStorageService extends MemoryStorageService { switch (message.action) { case "get": - case "getBypassCache": case "has": { result = await this[message.action](message.key); break; diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index b3ac8de55e..bd6a52c82f 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -1,7 +1,7 @@ import { Observable, Subject, filter, firstValueFrom, map } from "rxjs"; import { - AbstractMemoryStorageService, + AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,7 +11,7 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { MemoryStoragePortMessage } from "./port-messages"; import { portName } from "./port-name"; -export class ForegroundMemoryStorageService extends AbstractMemoryStorageService { +export class ForegroundMemoryStorageService extends AbstractStorageService { private _port: chrome.runtime.Port; private _backgroundResponses$: Observable; private updatesSubject = new Subject(); @@ -59,9 +59,6 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService async get(key: string): Promise { return await this.delegateToBackground("get", key); } - async getBypassCache(key: string): Promise { - return await this.delegateToBackground("getBypassCache", key); - } async has(key: string): Promise { return await this.delegateToBackground("has", key); } diff --git a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts index 43ffb6a065..c462f24269 100644 --- a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts +++ b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts @@ -25,9 +25,9 @@ describe("foreground background memory storage interaction", () => { jest.resetAllMocks(); }); - test.each(["has", "get", "getBypassCache"])( + test.each(["has", "get"])( "background should respond with the correct value for %s", - async (action: "get" | "has" | "getBypassCache") => { + async (action: "get" | "has") => { const key = "key"; const value = "value"; background[action] = jest.fn().mockResolvedValue(value); diff --git a/apps/browser/src/platform/storage/port-messages.d.ts b/apps/browser/src/platform/storage/port-messages.d.ts index a64a9b2ef7..60817c98a4 100644 --- a/apps/browser/src/platform/storage/port-messages.d.ts +++ b/apps/browser/src/platform/storage/port-messages.d.ts @@ -1,5 +1,5 @@ import { - AbstractMemoryStorageService, + AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -14,7 +14,7 @@ type MemoryStoragePortMessage = { data: string | string[] | StorageUpdate; originator: "foreground" | "background"; action?: - | keyof Pick + | keyof Pick | "subject_update" | "initialization"; }; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index ee08ed84b7..7dc79fa01e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -59,7 +59,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -411,7 +410,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, useFactory: ( - regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService, + regularMemoryStorageService: AbstractStorageService & ObservableStorageService, ) => { if (BrowserApi.isManifestVersion(2)) { return regularMemoryStorageService; @@ -439,7 +438,7 @@ const safeProviders: SafeProvider[] = [ useFactory: ( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, + memoryStorageService: AbstractStorageService, logService: LogService, accountService: AccountServiceAbstraction, environmentService: EnvironmentService, diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 185509e150..de47a69555 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -9,10 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -26,7 +23,7 @@ export class StateService extends BaseStateService { constructor( storageService: AbstractStorageService, @Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService, - @Inject(MEMORY_STORAGE) memoryStorageService: AbstractMemoryStorageService, + @Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService, logService: LogService, @Inject(STATE_FACTORY) stateFactory: StateFactory, accountService: AccountService, diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index b7989e7f32..c58931ce55 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -3,7 +3,6 @@ import { Observable, Subject } from "rxjs"; import { ClientType } from "@bitwarden/common/enums"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -24,7 +23,7 @@ export class SafeInjectionToken extends InjectionToken { export const WINDOW = new SafeInjectionToken("WINDOW"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< - AbstractMemoryStorageService & ObservableStorageService + AbstractStorageService & ObservableStorageService >("OBSERVABLE_MEMORY_STORAGE"); export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService @@ -32,9 +31,7 @@ export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken< export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("OBSERVABLE_DISK_LOCAL_STORAGE"); -export const MEMORY_STORAGE = new SafeInjectionToken( - "MEMORY_STORAGE", -); +export const MEMORY_STORAGE = new SafeInjectionToken("MEMORY_STORAGE"); export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); export const LOGOUT_CALLBACK = new SafeInjectionToken< diff --git a/libs/common/src/platform/abstractions/storage.service.ts b/libs/common/src/platform/abstractions/storage.service.ts index f380420c39..390d71ae2a 100644 --- a/libs/common/src/platform/abstractions/storage.service.ts +++ b/libs/common/src/platform/abstractions/storage.service.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { MemoryStorageOptions, StorageOptions } from "../models/domain/storage-options"; +import { StorageOptions } from "../models/domain/storage-options"; export type StorageUpdateType = "save" | "remove"; export type StorageUpdate = { @@ -24,12 +24,3 @@ export abstract class AbstractStorageService { abstract save(key: string, obj: T, options?: StorageOptions): Promise; abstract remove(key: string, options?: StorageOptions): Promise; } - -export abstract class AbstractMemoryStorageService extends AbstractStorageService { - // Used to identify the service in the session sync decorator framework - static readonly TYPE = "MemoryStorageService"; - readonly type = AbstractMemoryStorageService.TYPE; - - abstract get(key: string, options?: MemoryStorageOptions): Promise; - abstract getBypassCache(key: string, options?: MemoryStorageOptions): Promise; -} diff --git a/libs/common/src/platform/models/domain/storage-options.ts b/libs/common/src/platform/models/domain/storage-options.ts index 6ed430ac50..e27628b850 100644 --- a/libs/common/src/platform/models/domain/storage-options.ts +++ b/libs/common/src/platform/models/domain/storage-options.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { HtmlStorageLocation, StorageLocation } from "../../enums"; export type StorageOptions = { @@ -9,5 +7,3 @@ export type StorageOptions = { htmlStorageLocation?: HtmlStorageLocation; keySuffix?: string; }; - -export type MemoryStorageOptions = StorageOptions & { deserializer?: (obj: Jsonify) => T }; diff --git a/libs/common/src/platform/services/memory-storage.service.ts b/libs/common/src/platform/services/memory-storage.service.ts index 9cecee7538..d5debf46cc 100644 --- a/libs/common/src/platform/services/memory-storage.service.ts +++ b/libs/common/src/platform/services/memory-storage.service.ts @@ -1,8 +1,8 @@ import { Subject } from "rxjs"; -import { AbstractMemoryStorageService, StorageUpdate } from "../abstractions/storage.service"; +import { AbstractStorageService, StorageUpdate } from "../abstractions/storage.service"; -export class MemoryStorageService extends AbstractMemoryStorageService { +export class MemoryStorageService extends AbstractStorageService { protected store = new Map(); private updatesSubject = new Subject(); @@ -42,8 +42,4 @@ export class MemoryStorageService extends AbstractMemoryStorageService { this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } - - getBypassCache(key: string): Promise { - return this.get(key); - } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 156a871a2d..aa245f8688 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -14,10 +14,7 @@ import { InitOptions, StateService as StateServiceAbstraction, } from "../abstractions/state.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "../abstractions/storage.service"; +import { AbstractStorageService } from "../abstractions/storage.service"; import { HtmlStorageLocation, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; @@ -61,7 +58,7 @@ export class StateService< constructor( protected storageService: AbstractStorageService, protected secureStorageService: AbstractStorageService, - protected memoryStorageService: AbstractMemoryStorageService, + protected memoryStorageService: AbstractStorageService, protected logService: LogService, protected stateFactory: StateFactory, protected accountService: AccountService, @@ -1111,9 +1108,10 @@ export class StateService< } protected async state(): Promise> { - const state = await this.memoryStorageService.get>(keys.state, { - deserializer: (s) => State.fromJSON(s, this.accountDeserializer), - }); + let state = await this.memoryStorageService.get>(keys.state); + if (this.memoryStorageService.valuesRequireDeserialization) { + state = State.fromJSON(state, this.accountDeserializer); + } return state; } diff --git a/libs/common/src/platform/state/storage/memory-storage.service.ts b/libs/common/src/platform/state/storage/memory-storage.service.ts index 36116f5e4e..ab45c101f9 100644 --- a/libs/common/src/platform/state/storage/memory-storage.service.ts +++ b/libs/common/src/platform/state/storage/memory-storage.service.ts @@ -1,13 +1,13 @@ import { Subject } from "rxjs"; import { - AbstractMemoryStorageService, + AbstractStorageService, ObservableStorageService, StorageUpdate, } from "../../abstractions/storage.service"; export class MemoryStorageService - extends AbstractMemoryStorageService + extends AbstractStorageService implements ObservableStorageService { protected store: Record = {}; @@ -49,8 +49,4 @@ export class MemoryStorageService this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } - - getBypassCache(key: string): Promise { - return this.get(key); - } } From 27d4178287fca5f75fb3dbe48c0e6c294e6f17e1 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 7 May 2024 14:48:23 -0400 Subject: [PATCH 13/18] [PS] fix broken local Storybook (#9075) * add disableRoutesGraph to compdocArgs * fix popup-layout story routes --- angular.json | 10 +++++++++- .../platform/popup/layout/popup-layout.stories.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/angular.json b/angular.json index 4b62c771cb..cdf213e39d 100644 --- a/angular.json +++ b/angular.json @@ -142,7 +142,15 @@ "configDir": ".storybook", "browserTarget": "components:build", "compodoc": true, - "compodocArgs": ["-p", "./tsconfig.json", "-e", "json", "-d", "."], + "compodocArgs": [ + "-p", + "./tsconfig.json", + "-e", + "json", + "-d", + ".", + "--disableRoutesGraph" + ], "port": 6006 } }, diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 77530d06e5..28692c79e1 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -315,13 +315,13 @@ export default { importProvidersFrom( RouterModule.forRoot( [ - { path: "", redirectTo: "vault", pathMatch: "full" }, - { path: "vault", component: MockVaultPageComponent }, - { path: "generator", component: MockGeneratorPageComponent }, - { path: "send", component: MockSendPageComponent }, - { path: "settings", component: MockSettingsPageComponent }, + { path: "", redirectTo: "tabs/vault", pathMatch: "full" }, + { path: "tabs/vault", component: MockVaultPageComponent }, + { path: "tabs/generator", component: MockGeneratorPageComponent }, + { path: "tabs/send", component: MockSendPageComponent }, + { path: "tabs/settings", component: MockSettingsPageComponent }, // in case you are coming from a story that also uses the router - { path: "**", redirectTo: "vault" }, + { path: "**", redirectTo: "tabs/vault" }, ], { useHash: true }, ), From 3a71322510adaa517cc98b773133c8b11f1704f7 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 7 May 2024 12:35:28 -0700 Subject: [PATCH 14/18] [AC-1707] Restrict provider access to items (#8265) * [AC-1707] Add feature flag * [AC-1707] Prevent loading ciphers for provider users in the org vault when the feature flag is enabled * [AC-1707] Ensure new canEditAllCiphers logic only applies to organizations that have FC enabled * [AC-1707] Update editAllCiphers helper to check for restrictProviderAccess feature flag * [AC-1707] Remove un-used vaultFilterComponent reference * [AC-1707] Hide vault filter for providers * [AC-1707] Add search to vault header for provider users * [AC-1707] Hide New Item button for Providers when restrict provider access feature flag is enabled * [AC-1707] Remove leftover debug statement * [AC-1707] Update canEditAllCiphers references to consider the restrictProviderAccessFlag * [AC-1707] Fix collections component changes from main * [AC-1707] Fix some feature flag issues from merge with main * [AC-1707] Avoid 'readonly' collection dialog for providers * [AC-1707] Fix broken Browser component * [AC-1707] Fix broken Desktop component * [AC-1707] Add restrict provider flag to add access badge logic --- .../bulk-delete-dialog.component.ts | 15 +++- .../collections.component.html | 8 +- .../individual-vault/collections.component.ts | 8 +- .../app/vault/org-vault/add-edit.component.ts | 26 +++++- .../vault/org-vault/attachments.component.ts | 33 ++++++-- ...-collection-assignment-dialog.component.ts | 5 +- .../collection-access-restricted.component.ts | 10 ++- .../vault/org-vault/collections.component.ts | 10 ++- .../vault-header/vault-header.component.html | 23 ++++- .../vault-header/vault-header.component.ts | 26 ++++++ .../app/vault/org-vault/vault.component.html | 14 +++- .../app/vault/org-vault/vault.component.ts | 83 +++++++++++++++---- .../src/app/vault/org-vault/vault.module.ts | 3 +- .../components/collections.component.ts | 11 ++- .../vault/components/add-edit.component.ts | 21 ++++- .../models/domain/organization.ts | 24 ++++-- libs/common/src/enums/feature-flag.enum.ts | 2 + .../src/vault/models/view/collection.view.ts | 8 +- 18 files changed, 273 insertions(+), 57 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index a678a05ae3..f49c54ac32 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -1,4 +1,4 @@ -import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { firstValueFrom } from "rxjs"; @@ -56,6 +56,10 @@ export class BulkDeleteDialogComponent { FeatureFlag.FlexibleCollectionsV1, ); + private restrictProviderAccess$ = this.configService.getFeatureFlag$( + FeatureFlag.RestrictProviderAccess, + ); + constructor( @Inject(DIALOG_DATA) params: BulkDeleteDialogParams, private dialogRef: DialogRef, @@ -81,10 +85,11 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; if (this.cipherIds.length) { const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); if ( !this.organization || - !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled) + !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess) ) { deletePromises.push(this.deleteCiphers()); } else { @@ -118,7 +123,11 @@ export class BulkDeleteDialogComponent { private async deleteCiphers(): Promise { const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); - const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled); + const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); + const asAdmin = this.organization?.canEditAllCiphers( + flexibleCollectionsV1Enabled, + restrictProviderAccess, + ); if (this.permanent) { await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); } else { diff --git a/apps/web/src/app/vault/individual-vault/collections.component.html b/apps/web/src/app/vault/individual-vault/collections.component.html index 5adf9c4e58..d9c2145f0b 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -32,7 +32,13 @@ [(ngModel)]="$any(c).checked" name="Collection[{{ i }}].Checked" appStopProp - [disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)" + [disabled]=" + !c.canEditItems( + this.organization, + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess + ) + " /> {{ c.name }} diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 3bf9181905..af9c3476bd 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -50,7 +50,13 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On } check(c: CollectionView, select?: boolean) { - if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) { + if ( + !c.canEditItems( + this.organization, + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return; } (c as any).checked = select == null ? !(c as any).checked : select; diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 01e4dbaadf..82055cc916 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -82,7 +82,12 @@ export class AddEditComponent extends BaseAddEditComponent { } protected loadCollections() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.loadCollections(); } return Promise.resolve(this.collections); @@ -93,7 +98,10 @@ export class AddEditComponent extends BaseAddEditComponent { const firstCipherCheck = await super.loadCipher(); if ( - !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && firstCipherCheck != null ) { return firstCipherCheck; @@ -108,14 +116,24 @@ export class AddEditComponent extends BaseAddEditComponent { } protected encryptCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.encryptCipher(); } return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher); } protected async deleteCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.deleteCipher(); } return this.cipher.isDeleted diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index 2aecf277e6..30189e8021 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -29,6 +29,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On organization: Organization; private flexibleCollectionsV1Enabled = false; + private restrictProviderAccess = false; constructor( cipherService: CipherService, @@ -62,11 +63,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On this.flexibleCollectionsV1Enabled = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); + this.restrictProviderAccess = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess), + ); } protected async reupload(attachment: AttachmentView) { if ( - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.showFixOldAttachments(attachment) ) { await super.reuploadCipherAttachment(attachment, true); @@ -74,7 +81,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On } protected async loadCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -85,12 +97,20 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On return this.cipherService.saveAttachmentWithServer( this.cipherDomain, file, - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled), + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ), ); } protected deleteCipherAttachment(attachmentId: string) { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.deleteCipherAttachment(attachmentId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); @@ -99,7 +119,10 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On protected showFixOldAttachments(attachment: AttachmentView) { return ( attachment.key == null && - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) ); } } diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index e9f8401d73..e13ef49fc3 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -71,9 +71,12 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni async ngOnInit() { const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); + const restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); const org = await this.organizationService.get(this.params.organizationId); - if (org.canEditAllCiphers(v1FCEnabled)) { + if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) { this.editableItems = this.params.ciphers; } else { this.editableItems = this.params.ciphers.filter((c) => c.edit); diff --git a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts index 337d73b315..7a51f01577 100644 --- a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts +++ b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output } from "@angular/core"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components"; @@ -22,12 +22,18 @@ const icon = svgIcon` - {{ "viewCollection" | i18n }} + {{ buttonText | i18n }} `, }) export class CollectionAccessRestrictedComponent { protected icon = icon; + @Input() canEditCollection = false; + @Output() viewCollectionClicked = new EventEmitter(); + + get buttonText() { + return this.canEditCollection ? "editCollection" : "viewCollection"; + } } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 89e4884559..557b048a7b 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -61,7 +61,10 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected async loadCipher() { // if cipher is unassigned use apiService. We can see this by looking at this.collectionIds if ( - !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.collectionIds.length !== 0 ) { return await super.loadCipher(); @@ -86,7 +89,10 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected saveCollections() { if ( - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) || + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) || this.collectionIds.length === 0 ) { const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds); diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 97d99d5821..8388f4ea9d 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -73,8 +73,16 @@ + +
-
+
+ +
diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index a5cd468008..eecd2f434a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -43,6 +43,9 @@ export class VaultHeaderComponent implements OnInit { /** Currently selected collection */ @Input() collection?: TreeNode; + /** The current search text in the header */ + @Input() searchText: string; + /** Emits an event when the new item button is clicked in the header */ @Output() onAddCipher = new EventEmitter(); @@ -55,10 +58,14 @@ export class VaultHeaderComponent implements OnInit { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter(); + /** Emits an event when the search text changes in the header*/ + @Output() searchTextChanged = new EventEmitter(); + protected CollectionDialogTabType = CollectionDialogTabType; protected organizations$ = this.organizationService.organizations$; private flexibleCollectionsV1Enabled = false; + private restrictProviderAccessFlag = false; constructor( private organizationService: OrganizationService, @@ -73,6 +80,9 @@ export class VaultHeaderComponent implements OnInit { this.flexibleCollectionsV1Enabled = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); + this.restrictProviderAccessFlag = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); } get title() { @@ -197,7 +207,23 @@ export class VaultHeaderComponent implements OnInit { return this.collection.node.canDelete(this.organization); } + get canCreateCollection(): boolean { + return this.organization?.canCreateNewCollections; + } + + get canCreateCipher(): boolean { + if (this.organization?.isProviderUser && this.restrictProviderAccessFlag) { + return false; + } + return true; + } + deleteCollection() { this.onDeleteCollection.emit(); } + + onSearchTextChanged(t: string) { + this.searchText = t; + this.searchTextChanged.emit(t); + } } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index af7b5059e5..096389021f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -3,19 +3,20 @@ [loading]="refreshing" [organization]="organization" [collection]="selectedCollection" + [searchText]="currentSearchText$ | async" (onAddCipher)="addCipher()" (onAddCollection)="addCollection()" (onEditCollection)="editCollection(selectedCollection.node, $event.tab)" (onDeleteCollection)="deleteCollection(selectedCollection.node)" + (searchTextChanged)="filterSearchText($event)" >
-
+
-
+
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 f037170dda..103b29fad7 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -100,7 +100,6 @@ import { BulkCollectionsDialogResult, } from "./bulk-collections-dialog"; import { openOrgVaultCollectionsDialog } from "./collections.component"; -import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; @@ -118,8 +117,6 @@ enum AddAccessStatusType { export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; - @ViewChild("vaultFilter", { static: true }) - vaultFilterComponent: VaultFilterComponent; @ViewChild("attachments", { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef; @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) @@ -151,6 +148,10 @@ export class VaultComponent implements OnInit, OnDestroy { protected showMissingCollectionPermissionMessage: boolean; protected showCollectionAccessRestricted: boolean; protected currentSearchText$: Observable; + /** + * A list of collections that the user can assign items to and edit those items within. + * @protected + */ protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; private _flexibleCollectionsV1FlagEnabled: boolean; @@ -160,6 +161,11 @@ export class VaultComponent implements OnInit, OnDestroy { } protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; + private _restrictProviderAccessFlagEnabled: boolean; + protected get restrictProviderAccessEnabled(): boolean { + return this._restrictProviderAccessFlagEnabled && this.flexibleCollectionsV1Enabled; + } + private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -207,6 +213,10 @@ export class VaultComponent implements OnInit, OnDestroy { FeatureFlag.FlexibleCollectionsV1, ); + this._restrictProviderAccessFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); + const filter$ = this.routedVaultFilterService.filter$; const organizationId$ = filter$.pipe( map((filter) => filter.organizationId), @@ -297,10 +307,20 @@ export class VaultComponent implements OnInit, OnDestroy { this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe( map((collections) => { - // Users that can edit all ciphers can implicitly edit all collections - if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + // If restricted, providers can not add items to any collections or edit those items + if (this.organization.isProviderUser && this.restrictProviderAccessEnabled) { + return []; + } + // Users that can edit all ciphers can implicitly add to / edit within any collection + if ( + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { return collections; } + // The user is only allowed to add/edit items to assigned collections that are not readonly return collections.filter((c) => c.assigned && !c.readOnly); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -332,10 +352,19 @@ export class VaultComponent implements OnInit, OnDestroy { } let ciphers; + if (organization.isProviderUser && this.restrictProviderAccessEnabled) { + return []; + } + if (this.flexibleCollectionsV1Enabled) { // Flexible collections V1 logic. // If the user can edit all ciphers for the organization then fetch them ALL. - if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { // Otherwise, only fetch ciphers they have access to (includes unassigned for admins). @@ -343,7 +372,12 @@ export class VaultComponent implements OnInit, OnDestroy { } } else { // Pre-flexible collections logic, to be removed after flexible collections is fully released - if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { ciphers = (await this.cipherService.getAllDecrypted()).filter( @@ -443,9 +477,17 @@ export class VaultComponent implements OnInit, OnDestroy { organization$, ]).pipe( map(([filter, collection, organization]) => { + if (organization.isProviderUser && this.restrictProviderAccessEnabled) { + return collection != undefined || filter.collectionId === Unassigned; + } + return ( - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || - (!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + (filter.collectionId === Unassigned && + !organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) || + (!organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) && collection != undefined && !collection.node.assigned) ); @@ -490,7 +532,8 @@ export class VaultComponent implements OnInit, OnDestroy { map(([filter, collection, organization]) => { return ( // Filtering by unassigned, show message if not admin - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || + (filter.collectionId === Unassigned && + !organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) || // Filtering by a collection, so show message if user is not assigned (collection != undefined && !collection.node.assigned && @@ -513,7 +556,7 @@ export class VaultComponent implements OnInit, OnDestroy { if (this.flexibleCollectionsV1Enabled) { canEditCipher = - organization.canEditAllCiphers(true) || + organization.canEditAllCiphers(true, this.restrictProviderAccessEnabled) || (await firstValueFrom(allCipherMap$))[cipherId] != undefined; } else { canEditCipher = @@ -631,7 +674,10 @@ export class VaultComponent implements OnInit, OnDestroy { const canEditCiphersCheck = this._flexibleCollectionsV1FlagEnabled && - !this.organization.canEditAllCiphers(this._flexibleCollectionsV1FlagEnabled); + !this.organization.canEditAllCiphers( + this._flexibleCollectionsV1FlagEnabled, + this.restrictProviderAccessEnabled, + ); // This custom type check will show addAccess badge for // Custom users with canEdit access AND owner/admin manage access setting is OFF @@ -780,13 +826,13 @@ export class VaultComponent implements OnInit, OnDestroy { map((c) => { return c.sort((a, b) => { if ( - a.canEditItems(this.organization, true) && - !b.canEditItems(this.organization, true) + a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) && + !b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) ) { return -1; } else if ( - !a.canEditItems(this.organization, true) && - b.canEditItems(this.organization, true) + !a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) && + b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) ) { return 1; } else { @@ -1247,7 +1293,10 @@ export class VaultComponent implements OnInit, OnDestroy { } protected deleteCipherWithServer(id: string, permanent: boolean) { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ); return permanent ? this.cipherService.deleteWithServer(id, asAdmin) : this.cipherService.softDeleteWithServer(id, asAdmin); diff --git a/apps/web/src/app/vault/org-vault/vault.module.ts b/apps/web/src/app/vault/org-vault/vault.module.ts index 47365bb4b1..a478307123 100644 --- a/apps/web/src/app/vault/org-vault/vault.module.ts +++ b/apps/web/src/app/vault/org-vault/vault.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components"; +import { BreadcrumbsModule, NoItemsModule, SearchModule } from "@bitwarden/components"; import { LooseComponentsModule } from "../../shared/loose-components.module"; import { SharedModule } from "../../shared/shared.module"; @@ -32,6 +32,7 @@ import { VaultComponent } from "./vault.component"; CollectionDialogModule, CollectionAccessRestrictedComponent, NoItemsModule, + SearchModule, ], declarations: [VaultComponent, VaultHeaderComponent], exports: [VaultComponent], diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index d1f4f93072..445727ac61 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -25,6 +25,7 @@ export class CollectionsComponent implements OnInit { collections: CollectionView[] = []; organization: Organization; flexibleCollectionsV1Enabled: boolean; + restrictProviderAccess: boolean; protected cipherDomain: Cipher; @@ -42,6 +43,9 @@ export class CollectionsComponent implements OnInit { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, ); + this.restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); await this.load(); } @@ -68,7 +72,12 @@ export class CollectionsComponent implements OnInit { async submit(): Promise { const selectedCollectionIds = this.collections .filter((c) => { - if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return !!(c as any).checked; } else { return !!(c as any).checked && c.readOnly == null; diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 0397a7a663..74c368d726 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -91,6 +91,7 @@ export class AddEditComponent implements OnInit, OnDestroy { private previousCipherId: string; protected flexibleCollectionsV1Enabled = false; + protected restrictProviderAccess = false; get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -183,6 +184,9 @@ export class AddEditComponent implements OnInit, OnDestroy { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, ); + this.restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -668,11 +672,14 @@ export class AddEditComponent implements OnInit, OnDestroy { protected saveCipher(cipher: Cipher) { const isNotClone = this.editMode && !this.cloneMode; - let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + let orgAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection if (!cipher.collectionIds) { - orgAdmin = this.organization?.canEditUnassignedCiphers(); + orgAdmin = this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess); } return this.cipher.id == null @@ -681,14 +688,20 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected deleteCipher() { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id, asAdmin) : this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); } protected restoreCipher() { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); return this.cipherService.restoreWithServer(this.cipher.id, asAdmin); } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index bdf0b8fbbf..04840477df 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -203,22 +203,32 @@ export class Organization { ); } - canEditUnassignedCiphers() { - // TODO: Update this to exclude Providers if provider access is restricted in AC-1707 + canEditUnassignedCiphers(restrictProviderAccessFlagEnabled: boolean) { + if (this.isProviderUser) { + return !restrictProviderAccessFlagEnabled; + } return this.isAdmin || this.permissions.editAnyCollection; } - canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) { + canEditAllCiphers( + flexibleCollectionsV1Enabled: boolean, + restrictProviderAccessFlagEnabled: boolean, + ) { // Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers - if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) { + if (!this.flexibleCollections || !flexibleCollectionsV1Enabled || !this.flexibleCollections) { return this.isAdmin || this.permissions.editAnyCollection; } + + if (this.isProviderUser) { + return !restrictProviderAccessFlagEnabled; + } + // Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins - // Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag + // Custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag return ( - this.isProviderUser || (this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) || - (this.allowAdminAccessToAllCollectionItems && this.isAdmin) + (this.allowAdminAccessToAllCollectionItems && + (this.type === OrganizationUserType.Admin || this.type === OrganizationUserType.Owner)) ); } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 5ed3724f2f..221b251f3c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -17,6 +17,7 @@ export enum FeatureFlag { UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", + RestrictProviderAccess = "restrict-provider-access", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -44,6 +45,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.UnassignedItemsBanner]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, + [FeatureFlag.RestrictProviderAccess]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index f742b283bd..ebc0229f4e 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -39,7 +39,11 @@ export class CollectionView implements View, ITreeNodeObject { } } - canEditItems(org: Organization, v1FlexibleCollections: boolean): boolean { + canEditItems( + org: Organization, + v1FlexibleCollections: boolean, + restrictProviderAccess: boolean, + ): boolean { if (org != null && org.id !== this.organizationId) { throw new Error( "Id of the organization provided does not match the org id of the collection.", @@ -48,7 +52,7 @@ export class CollectionView implements View, ITreeNodeObject { if (org?.flexibleCollections) { return ( - org?.canEditAllCiphers(v1FlexibleCollections) || + org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) || this.manage || (this.assigned && !this.readOnly) ); From 29bd03e64ea3d88109308d44547450a65c654d31 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 7 May 2024 22:23:03 +0200 Subject: [PATCH 15/18] [PM-7172] Create account security settings component (navigational changes) (#8817) * Move about.component into tools ownership * Split out account security settings Move settings.component.ts to auth/popup/settings and rename to account-security.component.ts Move controls from settings.component.html and create account-security.component.html Move settings.component.html to tools/popup/settings.component.html Create settings.component.ts under tools/popup/settings Fixup module imports and routing Add new strings to en/message.json * Move vault-timeout-input.component to auth * Move await-desktop-dialog.component to auth * Add transition for account-security --------- Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 12 ++ .../settings/account-security.component.html | 140 ++++++++++++++++++ .../settings/account-security.component.ts} | 109 +++----------- .../await-desktop-dialog.component.html | 0 .../await-desktop-dialog.component.ts | 0 .../vault-timeout-input.component.html | 0 .../settings/vault-timeout-input.component.ts | 0 .../src/popup/app-routing.animations.ts | 3 + apps/browser/src/popup/app-routing.module.ts | 9 +- apps/browser/src/popup/app.module.ts | 6 +- .../settings/about}/about.component.html | 0 .../popup/settings/about}/about.component.ts | 0 .../popup/settings/settings.component.html | 125 ++-------------- .../popup/settings/settings.component.ts | 101 +++++++++++++ 14 files changed, 300 insertions(+), 205 deletions(-) create mode 100644 apps/browser/src/auth/popup/settings/account-security.component.html rename apps/browser/src/{popup/settings/settings.component.ts => auth/popup/settings/account-security.component.ts} (83%) rename apps/browser/src/{ => auth}/popup/settings/await-desktop-dialog.component.html (100%) rename apps/browser/src/{ => auth}/popup/settings/await-desktop-dialog.component.ts (100%) rename apps/browser/src/{ => auth}/popup/settings/vault-timeout-input.component.html (100%) rename apps/browser/src/{ => auth}/popup/settings/vault-timeout-input.component.ts (100%) rename apps/browser/src/{popup/settings => tools/popup/settings/about}/about.component.html (100%) rename apps/browser/src/{popup/settings => tools/popup/settings/about}/about.component.ts (100%) rename apps/browser/src/{ => tools}/popup/settings/settings.component.html (55%) create mode 100644 apps/browser/src/tools/popup/settings/settings.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index bd62b825e7..493a909f8a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -3023,6 +3032,9 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html new file mode 100644 index 0000000000..dff9675743 --- /dev/null +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -0,0 +1,140 @@ + +
+ +
+

+ {{ "accountSecurity" | i18n }} +

+
+ +
+
+
+
+

{{ "unlockMethods" | i18n }}

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

{{ "sessionTimeoutHeader" | i18n }}

+
+ + + {{ + "vaultTimeoutPolicyWithActionInEffect" + | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) + }} + + + {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} + + + {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} + + + + +
+ + +
+ +
+
+
+

{{ "otherOptions" | i18n }}

+
+ + + + + +
+
+
diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts similarity index 83% rename from apps/browser/src/popup/settings/settings.component.ts rename to apps/browser/src/auth/popup/settings/account-security.component.ts index c7e5b7dc95..88365e7b47 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -23,7 +22,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -34,35 +32,20 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { DialogService } from "@bitwarden/components"; -import { SetPinComponent } from "../../auth/popup/components/set-pin.component"; -import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../platform/flags"; -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { enableAccountSwitching } from "../../../platform/flags"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; +import { SetPinComponent } from "../components/set-pin.component"; -import { AboutComponent } from "./about.component"; import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; -const RateUrls = { - [DeviceType.ChromeExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.FirefoxExtension]: - "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", - [DeviceType.OperaExtension]: - "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", - [DeviceType.EdgeExtension]: - "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", - [DeviceType.VivaldiExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", -}; - @Component({ - selector: "app-settings", - templateUrl: "settings.component.html", + selector: "auth-account-security", + templateUrl: "account-security.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SettingsComponent implements OnInit { +export class AccountSecurityComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; availableVaultTimeoutActions: VaultTimeoutAction[] = []; @@ -95,7 +78,6 @@ export class SettingsComponent implements OnInit { private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, public messagingService: MessagingService, - private router: Router, private environmentService: EnvironmentService, private cryptoService: CryptoService, private stateService: StateService, @@ -425,23 +407,6 @@ export class SettingsComponent implements OnInit { ); } - async lock() { - await this.vaultTimeoutService.lock(); - } - - async logOut() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - type: "info", - }); - - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (confirmed) { - this.messagingService.send("logout", { userId: userId }); - } - } - async changePassword() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "continueToWebApp" }, @@ -468,44 +433,6 @@ export class SettingsComponent implements OnInit { } } - async share() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "learnOrg" }, - content: { key: "learnOrgConfirmation" }, - type: "info", - }); - if (confirmed) { - // 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 - BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/"); - } - } - - async webVault() { - const env = await firstValueFrom(this.environmentService.environment$); - const url = env.getWebVaultUrl(); - await BrowserApi.createNewTab(url); - } - - async import() { - await this.router.navigate(["/import"]); - if (await BrowserApi.isPopupOpen()) { - // 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 - BrowserPopupUtils.openCurrentPagePopout(window); - } - } - - export() { - // 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 - this.router.navigate(["/export"]); - } - - about() { - this.dialogService.open(AboutComponent); - } - async fingerprint() { const fingerprint = await this.cryptoService.getFingerprint( await this.stateService.getUserId(), @@ -518,11 +445,21 @@ export class SettingsComponent implements OnInit { return firstValueFrom(dialogRef.closed); } - rate() { - const deviceType = this.platformUtilsService.getDevice(); - // 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 - BrowserApi.createNewTab((RateUrls as any)[deviceType]); + async lock() { + await this.vaultTimeoutService.lock(); + } + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + type: "info", + }); + + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (confirmed) { + this.messagingService.send("logout", { userId: userId }); + } } ngOnDestroy() { diff --git a/apps/browser/src/popup/settings/await-desktop-dialog.component.html b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.html similarity index 100% rename from apps/browser/src/popup/settings/await-desktop-dialog.component.html rename to apps/browser/src/auth/popup/settings/await-desktop-dialog.component.html diff --git a/apps/browser/src/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts similarity index 100% rename from apps/browser/src/popup/settings/await-desktop-dialog.component.ts rename to apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts diff --git a/apps/browser/src/popup/settings/vault-timeout-input.component.html b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.html similarity index 100% rename from apps/browser/src/popup/settings/vault-timeout-input.component.html rename to apps/browser/src/auth/popup/settings/vault-timeout-input.component.html diff --git a/apps/browser/src/popup/settings/vault-timeout-input.component.ts b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts similarity index 100% rename from apps/browser/src/popup/settings/vault-timeout-input.component.ts rename to apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 13403545fd..e37c640bf9 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -174,6 +174,9 @@ export const routerTransition = trigger("routerTransition", [ transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft), transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight), + transition("tabs => account-security", inSlideLeft), + transition("account-security => tabs", outSlideRight), + transition("tabs => import", inSlideLeft), transition("import => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 0dcf496457..059e2e605d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -21,6 +21,7 @@ import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; +import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; @@ -35,6 +36,7 @@ import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.compo import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; +import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; @@ -53,7 +55,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component" import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -246,6 +247,12 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "autofill" }, }, + { + path: "account-security", + component: AccountSecurityComponent, + canActivate: [AuthGuard], + data: { state: "account-security" }, + }, { path: "folders", component: FoldersComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index bed40dfddc..40cdd29754 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -30,6 +30,8 @@ import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; +import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; +import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; @@ -49,6 +51,7 @@ import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.componen import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; +import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component"; @@ -77,9 +80,7 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component" import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; -import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -156,6 +157,7 @@ import "../platform/popup/locales"; SendListComponent, SendTypeComponent, SetPasswordComponent, + AccountSecurityComponent, SettingsComponent, ShareComponent, SsoComponent, diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/tools/popup/settings/about/about.component.html similarity index 100% rename from apps/browser/src/popup/settings/about.component.html rename to apps/browser/src/tools/popup/settings/about/about.component.html diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/tools/popup/settings/about/about.component.ts similarity index 100% rename from apps/browser/src/popup/settings/about.component.ts rename to apps/browser/src/tools/popup/settings/about/about.component.ts diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html similarity index 55% rename from apps/browser/src/popup/settings/settings.component.html rename to apps/browser/src/tools/popup/settings/settings.component.html index 98c218b0db..0b7773019b 100644 --- a/apps/browser/src/popup/settings/settings.component.html +++ b/apps/browser/src/tools/popup/settings/settings.component.html @@ -7,10 +7,18 @@
-
+

{{ "manage" | i18n }}

+
-
-

{{ "security" | i18n }}

-
- - - {{ - "vaultTimeoutPolicyWithActionInEffect" - | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) - }} - - - {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} - - - {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} - - - - -
- - -
- -
- - -
-
- - -
-
- - -
- - -
-

{{ "account" | i18n }}

@@ -145,35 +67,6 @@
- - -
diff --git a/apps/browser/src/tools/popup/settings/settings.component.ts b/apps/browser/src/tools/popup/settings/settings.component.ts new file mode 100644 index 0000000000..81727c442c --- /dev/null +++ b/apps/browser/src/tools/popup/settings/settings.component.ts @@ -0,0 +1,101 @@ +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom, Subject } from "rxjs"; + +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +import { AboutComponent } from "./about/about.component"; + +const RateUrls = { + [DeviceType.ChromeExtension]: + "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.FirefoxExtension]: + "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", + [DeviceType.OperaExtension]: + "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", + [DeviceType.EdgeExtension]: + "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", + [DeviceType.VivaldiExtension]: + "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", +}; + +@Component({ + selector: "tools-settings", + templateUrl: "settings.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class SettingsComponent implements OnInit { + private destroy$ = new Subject(); + + constructor( + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private vaultTimeoutService: VaultTimeoutService, + public messagingService: MessagingService, + private router: Router, + private environmentService: EnvironmentService, + private dialogService: DialogService, + ) {} + + async ngOnInit() {} + + async share() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "learnOrg" }, + content: { key: "learnOrgConfirmation" }, + type: "info", + }); + if (confirmed) { + // 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 + BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/"); + } + } + + async webVault() { + const env = await firstValueFrom(this.environmentService.environment$); + const url = env.getWebVaultUrl(); + await BrowserApi.createNewTab(url); + } + + async import() { + await this.router.navigate(["/import"]); + if (await BrowserApi.isPopupOpen()) { + // 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 + BrowserPopupUtils.openCurrentPagePopout(window); + } + } + + export() { + // 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 + this.router.navigate(["/export"]); + } + + about() { + this.dialogService.open(AboutComponent); + } + + rate() { + const deviceType = this.platformUtilsService.getDevice(); + // 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 + BrowserApi.createNewTab((RateUrls as any)[deviceType]); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} From 7bb37877eeb001a0e7302c1b3a22310e4fb12df7 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 7 May 2024 23:04:17 +0200 Subject: [PATCH 16/18] Fix test after session-removal with https://github.com/bitwarden/clients/pull/9024 (#9076) Co-authored-by: Daniel James Smith --- .../src/platform/services/browser-state.service.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 9077305f44..506f185b64 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -15,9 +15,6 @@ import { Account } from "../../models/account"; import { DefaultBrowserStateService } from "./default-browser-state.service"; -// disable session syncing to just test class -jest.mock("../decorators/session-sync-observable/"); - describe("Browser State Service", () => { let secureStorageService: MockProxy; let diskStorageService: MockProxy; From ea7d1ff6ed79bf6854647d2e84138fae204c00f4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 7 May 2024 17:43:42 -0400 Subject: [PATCH 17/18] Handle error object for biometric lock (#9070) --- apps/browser/src/auth/popup/lock.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 86352e2c82..782e37b864 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -143,15 +143,17 @@ export class LockComponent extends BaseLockComponent { try { success = await super.unlockBiometric(); } catch (e) { - const error = BiometricErrors[e as BiometricErrorTypes]; + const error = BiometricErrors[e?.message as BiometricErrorTypes]; if (error == null) { this.logService.error("Unknown error: " + e); + return false; } this.biometricError = this.i18nService.t(error.description); + } finally { + this.pendingBiometric = false; } - this.pendingBiometric = false; return success; } From 5682e38384edbed20963b43a67f7d57b73376400 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 7 May 2024 23:58:31 +0200 Subject: [PATCH 18/18] [PM-7175] Create vault settings component (navigational changes) (#8840) * Move about.component into tools ownership * Split out account security settings Move settings.component.ts to auth/popup/settings and rename to account-security.component.ts Move controls from settings.component.html and create account-security.component.html Move settings.component.html to tools/popup/settings.component.html Create settings.component.ts under tools/popup/settings Fixup module imports and routing Add new strings to en/message.json * Move vault-timeout-input.component to auth * Move await-desktop-dialog.component to auth * Move folder.component to vault/popup/settings * Move sync.component to vault/popup/settings * Create vault settings component Move controls from settings.component.html to vault-settings.component.html Register VaultSettingsComponent within app.module Register route for VaultSettingsComponent Add new string in en/messages.json * Fix routing for back navigation on child pages of vault settings * Add transitions to vault-settings sub-pages * Add transition for account-security * Add an await to popping out the extension * Use "Vault" instead of "Vault settings" as title --------- Co-authored-by: Daniel James Smith --- .../src/popup/app-routing.animations.ts | 20 ++++--- apps/browser/src/popup/app-routing.module.ts | 11 +++- apps/browser/src/popup/app.module.ts | 6 +- .../popup/settings/export.component.html | 2 +- .../import/import-browser.component.html | 2 +- .../popup/settings/settings.component.html | 33 +---------- .../popup/settings/folders.component.html | 2 +- .../popup/settings/folders.component.ts | 0 .../popup/settings/sync.component.html | 2 +- .../popup/settings/sync.component.ts | 0 .../settings/vault-settings.component.html | 56 +++++++++++++++++++ .../settings/vault-settings.component.ts | 25 +++++++++ 12 files changed, 112 insertions(+), 47 deletions(-) rename apps/browser/src/{ => vault}/popup/settings/folders.component.html (95%) rename apps/browser/src/{ => vault}/popup/settings/folders.component.ts (100%) rename apps/browser/src/{ => vault}/popup/settings/sync.component.html (94%) rename apps/browser/src/{ => vault}/popup/settings/sync.component.ts (100%) create mode 100644 apps/browser/src/vault/popup/settings/vault-settings.component.html create mode 100644 apps/browser/src/vault/popup/settings/vault-settings.component.ts diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index e37c640bf9..9bad33f744 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -177,20 +177,24 @@ export const routerTransition = trigger("routerTransition", [ transition("tabs => account-security", inSlideLeft), transition("account-security => tabs", outSlideRight), - transition("tabs => import", inSlideLeft), - transition("import => tabs", outSlideRight), + // Vault settings + transition("tabs => vault-settings", inSlideLeft), + transition("vault-settings => tabs", outSlideRight), - transition("tabs => export", inSlideLeft), - transition("export => tabs", outSlideRight), + transition("vault-settings => import", inSlideLeft), + transition("import => vault-settings", outSlideRight), - transition("tabs => folders", inSlideLeft), - transition("folders => tabs", outSlideRight), + transition("vault-settings => export", inSlideLeft), + transition("export => vault-settings", outSlideRight), + + transition("vault-settings => folders", inSlideLeft), + transition("folders => vault-settings", outSlideRight), transition("folders => edit-folder, folders => add-folder", inSlideUp), transition("edit-folder => folders, add-folder => folders", outSlideDown), - transition("tabs => sync", inSlideLeft), - transition("sync => tabs", outSlideRight), + transition("vault-settings => sync", inSlideLeft), + transition("sync => vault-settings", outSlideRight), transition("tabs => excluded-domains", inSlideLeft), transition("excluded-domains => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 059e2e605d..c4e9acbd75 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -48,14 +48,15 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; -import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SyncComponent } from "./settings/sync.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -253,6 +254,12 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "account-security" }, }, + { + path: "vault-settings", + component: VaultSettingsComponent, + canActivate: [AuthGuard], + data: { state: "vault-settings" }, + }, { path: "folders", component: FoldersComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 40cdd29754..71e6ed4f17 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -70,6 +70,9 @@ import { VaultSelectComponent } from "../vault/popup/components/vault/vault-sele import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; @@ -77,10 +80,8 @@ import { PopOutComponent } from "./components/pop-out.component"; import { UserVerificationComponent } from "./components/user-verification.component"; import { ServicesModule } from "./services/services.module"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; -import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { SyncComponent } from "./settings/sync.component"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -159,6 +160,7 @@ import "../platform/popup/locales"; SetPasswordComponent, AccountSecurityComponent, SettingsComponent, + VaultSettingsComponent, ShareComponent, SsoComponent, SyncComponent, diff --git a/apps/browser/src/tools/popup/settings/export.component.html b/apps/browser/src/tools/popup/settings/export.component.html index aae3584f6c..1b2ea1eb1d 100644 --- a/apps/browser/src/tools/popup/settings/export.component.html +++ b/apps/browser/src/tools/popup/settings/export.component.html @@ -1,7 +1,7 @@
- diff --git a/apps/browser/src/tools/popup/settings/import/import-browser.component.html b/apps/browser/src/tools/popup/settings/import/import-browser.component.html index df4f3f09aa..67b5eb348a 100644 --- a/apps/browser/src/tools/popup/settings/import/import-browser.component.html +++ b/apps/browser/src/tools/popup/settings/import/import-browser.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/tools/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html index 0b7773019b..71f4f1b991 100644 --- a/apps/browser/src/tools/popup/settings/settings.component.html +++ b/apps/browser/src/tools/popup/settings/settings.component.html @@ -30,17 +30,9 @@ - - diff --git a/apps/browser/src/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts similarity index 100% rename from apps/browser/src/popup/settings/folders.component.ts rename to apps/browser/src/vault/popup/settings/folders.component.ts diff --git a/apps/browser/src/popup/settings/sync.component.html b/apps/browser/src/vault/popup/settings/sync.component.html similarity index 94% rename from apps/browser/src/popup/settings/sync.component.html rename to apps/browser/src/vault/popup/settings/sync.component.html index 6743f12a1a..6d0a1c31a8 100644 --- a/apps/browser/src/popup/settings/sync.component.html +++ b/apps/browser/src/vault/popup/settings/sync.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/popup/settings/sync.component.ts b/apps/browser/src/vault/popup/settings/sync.component.ts similarity index 100% rename from apps/browser/src/popup/settings/sync.component.ts rename to apps/browser/src/vault/popup/settings/sync.component.ts diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.html b/apps/browser/src/vault/popup/settings/vault-settings.component.html new file mode 100644 index 0000000000..4928720e46 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.html @@ -0,0 +1,56 @@ + +
+ +
+

+ {{ "vault" | i18n }} +

+
+ +
+
+
+
+
+ + + + +
+
+
diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.ts b/apps/browser/src/vault/popup/settings/vault-settings.component.ts new file mode 100644 index 0000000000..a12f6d1d5b --- /dev/null +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +@Component({ + selector: "vault-settings", + templateUrl: "vault-settings.component.html", +}) +export class VaultSettingsComponent { + constructor( + public messagingService: MessagingService, + private router: Router, + ) {} + + async import() { + await this.router.navigate(["/import"]); + if (await BrowserApi.isPopupOpen()) { + await BrowserPopupUtils.openCurrentPagePopout(window); + } + } +}