From d240d963685daf9c93944f692618f76d9114c206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 6 Dec 2022 09:50:24 +0000 Subject: [PATCH] [EC-342] Gate custom permissions behind enterprise plan (#3907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [EC-342] Add 'UseCustomPermissions' property in Organization. * [EC-342] Add/Edit message texts for Permission types * [EC-342] Add check to determine if org can have custom permissions * [EC-342] Add description to message text * [EC-342] Checking if the selected user type is 'Custom' * Update apps/web/src/locales/en/messages.json Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [EC-342] Update custom permissions check to only look for UseCustomPermissions flag. Create updateUser and inviteUser methods. * [EC-342] Split Custom Permissions text into 3 parts. Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../manage/user-add-edit.component.html | 14 +++- .../manage/user-add-edit.component.ts | 68 ++++++++++++------- apps/web/src/locales/en/messages.json | 25 +++++-- .../src/models/data/organization.data.ts | 2 + libs/common/src/models/domain/organization.ts | 2 + .../response/profile-organization.response.ts | 2 + 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/apps/web/src/app/organizations/manage/user-add-edit.component.html b/apps/web/src/app/organizations/manage/user-add-edit.component.html index 458d1d0bab..6d44a6e8d1 100644 --- a/apps/web/src/app/organizations/manage/user-add-edit.component.html +++ b/apps/web/src/app/organizations/manage/user-add-edit.component.html @@ -122,10 +122,22 @@ id="userTypeCustom" [value]="organizationUserType.Custom" [(ngModel)]="type" + [attr.disabled]="!canUseCustomPermissions || null" /> diff --git a/apps/web/src/app/organizations/manage/user-add-edit.component.ts b/apps/web/src/app/organizations/manage/user-add-edit.component.ts index 4a9d01efb5..cb59311223 100644 --- a/apps/web/src/app/organizations/manage/user-add-edit.component.ts +++ b/apps/web/src/app/organizations/manage/user-add-edit.component.ts @@ -4,6 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; @@ -43,6 +44,7 @@ export class UserAddEditComponent implements OnInit { formPromise: Promise; deletePromise: Promise; organizationUserType = OrganizationUserType; + canUseCustomPermissions: boolean; manageAllCollectionsCheckboxes = [ { @@ -84,11 +86,14 @@ export class UserAddEditComponent implements OnInit { private i18nService: I18nService, private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService, + private organizationService: OrganizationService, private logService: LogService ) {} async ngOnInit() { this.editMode = this.loading = this.organizationUserId != null; + const organization = this.organizationService.get(this.organizationId); + this.canUseCustomPermissions = organization.useCustomPermissions; await this.loadCollections(); if (this.editMode) { @@ -163,6 +168,15 @@ export class UserAddEditComponent implements OnInit { } async submit() { + if (!this.canUseCustomPermissions && this.type === OrganizationUserType.Custom) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("customNonEnterpriseError") + ); + return; + } + let collections: SelectionReadOnlyRequest[] = null; if (this.access !== "all") { collections = this.collections @@ -172,30 +186,9 @@ export class UserAddEditComponent implements OnInit { try { if (this.editMode) { - const request = new OrganizationUserUpdateRequest(); - request.accessAll = this.access === "all"; - request.type = this.type; - request.collections = collections; - request.permissions = this.setRequestPermissions( - request.permissions ?? new PermissionsApi(), - request.type !== OrganizationUserType.Custom - ); - this.formPromise = this.apiService.putOrganizationUser( - this.organizationId, - this.organizationUserId, - request - ); + this.updateUser(collections); } else { - const request = new OrganizationUserInviteRequest(); - request.emails = [...new Set(this.emails.trim().split(/\s*,\s*/))]; - request.accessAll = this.access === "all"; - request.type = this.type; - request.permissions = this.setRequestPermissions( - request.permissions ?? new PermissionsApi(), - request.type !== OrganizationUserType.Custom - ); - request.collections = collections; - this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request); + this.inviteUser(collections); } await this.formPromise; this.platformUtilsService.showToast( @@ -301,4 +294,33 @@ export class UserAddEditComponent implements OnInit { this.logService.error(e); } } + + updateUser(collections: SelectionReadOnlyRequest[]) { + const request = new OrganizationUserUpdateRequest(); + request.accessAll = this.access === "all"; + request.type = this.type; + request.collections = collections; + request.permissions = this.setRequestPermissions( + request.permissions ?? new PermissionsApi(), + request.type !== OrganizationUserType.Custom + ); + this.formPromise = this.apiService.putOrganizationUser( + this.organizationId, + this.organizationUserId, + request + ); + } + + inviteUser(collections: SelectionReadOnlyRequest[]) { + const request = new OrganizationUserInviteRequest(); + request.emails = [...new Set(this.emails.trim().split(/\s*,\s*/))]; + request.accessAll = this.access === "all"; + request.type = this.type; + request.permissions = this.setRequestPermissions( + request.permissions ?? new PermissionsApi(), + request.type !== OrganizationUserType.Custom + ); + request.collections = collections; + this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request); + } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d126bb611d..94cb80076d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2444,7 +2444,7 @@ "message": "Owner" }, "ownerDesc": { - "message": "The highest access user that can manage all aspects of your organization." + "message": "Manage all aspects of your organization, including billing and subscriptions" }, "clientOwnerDesc": { "message": "This user should be independent of the Provider. If the Provider is disassociated with the organization, this user will maintain ownership of the organization." @@ -2453,19 +2453,19 @@ "message": "Admin" }, "adminDesc": { - "message": "Admins can access and manage all items, collections and users in your organization." + "message": "Manage organization access, all collections, members, reporting, and security settings" }, "user": { "message": "User" }, "userDesc": { - "message": "A regular user with access to assigned collections in your organization." + "message": "Access and add items to assigned collections" }, "manager": { "message": "Manager" }, "managerDesc": { - "message": "Managers can access and manage assigned collections in your organization." + "message": "Create, delete, and manage access in assigned collections" }, "all": { "message": "All" @@ -4117,7 +4117,22 @@ "message": "Custom" }, "customDesc": { - "message": "Allows more granular control of user permissions for advanced configurations." + "message": "Grant customized permissions to members" + }, + "customDescNonEnterpriseStart": { + "message": "Custom roles is an ", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Custom roles is an enterprise feature. Contact our support team to upgrade your subscription'" + }, + "customDescNonEnterpriseLink": { + "message": "enterprise feature", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Custom roles is an enterprise feature. Contact our support team to upgrade your subscription'" + }, + "customDescNonEnterpriseEnd": { + "message": ". Contact our support team to upgrade your subscription", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Custom roles is an enterprise feature. Contact our support team to upgrade your subscription'" + }, + "customNonEnterpriseError": { + "message": "To enable custom permissions the organization must be on an Enterprise 2020 plan." }, "permissions": { "message": "Permissions" diff --git a/libs/common/src/models/data/organization.data.ts b/libs/common/src/models/data/organization.data.ts index 98ed39ef34..85658d9764 100644 --- a/libs/common/src/models/data/organization.data.ts +++ b/libs/common/src/models/data/organization.data.ts @@ -20,6 +20,7 @@ export class OrganizationData { useSso: boolean; useKeyConnector: boolean; useScim: boolean; + useCustomPermissions: boolean; useResetPassword: boolean; selfHost: boolean; usersGetPremium: boolean; @@ -60,6 +61,7 @@ export class OrganizationData { this.useSso = response.useSso; this.useKeyConnector = response.useKeyConnector; this.useScim = response.useScim; + this.useCustomPermissions = response.useCustomPermissions; this.useResetPassword = response.useResetPassword; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; diff --git a/libs/common/src/models/domain/organization.ts b/libs/common/src/models/domain/organization.ts index 9e1f63ccb0..ef87b3dee1 100644 --- a/libs/common/src/models/domain/organization.ts +++ b/libs/common/src/models/domain/organization.ts @@ -22,6 +22,7 @@ export class Organization { useSso: boolean; useKeyConnector: boolean; useScim: boolean; + useCustomPermissions: boolean; useResetPassword: boolean; selfHost: boolean; usersGetPremium: boolean; @@ -66,6 +67,7 @@ export class Organization { this.useSso = obj.useSso; this.useKeyConnector = obj.useKeyConnector; this.useScim = obj.useScim; + this.useCustomPermissions = obj.useCustomPermissions; this.useResetPassword = obj.useResetPassword; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; diff --git a/libs/common/src/models/response/profile-organization.response.ts b/libs/common/src/models/response/profile-organization.response.ts index 34d72b10d4..1913784829 100644 --- a/libs/common/src/models/response/profile-organization.response.ts +++ b/libs/common/src/models/response/profile-organization.response.ts @@ -18,6 +18,7 @@ export class ProfileOrganizationResponse extends BaseResponse { useSso: boolean; useKeyConnector: boolean; useScim: boolean; + useCustomPermissions: boolean; useResetPassword: boolean; selfHost: boolean; usersGetPremium: boolean; @@ -59,6 +60,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.useSso = this.getResponseProperty("UseSso"); this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false; this.useScim = this.getResponseProperty("UseScim") ?? false; + this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false; this.useResetPassword = this.getResponseProperty("UseResetPassword"); this.selfHost = this.getResponseProperty("SelfHost"); this.usersGetPremium = this.getResponseProperty("UsersGetPremium");