Move custom role paywall to assignment step

This commit is contained in:
Tuan Dang
2023-11-26 13:18:49 +07:00
parent 5b923c25b5
commit 32f5c96dd2
12 changed files with 73 additions and 77 deletions
@@ -6,7 +6,7 @@ import { deleteMembership as deleteMember, findMembership } from "../../helpers/
import { sendMail } from "../../helpers/nodemailer"; import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config"; import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services"; import { EEAuditLogService, EELicenseService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership"; import * as reqValidator from "../../validation/membership";
import { import {
@@ -137,6 +137,13 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
workspace: membershipToChangeRole.workspace workspace: membershipToChangeRole.workspace
}); });
if (!wsRole) throw BadRequestError({ message: "Role not found" }); if (!wsRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(wsRole.organization);
if (!plan.rbac) return res.status(400).send({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await Membership.findByIdAndUpdate(membershipId, { const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM, role: CUSTOM,
customRole: wsRole customRole: wsRole
@@ -15,7 +15,7 @@ import {
} from "../../helpers/organization"; } from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg"; import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM } from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables";
import * as reqValidator from "../../validation/organization"; import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import { import {
@@ -23,6 +23,7 @@ import {
OrgPermissionSubjects, OrgPermissionSubjects,
getUserOrgPermissions getUserOrgPermissions
} from "../../ee/services/RoleService"; } from "../../ee/services/RoleService";
import { EELicenseService } from "../../ee/services";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
/** /**
@@ -152,10 +153,22 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
OrgPermissionSubjects.Member OrgPermissionSubjects.Member
); );
const isCustomRole = !["admin", "member"].includes(role); const isCustomRole = ![ADMIN, MEMBER].includes(role);
if (isCustomRole) { if (isCustomRole) {
const orgRole = await Role.findOne({ slug: role, isOrgRole: true }); const orgRole = await Role.findOne({
slug: role,
isOrgRole: true,
organization: new Types.ObjectId(organizationId)
});
if (!orgRole) throw BadRequestError({ message: "Role not found" }); if (!orgRole) throw BadRequestError({ message: "Role not found" });
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.rbac) return res.status(400).send({
message:
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, { const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
role: CUSTOM, role: CUSTOM,
@@ -30,7 +30,7 @@ import {
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors"; import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
import { ADMIN, MEMBER, VIEWER } from "../../variables"; import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
@@ -571,7 +571,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
machineMembership = await new MachineMembership({ machineMembership = await new MachineMembership({
machineIdentity: machineIdentity._id, machineIdentity: machineIdentity._id,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
role, role: customRole ? CUSTOM : role,
customRole customRole
}).save(); }).save();
+1 -1
View File
@@ -66,7 +66,7 @@ class EELicenseService {
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,
rbac: true, rbac: false,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
@@ -385,7 +385,7 @@ export const getRolePermissions = async (role: string, workspaceId: string) => {
* @param ability * @param ability
* @returns * @returns
*/ */
const extractPermissions = (ability: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet) => { const extractPermissions = (ability: any) => {
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`); return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
} }
+1 -1
View File
@@ -178,7 +178,7 @@ export const getOrgRolePermissions = async (role: string, orgId: string) => {
* @param ability * @param ability
* @returns * @returns
*/ */
const extractPermissions = (ability: MongoAbility<OrgPermissionSet> | OrgPermissionSet) => { const extractPermissions = (ability: any) => {
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`); return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
} }
@@ -57,7 +57,9 @@ export const OrgMembersSection = () => {
} }
if (isMoreUsersNotAllowed) { if (isMoreUsersNotAllowed) {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan."
});
} else { } else {
handlePopUpOpen("addMember"); handlePopUpOpen("addMember");
} }
@@ -134,7 +136,7 @@ export const OrgMembersSection = () => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add more members if you upgrade your Infisical plan." text={(popUp.upgradePlan?.data as { description: string })?.description}
/> />
<EmailServiceSetupModal <EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen} isOpen={popUp.setUpEmail?.isOpen}
@@ -28,8 +28,8 @@ import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrganization, useOrganization,
useUser useSubscription,
} from "@app/context"; useUser} from "@app/context";
import { import {
useAddUserToOrg, useAddUserToOrg,
useFetchServerStatus, useFetchServerStatus,
@@ -41,10 +41,11 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["removeMember"]>, popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
data?: { data?: {
orgMembershipId?: string; orgMembershipId?: string;
email?: string; email?: string;
description?: string;
} }
) => void; ) => void;
setCompleteInviteLink: (link: string) => void; setCompleteInviteLink: (link: string) => void;
@@ -55,6 +56,7 @@ export const OrgMembersTable = ({
setCompleteInviteLink setCompleteInviteLink
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { user } = useUser(); const { user } = useUser();
const userId = user?._id || ""; const userId = user?._id || "";
@@ -63,7 +65,7 @@ export const OrgMembersTable = ({
const { data: roles, isLoading: isRolesLoading } = useGetRoles({ const { data: roles, isLoading: isRolesLoading } = useGetRoles({
orgId orgId
}); });
const [searchMemberFilter, setSearchMemberFilter] = useState(""); const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
@@ -76,10 +78,21 @@ export const OrgMembersTable = ({
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
try { try {
// TODO: replace hardcoding default role
const isCustomRole = !["admin", "member"].includes(role);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description: "You can assign custom roles to members if you upgrade your Infisical plan."
});
return;
}
await updateUserOrgRole({ await updateUserOrgRole({
organizationId: currentOrg?._id, organizationId: currentOrg?._id,
membershipId, role membershipId, role
}); });
createNotification({ createNotification({
text: "Successfully updated user role", text: "Successfully updated user role",
type: "success" type: "success"
@@ -169,7 +182,6 @@ export const OrgMembersTable = ({
({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => { ({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-"; const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail; const email = u?.email || inviteEmail;
return ( return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full"> <Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td> <Td>{name}</Td>
@@ -183,7 +195,7 @@ export const OrgMembersTable = ({
<> <>
{status === "accepted" && ( {status === "accepted" && (
<Select <Select
defaultValue={ value={
role === "custom" ? findRoleFromId(customRole)?.slug : role role === "custom" ? findRoleFromId(customRole)?.slug : role
} }
isDisabled={userId === u?._id || !isAllowed} isDisabled={userId === u?._id || !isAllowed}
@@ -13,9 +13,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, FormControl, Input, UpgradePlanModal } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useCreateRole, useUpdateRole } from "@app/hooks/api"; import { useCreateRole, useUpdateRole } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types"; import { TRole } from "@app/hooks/api/roles/types";
@@ -85,9 +84,6 @@ const SIMPLE_PERMISSION_OPTIONS = [
] as const; ] as const;
export const OrgRoleModifySection = ({ role, onGoBack }: Props) => { export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
const { subscription } = useSubscription();
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["upgradePlan"] as const);
const isNonEditable = ["owner", "admin", "member"].includes(role?.slug || ""); const isNonEditable = ["owner", "admin", "member"].includes(role?.slug || "");
const isNewRole = !role?.slug; const isNewRole = !role?.slug;
@@ -127,11 +123,6 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
}; };
const handleFormSubmit = async (el: TFormSchema) => { const handleFormSubmit = async (el: TFormSchema) => {
if (subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan");
return;
}
if (!isNewRole) { if (!isNewRole) {
await handleRoleUpdate(el); await handleRoleUpdate(el);
return; return;
@@ -235,17 +226,6 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
</Button> </Button>
</div> </div>
</form> </form>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={
subscription.slug === null
? "You can use RBAC under an Enterprise license"
: "You can use RBAC if you switch to Infisical's Team Plan."
}
/>
)}
</div> </div>
); );
}; };
@@ -1,4 +1,3 @@
import { useCallback } from "react";
import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -96,13 +95,6 @@ export const MachineIdentityTable = ({
}); });
} }
} }
const findRoleFromId = useCallback(
(roleId: string) => {
return (roles || []).find(({ _id: id }) => id === roleId);
},
[roles]
);
return ( return (
<TableContainer> <TableContainer>
@@ -141,7 +133,7 @@ export const MachineIdentityTable = ({
return ( return (
<Select <Select
value={ value={
role === "custom" ? findRoleFromId(customRole)?.slug : role role === "custom" ? customRole.slug : role
} }
isDisabled={!isAllowed} isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600" className="w-40 bg-mineshaft-600"
@@ -38,9 +38,9 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useOrganization, useOrganization,
useSubscription,
useUser, useUser,
useWorkspace useWorkspace} from "@app/context";
} from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useAddUserToWs, useAddUserToWs,
@@ -60,6 +60,7 @@ type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
export const MemberListTab = () => { export const MemberListTab = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { subscription } = useSubscription();
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -170,6 +171,15 @@ export const MemberListTab = () => {
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
try { try {
const isCustomRole = !["admin", "member", "viewer"].includes(role);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description: "You can assign custom roles to members if you upgrade your Infisical plan."
});
return;
}
await updateUserWorkspaceRole({ membershipId, role }); await updateUserWorkspaceRole({ membershipId, role });
createNotification({ createNotification({
text: "Successfully updated user role", text: "Successfully updated user role",
@@ -302,7 +312,7 @@ export const MemberListTab = () => {
{(isAllowed) => ( {(isAllowed) => (
<> <>
<Select <Select
defaultValue={ value={
role === "custom" ? findRoleFromId(customRole)?.slug : role role === "custom" ? findRoleFromId(customRole)?.slug : role
} }
isDisabled={userId === u?._id || !isAllowed} isDisabled={userId === u?._id || !isAllowed}
@@ -443,7 +453,7 @@ export const MemberListTab = () => {
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan." text={(popUp.upgradePlan?.data as { description: string })?.description}
/> />
</div> </div>
); );
@@ -18,9 +18,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, FormControl, Input, UpgradePlanModal } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionSub, useOrganization, useSubscription, useWorkspace } from "@app/context"; import { ProjectPermissionSub, useOrganization, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useCreateRole, useUpdateRole } from "@app/hooks/api"; import { useCreateRole, useUpdateRole } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types"; import { TRole } from "@app/hooks/api/roles/types";
@@ -110,8 +109,6 @@ type Props = {
}; };
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => { export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["upgradePlan"] as const);
const isNonEditable = ["admin", "member", "viewer"].includes(role?.slug || ""); const isNonEditable = ["admin", "member", "viewer"].includes(role?.slug || "");
const isNewRole = !role?.slug; const isNewRole = !role?.slug;
@@ -119,7 +116,6 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || ""; const orgId = currentOrg?._id || "";
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { subscription } = useSubscription();
const workspaceId = currentWorkspace?._id || ""; const workspaceId = currentWorkspace?._id || "";
const { const {
@@ -155,11 +151,6 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
}; };
const handleFormSubmit = async (el: TFormSchema) => { const handleFormSubmit = async (el: TFormSchema) => {
if (subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan");
return;
}
if (!isNewRole) { if (!isNewRole) {
await handleRoleUpdate(el); await handleRoleUpdate(el);
return; return;
@@ -282,17 +273,6 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
</Button> </Button>
</div> </div>
</form> </form>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={
subscription.slug === null
? "You can use RBAC under an Enterprise license"
: "You can use RBAC if you switch to Infisical's Team Plan."
}
/>
)}
</div> </div>
); );
}; };