1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[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 7c18a5e512.
This commit is contained in:
Brandon Treston
2025-11-07 15:58:39 -05:00
committed by GitHub
parent ec07a5391a
commit 19626d1b3e
10 changed files with 151 additions and 19 deletions

View File

@@ -38,11 +38,11 @@
<div class="tw-flex tw-flex-col"> <div class="tw-flex tw-flex-col">
@let showBadge = firstTimeDialog(); @let showBadge = firstTimeDialog();
@if (showBadge) { @if (showBadge) {
<span bitBadge variant="info" class="tw-w-28 tw-my-2"> {{ "availableNow" | i18n }}</span> <span bitBadge variant="info" class="tw-w-[99px] tw-my-2"> {{ "availableNow" | i18n }}</span>
} }
<span> <span>
{{ (firstTimeDialog ? "autoConfirm" : "editPolicy") | i18n }} {{ (showBadge ? "autoConfirm" : "editPolicy") | i18n }}
@if (!firstTimeDialog) { @if (!showBadge) {
<span class="tw-text-muted tw-font-normal tw-text-sm"> <span class="tw-text-muted tw-font-normal tw-text-sm">
{{ policy.name | i18n }} {{ policy.name | i18n }}
</span> </span>
@@ -64,7 +64,7 @@
type="submit" type="submit"
> >
@let autoConfirmEnabled = autoConfirmEnabled$ | async; @let autoConfirmEnabled = autoConfirmEnabled$ | async;
@let managePoliciesOnly = managePolicies$ | async; @let managePoliciesOnly = managePoliciesOnly$ | async;
@if (autoConfirmEnabled || managePoliciesOnly) { @if (autoConfirmEnabled || managePoliciesOnly) {
{{ "save" | i18n }} {{ "save" | i18n }}
} @else { } @else {

View File

@@ -22,6 +22,7 @@ import {
tap, tap,
} from "rxjs"; } from "rxjs";
import { AutomaticUserConfirmationService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.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)), switchMap((userId) => this.policyService.policies$(userId)),
map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm)?.enabled ?? false), map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm)?.enabled ?? false),
); );
protected managePolicies$: Observable<boolean> = 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<boolean> = this.accountService.activeAccount$.pipe(
getUserId, getUserId,
switchMap((userId) => this.organizationService.organizations$(userId)), switchMap((userId) => this.organizationService.organizations$(userId)),
getById(this.data.organizationId), getById(this.data.organizationId),
@@ -116,6 +120,7 @@ export class AutoConfirmPolicyDialogComponent
private organizationService: OrganizationService, private organizationService: OrganizationService,
private policyService: PolicyService, private policyService: PolicyService,
private router: Router, private router: Router,
private autoConfirmService: AutomaticUserConfirmationService,
) { ) {
super( super(
data, data,
@@ -161,7 +166,7 @@ export class AutoConfirmPolicyDialogComponent
} }
private buildMultiStepSubmit(singleOrgPolicyEnabled: boolean): Observable<MultiStepSubmit[]> { private buildMultiStepSubmit(singleOrgPolicyEnabled: boolean): Observable<MultiStepSubmit[]> {
return this.managePolicies$.pipe( return this.managePoliciesOnly$.pipe(
map((managePoliciesOnly) => { map((managePoliciesOnly) => {
const submitSteps = [ const submitSteps = [
{ {
@@ -206,6 +211,17 @@ export class AutoConfirmPolicyDialogComponent
autoConfirmRequest, 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({ this.toastService.showToast({
variant: "success", variant: "success",
message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)),

View File

@@ -2,3 +2,6 @@ export { PoliciesComponent } from "./policies.component";
export { ossPolicyEditRegister } from "./policy-edit-register"; export { ossPolicyEditRegister } from "./policy-edit-register";
export { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component"; export { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component";
export { POLICY_EDIT_REGISTER } from "./policy-register-token"; 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";

View File

@@ -47,12 +47,12 @@
<bit-icon class="tw-w-[233px]" [icon]="autoConfirmSvg"></bit-icon> <bit-icon class="tw-w-[233px]" [icon]="autoConfirmSvg"></bit-icon>
</div> </div>
<ol> <ol>
<li>1. {{ "autoConfirmStep1" | i18n }}</li> <li>1. {{ "autoConfirmExtension1" | i18n }}</li>
<li> <li>
2. {{ "autoConfirmStep2a" | i18n }} 2. {{ "autoConfirmExtension2" | i18n }}
<strong> <strong>
{{ "autoConfirmStep2b" | i18n }} {{ "autoConfirmExtension3" | i18n }}
</strong> </strong>
</li> </li>
</ol> </ol>

View File

@@ -9,6 +9,10 @@ import {
DefaultCollectionAdminService, DefaultCollectionAdminService,
OrganizationUserApiService, OrganizationUserApiService,
CollectionService, CollectionService,
AutomaticUserConfirmationService,
DefaultAutomaticUserConfirmationService,
OrganizationUserService,
DefaultOrganizationUserService,
} from "@bitwarden/admin-console/common"; } from "@bitwarden/admin-console/common";
import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth/device-management/default-device-management-component.service"; 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"; import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
@@ -44,7 +48,10 @@ import {
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; 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 { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { import {
InternalPolicyService, InternalPolicyService,
@@ -338,6 +345,29 @@ const safeProviders: SafeProvider[] = [
OrganizationService, 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({ safeProvider({
provide: SdkLoadService, provide: SdkLoadService,
useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService,

View File

@@ -9,6 +9,7 @@ import {
lastValueFrom, lastValueFrom,
Observable, Observable,
Subject, Subject,
zip,
} from "rxjs"; } from "rxjs";
import { import {
concatMap, concatMap,
@@ -25,6 +26,7 @@ import {
} from "rxjs/operators"; } from "rxjs/operators";
import { import {
AutomaticUserConfirmationService,
CollectionData, CollectionData,
CollectionDetailsResponse, CollectionDetailsResponse,
CollectionService, 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums"; 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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -102,6 +106,11 @@ import {
getNestedCollectionTree, getNestedCollectionTree,
getFlatCollectionTree, getFlatCollectionTree,
} from "../../admin-console/organizations/collections"; } from "../../admin-console/organizations/collections";
import {
AutoConfirmPolicy,
AutoConfirmPolicyDialogComponent,
PolicyEditDialogResult,
} from "../../admin-console/organizations/policies";
import { import {
CollectionDialogAction, CollectionDialogAction,
CollectionDialogTabType, CollectionDialogTabType,
@@ -213,6 +222,8 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined; private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
private autoConfirmDialogRef?: DialogRef<PolicyEditDialogResult> | undefined;
protected showAddCipherBtn: boolean = false; protected showAddCipherBtn: boolean = false;
organizations$ = this.accountService.activeAccount$ organizations$ = this.accountService.activeAccount$
@@ -328,6 +339,8 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
private policyService: PolicyService, private policyService: PolicyService,
private unifiedUpgradePromptService: UnifiedUpgradePromptService, private unifiedUpgradePromptService: UnifiedUpgradePromptService,
private premiumUpgradePromptService: PremiumUpgradePromptService, private premiumUpgradePromptService: PremiumUpgradePromptService,
private autoConfirmService: AutomaticUserConfirmationService,
private configService: ConfigService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -629,6 +642,8 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
}, },
); );
void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); void this.unifiedUpgradePromptService.displayUpgradePromptConditionally();
this.setupAutoConfirm();
} }
ngOnDestroy() { ngOnDestroy() {
@@ -1547,6 +1562,72 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId);
return cipherView.login?.password; 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),
});
}
} }
/** /**

View File

@@ -5832,16 +5832,16 @@
"howToTurnOnAutoConfirm": { "howToTurnOnAutoConfirm": {
"message": "How to turn on automatic user confirmation" "message": "How to turn on automatic user confirmation"
}, },
"autoConfirmStep1": { "autoConfirmExtension1": {
"message": "Open your Bitwarden extension." "message": "Open your Bitwarden extension"
}, },
"autoConfirmStep2a": { "autoConfirmExtension2": {
"message": "Select", "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": { "autoConfirmExtension3": {
"message": " Turn on.", "message": " Turn on",
"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'"
}, },
"autoConfirmExtensionOpened": { "autoConfirmExtensionOpened": {
"message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting."

View File

@@ -16,6 +16,6 @@ export const AUTO_CONFIRM_STATE = UserKeyDefinition.record<AutoConfirmState>(
"autoConfirm", "autoConfirm",
{ {
deserializer: (autoConfirmState) => autoConfirmState, deserializer: (autoConfirmState) => autoConfirmState,
clearOn: ["logout"], clearOn: [],
}, },
); );

View File

@@ -285,6 +285,8 @@ export class DefaultPolicyService implements PolicyService {
case PolicyType.RemoveUnlockWithPin: case PolicyType.RemoveUnlockWithPin:
// Remove Unlock with PIN policy // Remove Unlock with PIN policy
return false; return false;
case PolicyType.AutoConfirm:
return false;
case PolicyType.OrganizationDataOwnership: case PolicyType.OrganizationDataOwnership:
// organization data ownership policy applies to everyone except admins and owners // organization data ownership policy applies to everyone except admins and owners
return organization.isAdmin; return organization.isAdmin;

View File

@@ -36,7 +36,7 @@ export const DELETE_MANAGED_USER_WARNING = new StateDefinition(
web: "disk-local", web: "disk-local",
}, },
); );
export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk"); export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk", { web: "disk-local" });
// Billing // Billing
export const BILLING_DISK = new StateDefinition("billing", "disk"); export const BILLING_DISK = new StateDefinition("billing", "disk");