mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 12:40:26 +00:00
[PM-30500] Centralize Organization Data Ownership (#18387)
* remove deprecated OrganizationDataOwnership components, promote vNext * WIP: add new components and copy * multi step dialog for organization- data ownership * disable save * clean up copy, fix bug * copy change, update button text * update copy * un-rename model * use policyApiService * simplify style
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,8 +1,57 @@
|
||||
<bit-callout type="warning">
|
||||
{{ "personalOwnershipExemption" | i18n }}
|
||||
</bit-callout>
|
||||
<p>
|
||||
{{ "organizationDataOwnershipDescContent" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<ng-template #dialog>
|
||||
<bit-simple-dialog background="alt">
|
||||
<span bitDialogTitle>{{ "organizationDataOwnershipWarningTitle" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-text-left tw-overflow-hidden">
|
||||
{{ "organizationDataOwnershipWarningContentTop" | i18n }}
|
||||
<div class="tw-flex tw-flex-col tw-p-2">
|
||||
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ "organizationDataOwnershipWarningContentBottom" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<span class="tw-flex tw-gap-2">
|
||||
<button bitButton buttonType="primary" [bitDialogClose]="true" type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" [bitDialogClose]="false" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
</ng-template>
|
||||
|
||||
@@ -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<boolean> {
|
||||
// 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<boolean> {
|
||||
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<unknown>;
|
||||
|
||||
override async confirm(): Promise<boolean> {
|
||||
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<VNextPolicyRequest> {
|
||||
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<EncString> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,52 @@
|
||||
<p>
|
||||
{{ "organizationDataOwnershipDescContent" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
<ng-container [ngTemplateOutlet]="steps[step()]()"></ng-container>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<ng-template #step0>
|
||||
<p>
|
||||
{{ "centralizeDataOwnershipDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "centralizeDataOwnershipContentAnchor" | i18n }}
|
||||
<i class="bwi bwi-external-link"></i>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<ng-template #dialog>
|
||||
<bit-simple-dialog background="alt">
|
||||
<span bitDialogTitle>{{ "organizationDataOwnershipWarningTitle" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-text-left tw-overflow-hidden">
|
||||
{{ "organizationDataOwnershipWarningContentTop" | i18n }}
|
||||
<div class="tw-flex tw-flex-col tw-p-2">
|
||||
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ "organizationDataOwnershipWarningContentBottom" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<span class="tw-flex tw-gap-2">
|
||||
<button bitButton buttonType="primary" [bitDialogClose]="true" type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" [bitDialogClose]="false" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
<div class="tw-text-left tw-overflow-hidden tw-mb-2">
|
||||
<strong>{{ "benefits" | i18n }}:</strong>
|
||||
<ul class="tw-pl-7 tw-space-y-2 tw-pt-2">
|
||||
<li>
|
||||
{{ "centralizeDataOwnershipBenefit1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "centralizeDataOwnershipBenefit2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "centralizeDataOwnershipBenefit3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<bit-form-control>
|
||||
<input class="tw-mt-4" type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step1>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2 tw-overflow-hidden">
|
||||
<span>
|
||||
{{ "centralizeDataOwnershipWarningDesc" | i18n }}
|
||||
</span>
|
||||
<a
|
||||
class="tw-mt-4"
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "centralizeDataOwnershipWarningLink" | i18n }}
|
||||
<i class="bwi bwi-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<TemplateRef<any> | undefined> = viewChild("step0");
|
||||
private readonly warningContent: Signal<TemplateRef<any> | undefined> = viewChild("step1");
|
||||
protected readonly step: WritableSignal<number> = 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<unknown>;
|
||||
|
||||
override async confirm(): Promise<boolean> {
|
||||
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<VNextPolicyRequest> {
|
||||
if (!this.policy) {
|
||||
@@ -90,4 +97,8 @@ export class vNextOrganizationDataOwnershipPolicyComponent
|
||||
|
||||
return encrypted.encryptedString;
|
||||
}
|
||||
|
||||
setStep(step: number) {
|
||||
this.step.set(step);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PolicyEditDialogResult>,
|
||||
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<VNextPolicyRequest>;
|
||||
} {
|
||||
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<VNextPolicyRequest>;
|
||||
},
|
||||
): Promise<void> {
|
||||
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<PolicyEditDialogData>) => {
|
||||
|
||||
@@ -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<void>;
|
||||
footerContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
titleContent: Signal<TemplateRef<unknown> | 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");
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./auto-confirm-edit-policy-dialog.component";
|
||||
export * from "./organization-data-ownership-edit-policy-dialog.component";
|
||||
export * from "./models";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Signal, TemplateRef } from "@angular/core";
|
||||
|
||||
export type MultiStepSubmit = {
|
||||
sideEffect?: () => Promise<void>;
|
||||
footerContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
titleContent: Signal<TemplateRef<unknown> | undefined>;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading">
|
||||
<ng-container bitDialogTitle>
|
||||
@let title = multiStepSubmit()[currentStep()]?.titleContent();
|
||||
@if (title) {
|
||||
<ng-container [ngTemplateOutlet]="title"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogContent>
|
||||
@if (loading) {
|
||||
<div>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
}
|
||||
<div [hidden]="loading">
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
</div>
|
||||
<ng-template #policyForm></ng-template>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
@let footer = multiStepSubmit()[currentStep()]?.footerContent();
|
||||
@if (footer) {
|
||||
<ng-container [ngTemplateOutlet]="footer"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
<ng-template #step0Title>
|
||||
{{ policy.name | i18n }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step1Title>
|
||||
{{ "centralizeDataOwnershipWarningTitle" | i18n }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step0>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="saveDisabled$ | async"
|
||||
bitFormButton
|
||||
type="submit"
|
||||
>
|
||||
@if (policyComponent?.policyResponse?.enabled) {
|
||||
{{ "save" | i18n }}
|
||||
} @else {
|
||||
{{ "continue" | i18n }}
|
||||
}
|
||||
</button>
|
||||
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #step1>
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -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<boolean> = defer(() =>
|
||||
from(
|
||||
this.policyApiService.getPolicy(
|
||||
this.data.organizationId,
|
||||
PolicyType.OrganizationDataOwnership,
|
||||
),
|
||||
).pipe(
|
||||
map((policy) => policy.enabled),
|
||||
catchError(() => of(false)),
|
||||
),
|
||||
);
|
||||
|
||||
protected readonly currentStep: WritableSignal<number> = signal(0);
|
||||
protected readonly multiStepSubmit: WritableSignal<MultiStepSubmit[]> = signal([]);
|
||||
|
||||
private readonly policyForm = viewChild.required<TemplateRef<unknown>>("step0");
|
||||
private readonly warningContent = viewChild.required<TemplateRef<unknown>>("step1");
|
||||
private readonly policyFormTitle = viewChild.required<TemplateRef<unknown>>("step0Title");
|
||||
private readonly warningTitle = viewChild.required<TemplateRef<unknown>>("step1Title");
|
||||
|
||||
override policyComponent: vNextOrganizationDataOwnershipPolicyComponent | undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
||||
accountService: AccountService,
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
cdr: ChangeDetectorRef,
|
||||
formBuilder: FormBuilder,
|
||||
dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||
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<PolicyEditDialogData>) => {
|
||||
return dialogService.open<PolicyEditDialogResult>(
|
||||
OrganizationDataOwnershipPolicyDialogComponent,
|
||||
config,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user