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 3042be240f7..eb614e180e1 100644 --- a/apps/web/src/app/admin-console/organizations/policies/index.ts +++ b/apps/web/src/app/admin-console/organizations/policies/index.ts @@ -2,6 +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"; +export * from "./policy-edit-dialogs"; 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 index cf2a2929905..66074918084 100644 --- 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 @@ -15,8 +15,8 @@ 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"; +import { AutoConfirmPolicyDialogComponent } from "../policy-edit-dialogs/auto-confirm-edit-policy-dialog.component"; export class AutoConfirmPolicy extends BasePolicyEditDefinition { name = "autoConfirm"; 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 9b46e228af9..042f9771529 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 @@ -1,7 +1,10 @@ export { DisableSendPolicy } from "./disable-send.component"; export { DesktopAutotypeDefaultSettingPolicy } from "./autotype-policy.component"; export { MasterPasswordPolicy } from "./master-password.component"; -export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component"; +export { + OrganizationDataOwnershipPolicy, + OrganizationDataOwnershipPolicyComponent, +} from "./organization-data-ownership.component"; export { PasswordGeneratorPolicy } from "./password-generator.component"; export { RemoveUnlockWithPinPolicy } from "./remove-unlock-with-pin.component"; export { RequireSsoPolicy } from "./require-sso.component"; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.html index 2b6c86b1fdc..bd2237bc2fd 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.html @@ -1,8 +1,57 @@ - - {{ "personalOwnershipExemption" | i18n }} - +

+ {{ "organizationDataOwnershipDescContent" | i18n }} + + {{ "organizationDataOwnershipContentAnchor" | i18n }}. + +

{{ "turnOn" | i18n }} + + + + {{ "organizationDataOwnershipWarningTitle" | i18n }} + +
+ {{ "organizationDataOwnershipWarningContentTop" | i18n }} +
+
    +
  • + {{ "organizationDataOwnershipWarning1" | i18n }} +
  • +
  • + {{ "organizationDataOwnershipWarning2" | i18n }} +
  • +
  • + {{ "organizationDataOwnershipWarning3" | i18n }} +
  • +
+
+ {{ "organizationDataOwnershipWarningContentBottom" | i18n }} + + {{ "organizationDataOwnershipContentAnchor" | i18n }}. + +
+
+ + + + + + +
+
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts index ceecf8f2ecc..e4a07b7440d 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts @@ -1,22 +1,38 @@ -import { ChangeDetectionStrategy, Component } from "@angular/core"; -import { of, Observable } from "rxjs"; +import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from "@angular/core"; +import { lastValueFrom, map, Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { CenterPositionStrategy, DialogService } from "@bitwarden/components"; +import { EncString } from "@bitwarden/sdk-internal"; import { SharedModule } from "../../../../shared"; import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component"; +export interface VNextPolicyRequest { + policy: PolicyRequest; + metadata: { + defaultUserCollectionName: string; + }; +} + export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { name = "organizationDataOwnership"; - description = "personalOwnershipPolicyDesc"; + description = "organizationDataOwnershipDesc"; type = PolicyType.OrganizationDataOwnership; component = OrganizationDataOwnershipPolicyComponent; + showDescription = false; - display$(organization: Organization, configService: ConfigService): Observable { - // TODO Remove this entire component upon verifying that it can be deleted due to its sole reliance of the CreateDefaultLocation feature flag - return of(false); + override display$(organization: Organization, configService: ConfigService): Observable { + return configService + .getFeatureFlag$(FeatureFlag.MigrateMyVaultToMyItems) + .pipe(map((enabled) => !enabled)); } } @@ -26,4 +42,61 @@ export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { imports: [SharedModule], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class OrganizationDataOwnershipPolicyComponent extends BasePolicyEditComponent {} +export class OrganizationDataOwnershipPolicyComponent + extends BasePolicyEditComponent + implements OnInit +{ + constructor( + private dialogService: DialogService, + private i18nService: I18nService, + private encryptService: EncryptService, + ) { + super(); + } + + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @ViewChild("dialog", { static: true }) warningContent!: TemplateRef; + + override async confirm(): Promise { + if (this.policyResponse?.enabled && !this.enabled.value) { + const dialogRef = this.dialogService.open(this.warningContent, { + positionStrategy: new CenterPositionStrategy(), + }); + const result = await lastValueFrom(dialogRef.closed); + return Boolean(result); + } + return true; + } + + async buildVNextRequest(orgKey: OrgKey): Promise { + if (!this.policy) { + throw new Error("Policy was not found"); + } + + const defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey); + + const request: VNextPolicyRequest = { + policy: { + enabled: this.enabled.value ?? false, + data: this.buildRequestData(), + }, + metadata: { + defaultUserCollectionName, + }, + }; + + return request; + } + + private async getEncryptedDefaultUserCollectionName(orgKey: OrgKey): Promise { + const defaultCollectionName = this.i18nService.t("myItems"); + const encrypted = await this.encryptService.encryptString(defaultCollectionName, orgKey); + + if (!encrypted.encryptedString) { + throw new Error("Encryption error"); + } + + return encrypted.encryptedString; + } +} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html index bd2237bc2fd..e6c93b323c2 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html @@ -1,57 +1,52 @@ -

- {{ "organizationDataOwnershipDescContent" | i18n }} - - {{ "organizationDataOwnershipContentAnchor" | i18n }}. - -

+ - - - {{ "turnOn" | i18n }} - + +

+ {{ "centralizeDataOwnershipDesc" | i18n }} + + {{ "centralizeDataOwnershipContentAnchor" | i18n }} + + +

- - - {{ "organizationDataOwnershipWarningTitle" | i18n }} - -
- {{ "organizationDataOwnershipWarningContentTop" | i18n }} -
-
    -
  • - {{ "organizationDataOwnershipWarning1" | i18n }} -
  • -
  • - {{ "organizationDataOwnershipWarning2" | i18n }} -
  • -
  • - {{ "organizationDataOwnershipWarning3" | i18n }} -
  • -
-
- {{ "organizationDataOwnershipWarningContentBottom" | i18n }} - - {{ "organizationDataOwnershipContentAnchor" | i18n }}. - -
-
- - - - - - -
+
+ {{ "benefits" | i18n }}: +
    +
  • + {{ "centralizeDataOwnershipBenefit1" | i18n }} +
  • +
  • + {{ "centralizeDataOwnershipBenefit2" | i18n }} +
  • +
  • + {{ "centralizeDataOwnershipBenefit3" | i18n }} +
  • +
+
+ + + + {{ "turnOn" | i18n }} + +
+ + +
+ + {{ "centralizeDataOwnershipWarningDesc" | i18n }} + + + {{ "centralizeDataOwnershipWarningLink" | i18n }} + + +
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts index 59670457d88..e1b2f14d457 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts @@ -1,18 +1,30 @@ -import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from "@angular/core"; -import { lastValueFrom } from "rxjs"; +import { + ChangeDetectionStrategy, + Component, + OnInit, + signal, + Signal, + TemplateRef, + viewChild, + WritableSignal, +} from "@angular/core"; +import { Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrgKey } from "@bitwarden/common/types/key"; -import { CenterPositionStrategy, DialogService } from "@bitwarden/components"; import { EncString } from "@bitwarden/sdk-internal"; import { SharedModule } from "../../../../shared"; import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component"; +import { OrganizationDataOwnershipPolicyDialogComponent } from "../policy-edit-dialogs"; -interface VNextPolicyRequest { +export interface VNextPolicyRequest { policy: PolicyRequest; metadata: { defaultUserCollectionName: string; @@ -20,11 +32,17 @@ interface VNextPolicyRequest { } export class vNextOrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { - name = "organizationDataOwnership"; - description = "organizationDataOwnershipDesc"; + name = "centralizeDataOwnership"; + description = "centralizeDataOwnershipDesc"; type = PolicyType.OrganizationDataOwnership; component = vNextOrganizationDataOwnershipPolicyComponent; showDescription = false; + + editDialogComponent = OrganizationDataOwnershipPolicyDialogComponent; + + override display$(organization: Organization, configService: ConfigService): Observable { + return configService.getFeatureFlag$(FeatureFlag.MigrateMyVaultToMyItems); + } } @Component({ @@ -38,27 +56,16 @@ export class vNextOrganizationDataOwnershipPolicyComponent implements OnInit { constructor( - private dialogService: DialogService, private i18nService: I18nService, private encryptService: EncryptService, ) { super(); } + private readonly policyForm: Signal | undefined> = viewChild("step0"); + private readonly warningContent: Signal | undefined> = viewChild("step1"); + protected readonly step: WritableSignal = signal(0); - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("dialog", { static: true }) warningContent!: TemplateRef; - - override async confirm(): Promise { - if (this.policyResponse?.enabled && !this.enabled.value) { - const dialogRef = this.dialogService.open(this.warningContent, { - positionStrategy: new CenterPositionStrategy(), - }); - const result = await lastValueFrom(dialogRef.closed); - return Boolean(result); - } - return true; - } + protected steps = [this.policyForm, this.warningContent]; async buildVNextRequest(orgKey: OrgKey): Promise { if (!this.policy) { @@ -90,4 +97,8 @@ export class vNextOrganizationDataOwnershipPolicyComponent return encrypted.encryptedString; } + + setStep(step: number) { + this.step.set(step); + } } 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 c633ff5f421..f1b3d04cc7a 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 @@ -16,6 +16,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; import { DIALOG_DATA, DialogConfig, @@ -28,7 +29,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/vnext-organization-data-ownership.component"; +import { VNextPolicyRequest } from "./policy-edit-definitions/organization-data-ownership.component"; export type PolicyEditDialogData = { /** @@ -73,13 +74,24 @@ export class PolicyEditDialogComponent implements AfterViewInit { private formBuilder: FormBuilder, protected dialogRef: DialogRef, protected toastService: ToastService, - private keyService: KeyService, + protected keyService: KeyService, ) {} get policy(): BasePolicyEditDefinition { return this.data.policy; } + /** + * Type guard to check if the policy component has the buildVNextRequest method. + */ + private hasVNextRequest( + component: BasePolicyEditComponent, + ): component is BasePolicyEditComponent & { + buildVNextRequest: (orgKey: OrgKey) => Promise; + } { + return "buildVNextRequest" in component && typeof component.buildVNextRequest === "function"; + } + /** * Instantiates the child policy component and inserts it into the view. */ @@ -129,7 +141,7 @@ export class PolicyEditDialogComponent implements AfterViewInit { } try { - if (this.policyComponent instanceof vNextOrganizationDataOwnershipPolicyComponent) { + if (this.hasVNextRequest(this.policyComponent)) { await this.handleVNextSubmission(this.policyComponent); } else { await this.handleStandardSubmission(); @@ -158,7 +170,9 @@ export class PolicyEditDialogComponent implements AfterViewInit { } private async handleVNextSubmission( - policyComponent: vNextOrganizationDataOwnershipPolicyComponent, + policyComponent: BasePolicyEditComponent & { + buildVNextRequest: (orgKey: OrgKey) => Promise; + }, ): Promise { const orgKey = await firstValueFrom( this.accountService.activeAccount$.pipe( @@ -173,12 +187,12 @@ export class PolicyEditDialogComponent implements AfterViewInit { throw new Error("No encryption key for this organization."); } - const vNextRequest = await policyComponent.buildVNextRequest(orgKey); + const request = await policyComponent.buildVNextRequest(orgKey); await this.policyApiService.putPolicyVNext( this.data.organizationId, this.data.policy.type, - vNextRequest, + request, ); } static open = (dialogService: DialogService, config: DialogConfig) => { 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/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.html similarity index 100% rename from apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.html rename to apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.html 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/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts similarity index 94% rename from apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts rename to apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts index 9dfb8ebb7e7..fbdeffc71bb 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/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts @@ -41,20 +41,15 @@ import { } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { SharedModule } from "../../../shared"; - -import { AutoConfirmPolicyEditComponent } from "./policy-edit-definitions/auto-confirm-policy.component"; +import { SharedModule } from "../../../../shared"; +import { AutoConfirmPolicyEditComponent } from "../policy-edit-definitions/auto-confirm-policy.component"; import { PolicyEditDialogComponent, PolicyEditDialogData, PolicyEditDialogResult, -} from "./policy-edit-dialog.component"; +} from "../policy-edit-dialog.component"; -export type MultiStepSubmit = { - sideEffect: () => Promise; - footerContent: Signal | undefined>; - titleContent: Signal | undefined>; -}; +import { MultiStepSubmit } from "./models"; export type AutoConfirmPolicyDialogData = PolicyEditDialogData & { firstTimeDialog?: boolean; @@ -202,6 +197,7 @@ export class AutoConfirmPolicyDialogComponent } const autoConfirmRequest = await this.policyComponent.buildRequest(); + await this.policyApiService.putPolicy( this.data.organizationId, this.data.policy.type, @@ -235,7 +231,7 @@ export class AutoConfirmPolicyDialogComponent data: null, }; - await this.policyApiService.putPolicy( + await this.policyApiService.putPolicyVNext( this.data.organizationId, PolicyType.SingleOrg, singleOrgRequest, @@ -260,7 +256,10 @@ export class AutoConfirmPolicyDialogComponent try { const multiStepSubmit = await firstValueFrom(this.multiStepSubmit); - await multiStepSubmit[this.currentStep()].sideEffect(); + const sideEffect = multiStepSubmit[this.currentStep()].sideEffect; + if (sideEffect) { + await sideEffect(); + } if (this.currentStep() === multiStepSubmit.length - 1) { this.dialogRef.close("saved"); diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/index.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/index.ts new file mode 100644 index 00000000000..307d0da04b0 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/index.ts @@ -0,0 +1,3 @@ +export * from "./auto-confirm-edit-policy-dialog.component"; +export * from "./organization-data-ownership-edit-policy-dialog.component"; +export * from "./models"; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/models.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/models.ts new file mode 100644 index 00000000000..86120623701 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/models.ts @@ -0,0 +1,7 @@ +import { Signal, TemplateRef } from "@angular/core"; + +export type MultiStepSubmit = { + sideEffect?: () => Promise; + footerContent: Signal | undefined>; + titleContent: Signal | undefined>; +}; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html new file mode 100644 index 00000000000..73691e94199 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html @@ -0,0 +1,72 @@ +
+ + + @let title = multiStepSubmit()[currentStep()]?.titleContent(); + @if (title) { + + } + + + + @if (loading) { +
+ + {{ "loading" | i18n }} +
+ } +
+ @if (policy.showDescription) { +

{{ policy.description | i18n }}

+ } +
+ +
+ + @let footer = multiStepSubmit()[currentStep()]?.footerContent(); + @if (footer) { + + } + +
+
+ + + {{ policy.name | i18n }} + + + + {{ "centralizeDataOwnershipWarningTitle" | i18n }} + + + + + + + + + + + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts new file mode 100644 index 00000000000..7869eab0063 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts @@ -0,0 +1,224 @@ +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Inject, + signal, + TemplateRef, + viewChild, + WritableSignal, +} from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { + catchError, + combineLatest, + defer, + firstValueFrom, + from, + map, + Observable, + of, + startWith, + switchMap, +} from "rxjs"; + +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + DIALOG_DATA, + DialogConfig, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; + +import { SharedModule } from "../../../../shared"; +import { vNextOrganizationDataOwnershipPolicyComponent } from "../policy-edit-definitions"; +import { + PolicyEditDialogComponent, + PolicyEditDialogData, + PolicyEditDialogResult, +} from "../policy-edit-dialog.component"; + +import { MultiStepSubmit } from "./models"; + +/** + * Custom policy dialog component for Centralize Organization Data + * Ownership policy. Satisfies the PolicyDialogComponent interface + * structurally via its static open() function. + */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + templateUrl: "organization-data-ownership-edit-policy-dialog.component.html", + imports: [SharedModule], +}) +export class OrganizationDataOwnershipPolicyDialogComponent + extends PolicyEditDialogComponent + implements AfterViewInit +{ + policyType = PolicyType; + + protected centralizeDataOwnershipEnabled$: Observable = defer(() => + from( + this.policyApiService.getPolicy( + this.data.organizationId, + PolicyType.OrganizationDataOwnership, + ), + ).pipe( + map((policy) => policy.enabled), + catchError(() => of(false)), + ), + ); + + protected readonly currentStep: WritableSignal = signal(0); + protected readonly multiStepSubmit: WritableSignal = signal([]); + + private readonly policyForm = viewChild.required>("step0"); + private readonly warningContent = viewChild.required>("step1"); + private readonly policyFormTitle = viewChild.required>("step0Title"); + private readonly warningTitle = viewChild.required>("step1Title"); + + override policyComponent: vNextOrganizationDataOwnershipPolicyComponent | undefined; + + constructor( + @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, + accountService: AccountService, + policyApiService: PolicyApiServiceAbstraction, + i18nService: I18nService, + cdr: ChangeDetectorRef, + formBuilder: FormBuilder, + dialogRef: DialogRef, + toastService: ToastService, + protected keyService: KeyService, + ) { + super( + data, + accountService, + policyApiService, + i18nService, + cdr, + formBuilder, + dialogRef, + toastService, + keyService, + ); + } + + async ngAfterViewInit() { + await super.ngAfterViewInit(); + + if (this.policyComponent) { + this.saveDisabled$ = combineLatest([ + this.centralizeDataOwnershipEnabled$, + this.policyComponent.enabled.valueChanges.pipe( + startWith(this.policyComponent.enabled.value), + ), + ]).pipe(map(([policyEnabled, value]) => !policyEnabled && !value)); + } + + this.multiStepSubmit.set(this.buildMultiStepSubmit()); + } + + private buildMultiStepSubmit(): MultiStepSubmit[] { + if (this.policyComponent?.policyResponse?.enabled) { + return [ + { + sideEffect: () => this.handleSubmit(), + footerContent: this.policyForm, + titleContent: this.policyFormTitle, + }, + ]; + } + + return [ + { + footerContent: this.policyForm, + titleContent: this.policyFormTitle, + }, + { + sideEffect: () => this.handleSubmit(), + footerContent: this.warningContent, + titleContent: this.warningTitle, + }, + ]; + } + + private async handleSubmit() { + if (!this.policyComponent) { + throw new Error("PolicyComponent not initialized."); + } + + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + ), + ); + + assertNonNullish(orgKey, "Org key not provided"); + + const request = await this.policyComponent.buildVNextRequest( + orgKey[this.data.organizationId as OrganizationId], + ); + + await this.policyApiService.putPolicyVNext( + this.data.organizationId, + this.data.policy.type, + request, + ); + + 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"); + } + } + + submit = async () => { + if (!this.policyComponent) { + throw new Error("PolicyComponent not initialized."); + } + + if ((await this.policyComponent.confirm()) == false) { + this.dialogRef.close(); + return; + } + + try { + const sideEffect = this.multiStepSubmit()[this.currentStep()].sideEffect; + if (sideEffect) { + await sideEffect(); + } + + if (this.currentStep() === this.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( + OrganizationDataOwnershipPolicyDialogComponent, + config, + ); + }; +} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index bf7c1a4c908..e7cc7c6cb5c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5915,6 +5915,37 @@ } } }, + "centralizeDataOwnership":{ + "message": "Centralize organization ownership" + }, + "centralizeDataOwnershipDesc":{ + "message": "All member items will be owned and managed by the organization. Admins and owners are exempt. " + }, + "centralizeDataOwnershipContentAnchor": { + "message": "Learn more about centralized ownership", + "description": "This will be used as a hyperlink" + }, + "benefits":{ + "message": "Benefits" + }, + "centralizeDataOwnershipBenefit1":{ + "message": "Gain full visibility into credential health, including shared and unshared items." + }, + "centralizeDataOwnershipBenefit2":{ + "message": "Easily transfer items during member offboarding and succession, ensuring there are no access gaps." + }, + "centralizeDataOwnershipBenefit3":{ + "message": "Give all users a dedicated \"My Items\" space for managing their own logins." + }, + "centralizeDataOwnershipWarningTitle": { + "message": "Prompt members to transfer their items" + }, + "centralizeDataOwnershipWarningDesc": { + "message": "If members have items in their individual vault, they will be prompted to either transfer them to the organization or leave. If they leave, their access is revoked but can be restored anytime." + }, + "centralizeDataOwnershipWarningLink": { + "message": "Learn more about the transfer" + }, "organizationDataOwnership": { "message": "Enforce organization data ownership" },