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
new file mode 100644
index 0000000000..2388bb06bd
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html
@@ -0,0 +1,91 @@
+
+
+
+
+ @let showBadge = firstTimeDialog();
+ @if (showBadge) {
+ {{ "availableNow" | i18n }}
+ }
+
+ {{ (firstTimeDialog ? "autoConfirm" : "editPolicy") | i18n }}
+ @if (!firstTimeDialog) {
+
+ {{ policy.name | i18n }}
+
+ }
+
+
+
+
+
+ {{ "howToTurnOnAutoConfirm" | i18n }}
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000000..18a9306b7d
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts
@@ -0,0 +1,249 @@
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Component,
+ Inject,
+ signal,
+ Signal,
+ TemplateRef,
+ viewChild,
+} from "@angular/core";
+import { FormBuilder } from "@angular/forms";
+import { Router } from "@angular/router";
+import {
+ combineLatest,
+ firstValueFrom,
+ map,
+ Observable,
+ of,
+ shareReplay,
+ startWith,
+ switchMap,
+ tap,
+} from "rxjs";
+
+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 { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import {
+ DIALOG_DATA,
+ DialogConfig,
+ DialogRef,
+ DialogService,
+ ToastService,
+} from "@bitwarden/components";
+import { KeyService } from "@bitwarden/key-management";
+
+import { SharedModule } from "../../../shared";
+
+import { AutoConfirmPolicyEditComponent } from "./policy-edit-definitions/auto-confirm-policy.component";
+import {
+ PolicyEditDialogComponent,
+ PolicyEditDialogData,
+ PolicyEditDialogResult,
+} from "./policy-edit-dialog.component";
+
+export type MultiStepSubmit = {
+ sideEffect: () => Promise;
+ footerContent: Signal | undefined>;
+ titleContent: Signal | undefined>;
+};
+
+export type AutoConfirmPolicyDialogData = PolicyEditDialogData & {
+ firstTimeDialog?: boolean;
+};
+
+/**
+ * Custom policy dialog component for Auto-Confirm policy.
+ * Satisfies the PolicyDialogComponent interface structurally
+ * via its static open() function.
+ */
+@Component({
+ templateUrl: "auto-confirm-edit-policy-dialog.component.html",
+ imports: [SharedModule],
+})
+export class AutoConfirmPolicyDialogComponent
+ extends PolicyEditDialogComponent
+ implements AfterViewInit
+{
+ policyType = PolicyType;
+
+ protected firstTimeDialog = signal(false);
+ protected currentStep = signal(0);
+ protected multiStepSubmit: Observable = of([]);
+ protected autoConfirmEnabled$: Observable = this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) => this.policyService.policies$(userId)),
+ map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm)?.enabled ?? false),
+ );
+
+ private submitPolicy: Signal | undefined> = viewChild("step0");
+ private openExtension: Signal | undefined> = viewChild("step1");
+
+ private submitPolicyTitle: Signal | undefined> = viewChild("step0Title");
+ private openExtensionTitle: Signal | undefined> = viewChild("step1Title");
+
+ override policyComponent: AutoConfirmPolicyEditComponent | undefined;
+
+ constructor(
+ @Inject(DIALOG_DATA) protected data: AutoConfirmPolicyDialogData,
+ accountService: AccountService,
+ policyApiService: PolicyApiServiceAbstraction,
+ i18nService: I18nService,
+ cdr: ChangeDetectorRef,
+ formBuilder: FormBuilder,
+ dialogRef: DialogRef,
+ toastService: ToastService,
+ configService: ConfigService,
+ keyService: KeyService,
+ private policyService: PolicyService,
+ private router: Router,
+ ) {
+ super(
+ data,
+ accountService,
+ policyApiService,
+ i18nService,
+ cdr,
+ formBuilder,
+ dialogRef,
+ toastService,
+ configService,
+ keyService,
+ );
+
+ this.firstTimeDialog.set(data.firstTimeDialog ?? false);
+ }
+
+ /**
+ * Instantiates the child policy component and inserts it into the view.
+ */
+ async ngAfterViewInit() {
+ await super.ngAfterViewInit();
+
+ if (this.policyComponent) {
+ this.saveDisabled$ = combineLatest([
+ this.autoConfirmEnabled$,
+ this.policyComponent.enabled.valueChanges.pipe(
+ startWith(this.policyComponent.enabled.value),
+ ),
+ ]).pipe(map(([policyEnabled, value]) => !policyEnabled && !value));
+ }
+
+ this.multiStepSubmit = this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) => this.policyService.policies$(userId)),
+ map((policies) => policies.find((p) => p.type === PolicyType.SingleOrg)?.enabled ?? false),
+ tap((singleOrgPolicyEnabled) =>
+ this.policyComponent?.setSingleOrgEnabled(singleOrgPolicyEnabled),
+ ),
+ map((singleOrgPolicyEnabled) => [
+ {
+ sideEffect: () => this.handleSubmit(singleOrgPolicyEnabled ?? false),
+ footerContent: this.submitPolicy,
+ titleContent: this.submitPolicyTitle,
+ },
+ {
+ sideEffect: () => this.openBrowserExtension(),
+ footerContent: this.openExtension,
+ titleContent: this.openExtensionTitle,
+ },
+ ]),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
+ }
+
+ private async handleSubmit(singleOrgEnabled: boolean) {
+ if (!singleOrgEnabled) {
+ await this.submitSingleOrg();
+ }
+ await this.submitAutoConfirm();
+ }
+
+ /**
+ * Triggers policy submission for auto confirm.
+ * @returns boolean: true if multi-submit workflow should continue, false otherwise.
+ */
+ private async submitAutoConfirm() {
+ if (!this.policyComponent) {
+ throw new Error("PolicyComponent not initialized.");
+ }
+
+ const autoConfirmRequest = await this.policyComponent.buildRequest();
+ await this.policyApiService.putPolicy(
+ this.data.organizationId,
+ this.data.policy.type,
+ autoConfirmRequest,
+ );
+
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)),
+ });
+
+ if (!this.policyComponent.enabled.value) {
+ this.dialogRef.close("saved");
+ }
+ }
+
+ private async submitSingleOrg(): Promise {
+ const singleOrgRequest: PolicyRequest = {
+ type: PolicyType.SingleOrg,
+ enabled: true,
+ data: null,
+ };
+
+ await this.policyApiService.putPolicy(
+ this.data.organizationId,
+ PolicyType.SingleOrg,
+ singleOrgRequest,
+ );
+ }
+
+ private async openBrowserExtension() {
+ await this.router.navigate(["/browser-extension-prompt"], {
+ queryParams: { url: "AutoConfirm" },
+ });
+ }
+
+ submit = async () => {
+ if (!this.policyComponent) {
+ throw new Error("PolicyComponent not initialized.");
+ }
+
+ if ((await this.policyComponent.confirm()) == false) {
+ this.dialogRef.close();
+ return;
+ }
+
+ try {
+ const multiStepSubmit = await firstValueFrom(this.multiStepSubmit);
+ await multiStepSubmit[this.currentStep()].sideEffect();
+
+ if (this.currentStep() === multiStepSubmit.length - 1) {
+ this.dialogRef.close("saved");
+ return;
+ }
+
+ this.currentStep.update((value) => value + 1);
+ this.policyComponent.setStep(this.currentStep());
+ } catch (error: any) {
+ this.toastService.showToast({
+ variant: "error",
+ message: error.message,
+ });
+ }
+ };
+
+ static open = (
+ dialogService: DialogService,
+ config: DialogConfig,
+ ) => {
+ return dialogService.open(AutoConfirmPolicyDialogComponent, config);
+ };
+}
diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts
index 9293c686b7..9bf0ad24b1 100644
--- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts
@@ -8,8 +8,20 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
-import type { PolicyEditDialogComponent } from "./policy-edit-dialog.component";
+import type { PolicyEditDialogData, PolicyEditDialogResult } from "./policy-edit-dialog.component";
+
+/**
+ * Interface for policy dialog components.
+ * Any component that implements this interface can be used as a custom policy edit dialog.
+ */
+export interface PolicyDialogComponent {
+ open: (
+ dialogService: DialogService,
+ config: DialogConfig,
+ ) => DialogRef;
+}
/**
* A metadata class that defines how a policy is displayed in the Admin Console Policies page for editing.
@@ -37,9 +49,8 @@ export abstract class BasePolicyEditDefinition {
/**
* The dialog component that will be opened when editing this policy.
* This allows customizing the look and feel of each policy's dialog contents.
- * If not specified, defaults to {@link PolicyEditDialogComponent}.
*/
- editDialogComponent?: typeof PolicyEditDialogComponent;
+ editDialogComponent?: PolicyDialogComponent;
/**
* If true, the {@link description} will be reused in the policy edit modal. Set this to false if you
diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts
index 95c00f74f1..7bab6f262a 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts
@@ -1,17 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import {
combineLatest,
firstValueFrom,
- lastValueFrom,
Observable,
of,
switchMap,
first,
map,
withLatestFrom,
+ tap,
} from "rxjs";
import {
@@ -19,9 +20,11 @@ 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";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
import { safeProvider } from "@bitwarden/ui-common";
@@ -29,7 +32,7 @@ import { safeProvider } from "@bitwarden/ui-common";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
-import { BasePolicyEditDefinition } from "./base-policy-edit.component";
+import { BasePolicyEditDefinition, PolicyDialogComponent } from "./base-policy-edit.component";
import { PolicyEditDialogComponent } from "./policy-edit-dialog.component";
import { PolicyListService } from "./policy-list.service";
import { POLICY_EDIT_REGISTER } from "./policy-register-token";
@@ -59,8 +62,18 @@ export class PoliciesComponent implements OnInit {
private policyApiService: PolicyApiServiceAbstraction,
private policyListService: PolicyListService,
private dialogService: DialogService,
+ private policyService: PolicyService,
protected configService: ConfigService,
- ) {}
+ ) {
+ this.accountService.activeAccount$
+ .pipe(
+ getUserId,
+ switchMap((userId) => this.policyService.policies$(userId)),
+ tap(async () => await this.load()),
+ takeUntilDestroyed(),
+ )
+ .subscribe();
+ }
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
@@ -127,17 +140,13 @@ export class PoliciesComponent implements OnInit {
}
async edit(policy: BasePolicyEditDefinition) {
- const dialogComponent = policy.editDialogComponent ?? PolicyEditDialogComponent;
- const dialogRef = dialogComponent.open(this.dialogService, {
+ const dialogComponent: PolicyDialogComponent =
+ policy.editDialogComponent ?? PolicyEditDialogComponent;
+ dialogComponent.open(this.dialogService, {
data: {
policy: policy,
organizationId: this.organizationId,
},
});
-
- const result = await lastValueFrom(dialogRef.closed);
- if (result == "saved") {
- await this.load();
- }
}
}
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
new file mode 100644
index 0000000000..8334b451d2
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.html
@@ -0,0 +1,59 @@
+
+
+
+
+ {{ "autoConfirmPolicyEditDescription" | i18n }}
+
+
+
+ -
+
+ {{ "autoConfirmAcceptSecurityRiskTitle" | i18n }}
+
+ {{ "autoConfirmAcceptSecurityRiskDescription" | i18n }}
+
+ {{ "autoConfirmAcceptSecurityRiskLearnMore" | i18n }}
+
+
+
+
+ -
+ @if (singleOrgEnabled$ | async) {
+
+ {{ "autoConfirmSingleOrgExemption" | i18n }}
+
+ } @else {
+
+ {{ "autoConfirmSingleOrgRequired" | i18n }}
+
+ }
+ {{ "autoConfirmSingleOrgRequiredDescription" | i18n }}
+
+
+ -
+
+ {{ "autoConfirmNoEmergencyAccess" | i18n }}
+
+ {{ "autoConfirmNoEmergencyAccessDescription" | i18n }}
+
+
+
+
+ {{ "autoConfirmCheckBoxLabel" | i18n }}
+
+
+
+
+
+
+
+ - 1. {{ "autoConfirmStep1" | i18n }}
+
+ -
+ 2. {{ "autoConfirmStep2a" | i18n }}
+
+ {{ "autoConfirmStep2b" | i18n }}
+
+
+
+
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts
new file mode 100644
index 0000000000..a5ea2ef879
--- /dev/null
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts
@@ -0,0 +1,50 @@
+import { Component, OnInit, Signal, TemplateRef, viewChild } from "@angular/core";
+import { BehaviorSubject, map, Observable } from "rxjs";
+
+import { AutoConfirmSvg } from "@bitwarden/assets/svg";
+import { PolicyType } from "@bitwarden/common/admin-console/enums";
+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 { SharedModule } from "../../../../shared";
+import { AutoConfirmPolicyDialogComponent } from "../auto-confirm-edit-policy-dialog.component";
+import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component";
+
+export class AutoConfirmPolicy extends BasePolicyEditDefinition {
+ name = "autoConfirm";
+ description = "autoConfirmDescription";
+ type = PolicyType.AutoConfirm;
+ component = AutoConfirmPolicyEditComponent;
+ showDescription = false;
+ editDialogComponent = AutoConfirmPolicyDialogComponent;
+
+ override display$(organization: Organization, configService: ConfigService): Observable {
+ return configService
+ .getFeatureFlag$(FeatureFlag.AutoConfirm)
+ .pipe(map((enabled) => enabled && organization.useAutomaticUserConfirmation));
+ }
+}
+
+@Component({
+ templateUrl: "auto-confirm-policy.component.html",
+ imports: [SharedModule],
+})
+export class AutoConfirmPolicyEditComponent extends BasePolicyEditComponent implements OnInit {
+ protected readonly autoConfirmSvg = AutoConfirmSvg;
+ private policyForm: Signal | undefined> = viewChild("step0");
+ private extensionButton: Signal | undefined> = viewChild("step1");
+
+ protected step: number = 0;
+ protected steps = [this.policyForm, this.extensionButton];
+
+ protected singleOrgEnabled$: BehaviorSubject = new BehaviorSubject(false);
+
+ setSingleOrgEnabled(enabled: boolean) {
+ this.singleOrgEnabled$.next(enabled);
+ }
+
+ setStep(step: number) {
+ this.step = step;
+ }
+}
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts
index bb2c40b7a7..7373e1ff88 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/index.ts
@@ -14,3 +14,4 @@ export {
vNextOrganizationDataOwnershipPolicy,
vNextOrganizationDataOwnershipPolicyComponent,
} from "./vnext-organization-data-ownership.component";
+export { AutoConfirmPolicy } from "./auto-confirm-policy.component";
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts
index f0672d0f86..d98b5d4809 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts
@@ -30,7 +30,7 @@ import { KeyService } from "@bitwarden/key-management";
import { SharedModule } from "../../../shared";
import { BasePolicyEditDefinition, BasePolicyEditComponent } from "./base-policy-edit.component";
-import { vNextOrganizationDataOwnershipPolicyComponent } from "./policy-edit-definitions";
+import { vNextOrganizationDataOwnershipPolicyComponent } from "./policy-edit-definitions/vnext-organization-data-ownership.component";
export type PolicyEditDialogData = {
/**
@@ -64,13 +64,13 @@ export class PolicyEditDialogComponent implements AfterViewInit {
});
constructor(
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
- private accountService: AccountService,
- private policyApiService: PolicyApiServiceAbstraction,
- private i18nService: I18nService,
+ protected accountService: AccountService,
+ protected policyApiService: PolicyApiServiceAbstraction,
+ protected i18nService: I18nService,
private cdr: ChangeDetectorRef,
private formBuilder: FormBuilder,
- private dialogRef: DialogRef,
- private toastService: ToastService,
+ protected dialogRef: DialogRef,
+ protected toastService: ToastService,
private configService: ConfigService,
private keyService: KeyService,
) {}
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts
index 5e63ba1358..ca44818764 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-register.ts
@@ -1,5 +1,6 @@
import { BasePolicyEditDefinition } from "./base-policy-edit.component";
import {
+ AutoConfirmPolicy,
DesktopAutotypeDefaultSettingPolicy,
DisableSendPolicy,
MasterPasswordPolicy,
@@ -33,4 +34,5 @@ export const ossPolicyEditRegister: BasePolicyEditDefinition[] = [
new SendOptionsPolicy(),
new RestrictedItemTypesPolicy(),
new DesktopAutotypeDefaultSettingPolicy(),
+ new AutoConfirmPolicy(),
];
diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html
index 56332cc424..aff549a84f 100644
--- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html
+++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html
@@ -4,13 +4,14 @@
{{ "openingExtension" | i18n }}
+ @let page = extensionPage$ | async;
{{ "openingExtensionError" | i18n }}