From 19626d1b3eece71ed6fa59c46484780ee2bd81d4 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 7 Nov 2025 15:58:39 -0500 Subject: [PATCH] [PM-26363] Add one time setup dialog for auto confirm (#17104) * add one time setup dialog for auto confirm * add one time setup dialog for auto confirm * fix copy, padding, cleanup observable logic * cleanup * cleanup * refactor * clean up * more cleanup * Fix deleted files This reverts commit 7c18a5e5123f90e74fad3f766a74b9d13bec234b. --- ...-confirm-edit-policy-dialog.component.html | 8 +- ...to-confirm-edit-policy-dialog.component.ts | 20 ++++- .../organizations/policies/index.ts | 3 + .../auto-confirm-policy.component.html | 6 +- apps/web/src/app/core/core.module.ts | 32 +++++++- .../vault/individual-vault/vault.component.ts | 81 +++++++++++++++++++ apps/web/src/locales/en/messages.json | 14 ++-- .../models/auto-confirm-state.model.ts | 2 +- .../services/policy/default-policy.service.ts | 2 + libs/state/src/core/state-definitions.ts | 2 +- 10 files changed, 151 insertions(+), 19 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html index b85f79f603..4d1db65034 100644 --- a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html @@ -38,11 +38,11 @@
@let showBadge = firstTimeDialog(); @if (showBadge) { - {{ "availableNow" | i18n }} + {{ "availableNow" | i18n }} } - {{ (firstTimeDialog ? "autoConfirm" : "editPolicy") | i18n }} - @if (!firstTimeDialog) { + {{ (showBadge ? "autoConfirm" : "editPolicy") | i18n }} + @if (!showBadge) { {{ policy.name | i18n }} @@ -64,7 +64,7 @@ type="submit" > @let autoConfirmEnabled = autoConfirmEnabled$ | async; - @let managePoliciesOnly = managePolicies$ | async; + @let managePoliciesOnly = managePoliciesOnly$ | async; @if (autoConfirmEnabled || managePoliciesOnly) { {{ "save" | i18n }} } @else { diff --git a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts index 179dda5a5f..bdc664e208 100644 --- a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts @@ -22,6 +22,7 @@ import { tap, } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -85,7 +86,10 @@ export class AutoConfirmPolicyDialogComponent switchMap((userId) => this.policyService.policies$(userId)), map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm)?.enabled ?? false), ); - protected managePolicies$: Observable = this.accountService.activeAccount$.pipe( + // Users with manage policies custom permission should not see the dialog's second step since + // they do not have permission to configure the setting. This will only allow them to configure + // the policy. + protected managePoliciesOnly$: Observable = this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => this.organizationService.organizations$(userId)), getById(this.data.organizationId), @@ -116,6 +120,7 @@ export class AutoConfirmPolicyDialogComponent private organizationService: OrganizationService, private policyService: PolicyService, private router: Router, + private autoConfirmService: AutomaticUserConfirmationService, ) { super( data, @@ -161,7 +166,7 @@ export class AutoConfirmPolicyDialogComponent } private buildMultiStepSubmit(singleOrgPolicyEnabled: boolean): Observable { - return this.managePolicies$.pipe( + return this.managePoliciesOnly$.pipe( map((managePoliciesOnly) => { const submitSteps = [ { @@ -206,6 +211,17 @@ export class AutoConfirmPolicyDialogComponent autoConfirmRequest, ); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const currentAutoConfirmState = await firstValueFrom( + this.autoConfirmService.configuration$(userId), + ); + + await this.autoConfirmService.upsert(userId, { + ...currentAutoConfirmState, + showSetupDialog: false, + }); + this.toastService.showToast({ variant: "success", message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), diff --git a/apps/web/src/app/admin-console/organizations/policies/index.ts b/apps/web/src/app/admin-console/organizations/policies/index.ts index 624e5132fa..3042be240f 100644 --- a/apps/web/src/app/admin-console/organizations/policies/index.ts +++ b/apps/web/src/app/admin-console/organizations/policies/index.ts @@ -2,3 +2,6 @@ export { PoliciesComponent } from "./policies.component"; export { ossPolicyEditRegister } from "./policy-edit-register"; export { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component"; export { POLICY_EDIT_REGISTER } from "./policy-register-token"; +export { AutoConfirmPolicyDialogComponent } from "./auto-confirm-edit-policy-dialog.component"; +export { AutoConfirmPolicy } from "./policy-edit-definitions"; +export { PolicyEditDialogResult } from "./policy-edit-dialog.component"; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html index c6a62ab264..54f166b662 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html @@ -47,12 +47,12 @@
    -
  1. 1. {{ "autoConfirmStep1" | i18n }}
  2. +
  3. 1. {{ "autoConfirmExtension1" | i18n }}
  4. - 2. {{ "autoConfirmStep2a" | i18n }} + 2. {{ "autoConfirmExtension2" | i18n }} - {{ "autoConfirmStep2b" | i18n }} + {{ "autoConfirmExtension3" | i18n }}
diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 72a563a77a..bf741132b0 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -9,6 +9,10 @@ import { DefaultCollectionAdminService, OrganizationUserApiService, CollectionService, + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, + OrganizationUserService, + DefaultOrganizationUserService, } from "@bitwarden/admin-console/common"; import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth/device-management/default-device-management-component.service"; import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction"; @@ -44,7 +48,10 @@ import { } from "@bitwarden/auth/common"; 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 { + InternalOrganizationServiceAbstraction, + OrganizationService, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService, @@ -338,6 +345,29 @@ const safeProviders: SafeProvider[] = [ OrganizationService, ], }), + safeProvider({ + provide: OrganizationUserService, + useClass: DefaultOrganizationUserService, + deps: [ + KeyServiceAbstraction, + EncryptService, + OrganizationUserApiService, + AccountService, + I18nServiceAbstraction, + ], + }), + safeProvider({ + provide: AutomaticUserConfirmationService, + useClass: DefaultAutomaticUserConfirmationService, + deps: [ + ConfigService, + ApiService, + OrganizationUserService, + StateProvider, + InternalOrganizationServiceAbstraction, + OrganizationUserApiService, + ], + }), safeProvider({ provide: SdkLoadService, useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 7bdd290336..4c23119f1e 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -9,6 +9,7 @@ import { lastValueFrom, Observable, Subject, + zip, } from "rxjs"; import { concatMap, @@ -25,6 +26,7 @@ import { } from "rxjs/operators"; import { + AutomaticUserConfirmationService, CollectionData, CollectionDetailsResponse, CollectionService, @@ -54,7 +56,9 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -102,6 +106,11 @@ import { getNestedCollectionTree, getFlatCollectionTree, } from "../../admin-console/organizations/collections"; +import { + AutoConfirmPolicy, + AutoConfirmPolicyDialogComponent, + PolicyEditDialogResult, +} from "../../admin-console/organizations/policies"; import { CollectionDialogAction, CollectionDialogTabType, @@ -213,6 +222,8 @@ export class VaultComponent implements OnInit, OnDestr private destroy$ = new Subject(); private vaultItemDialogRef?: DialogRef | undefined; + private autoConfirmDialogRef?: DialogRef | undefined; + protected showAddCipherBtn: boolean = false; organizations$ = this.accountService.activeAccount$ @@ -328,6 +339,8 @@ export class VaultComponent implements OnInit, OnDestr private policyService: PolicyService, private unifiedUpgradePromptService: UnifiedUpgradePromptService, private premiumUpgradePromptService: PremiumUpgradePromptService, + private autoConfirmService: AutomaticUserConfirmationService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -629,6 +642,8 @@ export class VaultComponent implements OnInit, OnDestr }, ); void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); + + this.setupAutoConfirm(); } ngOnDestroy() { @@ -1547,6 +1562,72 @@ export class VaultComponent implements OnInit, OnDestr const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); return cipherView.login?.password; } + + private async openAutoConfirmFeatureDialog(organization: Organization) { + if (this.autoConfirmDialogRef) { + return; + } + + this.autoConfirmDialogRef = AutoConfirmPolicyDialogComponent.open(this.dialogService, { + data: { + policy: new AutoConfirmPolicy(), + organizationId: organization.id, + firstTimeDialog: true, + }, + }); + + await lastValueFrom(this.autoConfirmDialogRef.closed); + this.autoConfirmDialogRef = undefined; + } + + private setupAutoConfirm() { + // if the policy is enabled, then the user may only belong to one organization at most. + const organization$ = this.organizations$.pipe(map((organizations) => organizations[0])); + + const featureFlag$ = this.configService.getFeatureFlag$(FeatureFlag.AutoConfirm); + + const autoConfirmState$ = this.userId$.pipe( + switchMap((userId) => this.autoConfirmService.configuration$(userId)), + ); + + const policyEnabled$ = combineLatest([ + this.userId$.pipe( + switchMap((userId) => this.policyService.policies$(userId)), + map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm && p.enabled)), + ), + organization$, + ]).pipe( + map( + ([policy, organization]) => (policy && policy.organizationId === organization?.id) ?? false, + ), + ); + + zip([organization$, featureFlag$, autoConfirmState$, policyEnabled$, this.userId$]) + .pipe( + first(), + switchMap(async ([organization, flagEnabled, autoConfirmState, policyEnabled, userId]) => { + const showDialog = + flagEnabled && + !policyEnabled && + autoConfirmState.showSetupDialog && + !!organization && + (organization.canManageUsers || organization.canManagePolicies); + + if (showDialog) { + await this.openAutoConfirmFeatureDialog(organization); + + await this.autoConfirmService.upsert(userId, { + ...autoConfirmState, + showSetupDialog: false, + }); + } + }), + takeUntil(this.destroy$), + ) + .subscribe({ + error: (err: unknown) => this.logService.error("Failed to update auto-confirm state", err), + }); + } } /** diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 56332e5ac5..5c712c98e0 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5832,16 +5832,16 @@ "howToTurnOnAutoConfirm": { "message": "How to turn on automatic user confirmation" }, - "autoConfirmStep1": { - "message": "Open your Bitwarden extension." + "autoConfirmExtension1": { + "message": "Open your Bitwarden extension" }, - "autoConfirmStep2a": { + "autoConfirmExtension2": { "message": "Select", - "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on'" }, - "autoConfirmStep2b": { - "message": " Turn on.", - "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + "autoConfirmExtension3": { + "message": " Turn on", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on'" }, "autoConfirmExtensionOpened": { "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." diff --git a/libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts b/libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts index c69db69746..fd3cfa2f59 100644 --- a/libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts +++ b/libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts @@ -16,6 +16,6 @@ export const AUTO_CONFIRM_STATE = UserKeyDefinition.record( "autoConfirm", { deserializer: (autoConfirmState) => autoConfirmState, - clearOn: ["logout"], + clearOn: [], }, ); diff --git a/libs/common/src/admin-console/services/policy/default-policy.service.ts b/libs/common/src/admin-console/services/policy/default-policy.service.ts index 1107e88e79..b9d7655195 100644 --- a/libs/common/src/admin-console/services/policy/default-policy.service.ts +++ b/libs/common/src/admin-console/services/policy/default-policy.service.ts @@ -285,6 +285,8 @@ export class DefaultPolicyService implements PolicyService { case PolicyType.RemoveUnlockWithPin: // Remove Unlock with PIN policy return false; + case PolicyType.AutoConfirm: + return false; case PolicyType.OrganizationDataOwnership: // organization data ownership policy applies to everyone except admins and owners return organization.isAdmin; diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 42d7f5aaaf..7b1d75b298 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -36,7 +36,7 @@ export const DELETE_MANAGED_USER_WARNING = new StateDefinition( web: "disk-local", }, ); -export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk"); +export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk", { web: "disk-local" }); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk");