diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index f78ab20702..ad1099af3c 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -9,13 +9,17 @@ import { ViewContainerRef, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Observable, map } from "rxjs"; +import { Observable, map, firstValueFrom, 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 { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { DIALOG_DATA, DialogConfig, @@ -23,8 +27,10 @@ import { DialogService, ToastService, } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; import { BasePolicy, BasePolicyComponent } from "../policies"; +import { vNextOrganizationDataOwnershipPolicyComponent } from "../policies/vnext-organization-data-ownership.component"; export type PolicyEditDialogData = { /** Returns policy abstracts. */ @@ -59,12 +65,15 @@ export class PolicyEditComponent implements AfterViewInit { }); constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, private i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, private toastService: ToastService, + private configService: ConfigService, + private keyService: KeyService, ) {} get policy(): BasePolicy { return this.data.policy; @@ -107,24 +116,70 @@ export class PolicyEditComponent implements AfterViewInit { return; } - let request: PolicyRequest; - try { - request = await this.policyComponent.buildRequest(); - } catch (e) { - this.toastService.showToast({ variant: "error", title: null, message: e.message }); - return; - } + if ( + this.policyComponent instanceof vNextOrganizationDataOwnershipPolicyComponent && + (await this.isVNextEnabled()) + ) { + await this.handleVNextSubmission(this.policyComponent); + } else { + await this.handleStandardSubmission(); + } - await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), - }); - this.dialogRef.close(PolicyEditDialogResult.Saved); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), + }); + this.dialogRef.close(PolicyEditDialogResult.Saved); + } catch (error) { + this.toastService.showToast({ + variant: "error", + title: null, + message: error.message, + }); + } }; + private async isVNextEnabled(): Promise { + const isVNextFeatureEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation), + ); + + return isVNextFeatureEnabled; + } + + private async handleStandardSubmission(): Promise { + const request = await this.policyComponent.buildRequest(); + await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request); + } + + private async handleVNextSubmission( + policyComponent: vNextOrganizationDataOwnershipPolicyComponent, + ): Promise { + const orgKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.orgKeys$(userId)), + map( + (orgKeys: { [key: OrganizationId]: any }) => + orgKeys[this.data.organizationId as OrganizationId] ?? null, + ), + ), + ); + + if (orgKey == null) { + throw new Error("No encryption key for this organization."); + } + + const vNextRequest = await policyComponent.buildVNextRequest(orgKey); + + await this.policyApiService.putPolicyVNext( + this.data.organizationId, + this.data.policy.type, + vNextRequest, + ); + } static open = (dialogService: DialogService, config: DialogConfig) => { return dialogService.open(PolicyEditComponent, config); }; diff --git a/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts index 11b1548d9f..89964e9439 100644 --- a/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts @@ -3,14 +3,26 @@ import { lastValueFrom, 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 { DialogService } from "@bitwarden/components"; +import { EncString } from "@bitwarden/sdk-internal"; import { SharedModule } from "../../../shared"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; +interface VNextPolicyRequest { + policy: PolicyRequest; + metadata: { + defaultUserCollectionName: string; + }; +} + export class vNextOrganizationDataOwnershipPolicy extends BasePolicy { name = "organizationDataOwnership"; description = "organizationDataOwnershipDesc"; @@ -33,7 +45,11 @@ export class vNextOrganizationDataOwnershipPolicyComponent extends BasePolicyComponent implements OnInit { - constructor(private dialogService: DialogService) { + constructor( + private dialogService: DialogService, + private i18nService: I18nService, + private encryptService: EncryptService, + ) { super(); } @@ -47,4 +63,36 @@ export class vNextOrganizationDataOwnershipPolicyComponent } 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: { + type: this.policy.type, + enabled: this.enabled.value, + 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/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts index 4db0fc1675..79055d3cb1 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy-api.service.abstraction.ts @@ -24,4 +24,5 @@ export abstract class PolicyApiServiceAbstraction { type: PolicyType, request: PolicyRequest, ) => Promise; + abstract putPolicyVNext: (organizationId: string, type: PolicyType, request: any) => Promise; } diff --git a/libs/common/src/admin-console/services/policy/policy-api.service.ts b/libs/common/src/admin-console/services/policy/policy-api.service.ts index 8f9854f49c..c0a5c74f1e 100644 --- a/libs/common/src/admin-console/services/policy/policy-api.service.ts +++ b/libs/common/src/admin-console/services/policy/policy-api.service.ts @@ -116,16 +116,32 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { } async putPolicy(organizationId: string, type: PolicyType, request: PolicyRequest): Promise { - const r = await this.apiService.send( + const response = await this.apiService.send( "PUT", "/organizations/" + organizationId + "/policies/" + type, request, true, true, ); + await this.handleResponse(response); + } + + async putPolicyVNext(organizationId: string, type: PolicyType, request: any): Promise { + const response = await this.apiService.send( + "PUT", + `/organizations/${organizationId}/policies/${type}/vnext`, + request, + true, + true, + ); + + await this.handleResponse(response); + } + + private async handleResponse(response: any): Promise { const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const response = new PolicyResponse(r); - const data = new PolicyData(response); + const policyResponse = new PolicyResponse(response); + const data = new PolicyData(policyResponse); await this.policyService.upsert(data, userId); } }