diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index c7ac9910ac5..5481c1f0ab5 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -7,7 +7,7 @@ > {{ "loading" | i18n }} -
+
@@ -52,6 +52,27 @@ {{ "rotateApiKey" | i18n }} + +

{{ "collectionManagement" | i18n }}

+

{{ "collectionManagementDesc" | i18n }}

+ + {{ "limitCollectionCdOwnerAdminDesc" | i18n }} + + + +

{{ "dangerZone" | i18n }}

{{ "dangerZoneDesc" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index f8628ed1f79..df1d8f7c33d 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -1,18 +1,18 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from } from "rxjs"; +import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from, of } from "rxjs"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-collection-management-update.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request"; import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -38,7 +38,6 @@ export class AccountComponent { loading = true; canUseApi = false; org: OrganizationResponse; - formPromise: Promise; taxFormPromise: Promise; // FormGroup validators taken from server Organization domain object @@ -60,6 +59,10 @@ export class AccountComponent { ), }); + protected collectionManagementFormGroup = this.formBuilder.group({ + limitCollectionCdOwnerAdmin: [false], + }); + protected organizationId: string; protected publicKeyBuffer: Uint8Array; @@ -71,7 +74,6 @@ export class AccountComponent { private route: ActivatedRoute, private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService, - private logService: LogService, private router: Router, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, @@ -82,16 +84,16 @@ export class AccountComponent { async ngOnInit() { this.selfHosted = this.platformUtilsService.isSelfHost(); - this.route.parent.parent.params + this.route.params .pipe( - switchMap((params) => { + switchMap((params) => this.organizationService.get$(params.organizationId)), + switchMap((organization) => { return combineLatest([ - // Organization domain - this.organizationService.get$(params.organizationId), + of(organization), // OrganizationResponse for form population - from(this.organizationApiService.get(params.organizationId)), + from(this.organizationApiService.get(organization.id)), // Organization Public Key - from(this.organizationApiService.getKeys(params.organizationId)), + from(this.organizationApiService.getKeys(organization.id)), ]); }), takeUntil(this.destroy$) @@ -102,6 +104,16 @@ export class AccountComponent { this.canEditSubscription = organization.canEditSubscription; this.canUseApi = organization.useApi; + // Update disabled states - reactive forms prefers not using disabled attribute + if (!this.selfHosted) { + this.formGroup.get("orgName").enable(); + } + + if (!this.selfHosted || this.canEditSubscription) { + this.formGroup.get("billingEmail").enable(); + this.formGroup.get("businessName").enable(); + } + // Org Response this.org = orgResponse; @@ -114,16 +126,9 @@ export class AccountComponent { billingEmail: this.org.billingEmail, businessName: this.org.businessName, }); - - // Update disabled states - reactive forms prefers not using disabled attribute - if (!this.selfHosted) { - this.formGroup.get("orgName").enable(); - } - - if (!this.selfHosted || this.canEditSubscription) { - this.formGroup.get("billingEmail").enable(); - this.formGroup.get("businessName").enable(); - } + this.collectionManagementFormGroup.patchValue({ + limitCollectionCdOwnerAdmin: this.org.limitCollectionCdOwnerAdmin, + }); this.loading = false; }); @@ -153,11 +158,25 @@ export class AccountComponent { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - this.formPromise = this.organizationApiService.save(this.organizationId, request); - await this.formPromise; + await this.organizationApiService.save(this.organizationId, request); + this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated")); }; + submitCollectionManagement = async () => { + const request = new OrganizationCollectionManagementUpdateRequest(); + request.limitCreateDeleteOwnerAdmin = + this.collectionManagementFormGroup.value.limitCollectionCdOwnerAdmin; + + await this.organizationApiService.updateCollectionManagement(this.organizationId, request); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("collectionManagementUpdated") + ); + }; + async deleteOrganization() { const dialog = openDeleteOrganizationDialog(this.dialogService, { data: { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 4617065b730..b168682ef6d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7073,6 +7073,18 @@ } } }, + "collectionManagement": { + "message": "Collection management" + }, + "collectionManagementDesc": { + "message": "Manage the collection behavior for the organization" + }, + "limitCollectionCdOwnerAdminDesc": { + "message": "Limit collection creation and deletion to owners and admins" + }, + "collectionManagementUpdated": { + "message": "Collection management behavior saved" + }, "passwordManagerPlanPrice": { "message": "Password Manager plan price" }, diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index a1792b1fe75..ae62cad5fdb 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -18,6 +18,7 @@ import { StorageRequest } from "../../../models/request/storage.request"; import { VerifyBankRequest } from "../../../models/request/verify-bank.request"; import { ListResponse } from "../../../models/response/list.response"; import { OrganizationApiKeyType } from "../../enums"; +import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request"; import { OrganizationCreateRequest } from "../../models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "../../models/request/organization-update.request"; @@ -73,4 +74,8 @@ export class OrganizationApiServiceAbstraction { id: string, request: SecretsManagerSubscribeRequest ) => Promise; + updateCollectionManagement: ( + id: string, + request: OrganizationCollectionManagementUpdateRequest + ) => Promise; } diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index 10f5e9f2625..ff3a041ffcf 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -49,6 +49,7 @@ export class OrganizationData { familySponsorshipValidUntil?: Date; familySponsorshipToDelete?: boolean; accessSecretsManager: boolean; + limitCollectionCdOwnerAdmin: boolean; constructor( response: ProfileOrganizationResponse, @@ -100,6 +101,7 @@ export class OrganizationData { this.familySponsorshipValidUntil = response.familySponsorshipValidUntil; this.familySponsorshipToDelete = response.familySponsorshipToDelete; this.accessSecretsManager = response.accessSecretsManager; + this.limitCollectionCdOwnerAdmin = response.limitCollectionCdOwnerAdmin; this.isMember = options.isMember; this.isProviderUser = options.isProviderUser; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index e1e5e8a6e2c..e192a889a55 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -64,6 +64,10 @@ export class Organization { familySponsorshipValidUntil?: Date; familySponsorshipToDelete?: boolean; accessSecretsManager: boolean; + /** + * Refers to the ability for an organization to limit collection creation and deletion to owners and admins only + */ + limitCollectionCdOwnerAdmin: boolean; constructor(obj?: OrganizationData) { if (obj == null) { @@ -115,6 +119,7 @@ export class Organization { this.familySponsorshipValidUntil = obj.familySponsorshipValidUntil; this.familySponsorshipToDelete = obj.familySponsorshipToDelete; this.accessSecretsManager = obj.accessSecretsManager; + this.limitCollectionCdOwnerAdmin = obj.limitCollectionCdOwnerAdmin; } get canAccess() { diff --git a/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts b/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts new file mode 100644 index 00000000000..1c6ed27f19c --- /dev/null +++ b/libs/common/src/admin-console/models/request/organization-collection-management-update.request.ts @@ -0,0 +1,3 @@ +export class OrganizationCollectionManagementUpdateRequest { + limitCreateDeleteOwnerAdmin: boolean; +} diff --git a/libs/common/src/admin-console/models/response/organization.response.ts b/libs/common/src/admin-console/models/response/organization.response.ts index b248c6d0df9..081ea1fc77b 100644 --- a/libs/common/src/admin-console/models/response/organization.response.ts +++ b/libs/common/src/admin-console/models/response/organization.response.ts @@ -33,6 +33,7 @@ export class OrganizationResponse extends BaseResponse { smServiceAccounts?: number; maxAutoscaleSmSeats?: number; maxAutoscaleSmServiceAccounts?: number; + limitCollectionCdOwnerAdmin: boolean; constructor(response: any) { super(response); @@ -72,5 +73,6 @@ export class OrganizationResponse extends BaseResponse { this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts"); this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats"); this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts"); + this.limitCollectionCdOwnerAdmin = this.getResponseProperty("LimitCollectionCdOwnerAdmin"); } } diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index e042bf145f8..b7aebd2cd55 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -48,6 +48,7 @@ export class ProfileOrganizationResponse extends BaseResponse { familySponsorshipValidUntil?: Date; familySponsorshipToDelete?: boolean; accessSecretsManager: boolean; + limitCollectionCdOwnerAdmin: boolean; constructor(response: any) { super(response); @@ -105,5 +106,6 @@ export class ProfileOrganizationResponse extends BaseResponse { } this.familySponsorshipToDelete = this.getResponseProperty("FamilySponsorshipToDelete"); this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager"); + this.limitCollectionCdOwnerAdmin = this.getResponseProperty("LimitCollectionCdOwnerAdmin"); } } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 76b5fb0eca0..b22cf70efe6 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -21,6 +21,7 @@ import { ListResponse } from "../../../models/response/list.response"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiKeyType } from "../../enums"; +import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request"; import { OrganizationCreateRequest } from "../../models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "../../models/request/organization-update.request"; @@ -322,4 +323,20 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction ); return new ProfileOrganizationResponse(r); } + + async updateCollectionManagement( + id: string, + request: OrganizationCollectionManagementUpdateRequest + ): Promise { + const r = await this.apiService.send( + "PUT", + "/organizations/" + id + "/collection-management", + request, + true, + true + ); + const data = new OrganizationResponse(r); + await this.syncService.fullSync(true); + return data; + } }