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");