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 b85f79f6038..4d1db65034d 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 179dda5a5f4..bdc664e208e 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 624e5132faf..3042be240f7 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 c6a62ab2641..54f166b662e 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. {{ "autoConfirmStep1" | i18n }}
+ - 1. {{ "autoConfirmExtension1" | i18n }}
-
- 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 72a563a77a3..bf741132b00 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 7bdd290336d..4c23119f1eb 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 56332e5ac50..5c712c98e0d 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 c69db69746c..fd3cfa2f590 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 1107e88e796..b9d7655195b 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 42d7f5aaaf8..7b1d75b2985 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");