Merge pull request #3682 from Infisical/add-managed-permission

add manage permission for billing
This commit is contained in:
Maidul Islam
2025-05-29 18:51:35 -04:00
committed by GitHub
19 changed files with 273 additions and 71 deletions

View File

@@ -17,7 +17,7 @@ import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums";
import { TLicenseDALFactory } from "./license-dal";
@@ -288,7 +288,7 @@ export const licenseServiceFactory = ({
billingCycle
}: TOrgPlansTableDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
@@ -310,8 +310,10 @@ export const licenseServiceFactory = ({
success_url
}: TStartOrgTrialDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -338,8 +340,10 @@ export const licenseServiceFactory = ({
actorOrgId
}: TCreateOrgPortalSession) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -385,7 +389,7 @@ export const licenseServiceFactory = ({
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -413,7 +417,7 @@ export const licenseServiceFactory = ({
// returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -484,7 +488,7 @@ export const licenseServiceFactory = ({
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -509,7 +513,10 @@ export const licenseServiceFactory = ({
email
}: TUpdateOrgBillingDetailsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -529,7 +536,7 @@ export const licenseServiceFactory = ({
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -556,7 +563,10 @@ export const licenseServiceFactory = ({
cancel_url
}: TAddOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -585,7 +595,10 @@ export const licenseServiceFactory = ({
pmtMethodId
}: TDelOrgPmtMethodDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -602,7 +615,7 @@ export const licenseServiceFactory = ({
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -620,7 +633,10 @@ export const licenseServiceFactory = ({
const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -641,7 +657,10 @@ export const licenseServiceFactory = ({
const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing
);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -658,7 +677,7 @@ export const licenseServiceFactory = ({
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {
@@ -675,7 +694,7 @@ export const licenseServiceFactory = ({
const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId);
if (!organization) {

View File

@@ -67,6 +67,11 @@ export enum OrgPermissionGroupActions {
RemoveMembers = "remove-members"
}
export enum OrgPermissionBillingActions {
Read = "read",
ManageBilling = "manage-billing"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@@ -107,7 +112,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
@@ -298,10 +303,8 @@ const buildAdminPermission = () => {
can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups);
can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
can(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionBillingActions.ManageBilling, OrgPermissionSubjects.Billing);
can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
@@ -362,7 +365,7 @@ const buildMemberPermission = () => {
can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);

View File

@@ -142,12 +142,10 @@ Below is a comprehensive list of all available organization-level subjects and t
#### Subject: `billing`
| Action | Description |
| -------- | ------------------------------------------------ |
| `read` | View billing information and subscription status |
| `create` | Set up new payment methods or subscriptions |
| `edit` | Modify billing details or subscription plans |
| `delete` | Remove payment methods or cancel subscriptions |
| Action | Description |
| ---------------- | ------------------------------------------------ |
| `read` | View billing information and subscription status |
| `manage-billing` | Manage billing details and subscription plans |
### Templates & Automation

View File

@@ -2,6 +2,7 @@ export { useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export {
OrgPermissionActions,
OrgPermissionBillingActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects

View File

@@ -7,6 +7,11 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgPermissionBillingActions {
Read = "read",
ManageBilling = "manage-billing"
}
export enum OrgGatewayPermissionActions {
// is there a better word for this. This mean can an identity be a gateway
CreateGateways = "create-gateways",
@@ -100,7 +105,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]

View File

@@ -2,6 +2,7 @@ export { useOrganization } from "./OrganizationContext";
export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgPermissionActions,
OrgPermissionBillingActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects,

View File

@@ -2,7 +2,7 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { BillingTabGroup } from "./components";
@@ -24,7 +24,7 @@ export const BillingPage = () => {
</div>
<OrgPermissionCan
passThrough={false}
I={OrgPermissionActions.Read}
I={OrgPermissionBillingActions.Read}
a={OrgPermissionSubjects.Billing}
>
<BillingTabGroup />

View File

@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionBillingActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
@@ -112,7 +112,7 @@ export const PreviewSection = () => {
Get unlimited members, projects, RBAC, smart alerts, and so much more.
</p>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
onClick={() => handleUpgradeBtnClick()}
@@ -156,7 +156,7 @@ export const PreviewSection = () => {
}`}
</p>
{isInfisicalCloud() && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<button
type="button"

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
const schema = z
@@ -74,7 +74,7 @@ export const CompanyNameSection = () => {
name="name"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
type="submit"

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
const schema = z
@@ -75,7 +75,7 @@ export const InvoiceEmailSection = () => {
name="email"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
type="submit"

View File

@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useAddOrgPmtMethod } from "@app/hooks/api";
import { PmtMethodsTable } from "./PmtMethodsTable";
@@ -27,7 +27,7 @@ export const PmtMethodsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Payment methods</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
onClick={handleAddPmtMethodBtnClick}

View File

@@ -16,7 +16,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
@@ -75,7 +75,7 @@ export const PmtMethodsTable = () => {
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (

View File

@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { TaxIDModal } from "./TaxIDModal";
@@ -18,7 +18,7 @@ export const TaxIDSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Tax ID</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
<OrgPermissionCan I={OrgPermissionBillingActions.ManageBilling} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
onClick={() => handlePopUpOpen("addTaxID")}

View File

@@ -14,7 +14,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = {
@@ -103,7 +103,7 @@ export const TaxIDTable = () => {
<Td>{value}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
I={OrgPermissionBillingActions.ManageBilling}
a={OrgPermissionSubjects.Billing}
>
{(isAllowed) => (

View File

@@ -1,5 +1,5 @@
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionBillingActions, OrgPermissionSubjects } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { withPermission } from "@app/hoc";
@@ -47,5 +47,5 @@ export const BillingTabGroup = withPermission(
</Tabs>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
{ action: OrgPermissionBillingActions.Read, subject: OrgPermissionSubjects.Billing }
);

View File

@@ -5,6 +5,7 @@ import { OrgPermissionSubjects } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionAppConnectionActions,
OrgPermissionBillingActions,
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionKmipActions,
@@ -21,6 +22,13 @@ const generalPermissionSchema = z
})
.optional();
const billingPermissionSchema = z
.object({
[OrgPermissionBillingActions.Read]: z.boolean().optional(),
[OrgPermissionBillingActions.ManageBilling]: z.boolean().optional()
})
.optional();
const appConnectionsPermissionSchema = z
.object({
[OrgPermissionAppConnectionActions.Read]: z.boolean().optional(),
@@ -113,7 +121,7 @@ export const formSchema = z.object({
scim: generalPermissionSchema,
[OrgPermissionSubjects.GithubOrgSync]: generalPermissionSchema,
ldap: generalPermissionSchema,
billing: generalPermissionSchema,
billing: billingPermissionSchema,
identity: identityPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema,

View File

@@ -0,0 +1,174 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionBillingActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
const PERMISSION_ACTIONS = [
{ action: OrgPermissionBillingActions.Read, label: "View bills" },
{ action: OrgPermissionBillingActions.ManageBilling, label: "Manage billing" }
] as const;
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
export const OrgPermissionBillingRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.billing"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.[OrgPermissionBillingActions.Read]) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: false,
[OrgPermissionBillingActions.ManageBilling]: false
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: true,
[OrgPermissionBillingActions.ManageBilling]: false
},
{ shouldDirty: true }
);
break;
case Permission.FullAccess:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: true,
[OrgPermissionBillingActions.ManageBilling]: true
},
{ shouldDirty: true }
);
break;
default:
setValue(
"permissions.billing",
{
[OrgPermissionBillingActions.Read]: false,
[OrgPermissionBillingActions.ManageBilling]: false
},
{ shouldDirty: true }
);
break;
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Billing</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.billing.${action}`}
key={`permissions.billing.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.billing.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};

View File

@@ -38,13 +38,6 @@ const MEMBERS_PERMISSIONS = [
{ action: "delete", label: "Remove members" }
] as const;
const BILLING_PERMISSIONS = [
{ action: "read", label: "View bills" },
{ action: "create", label: "Add payment methods" },
{ action: "edit", label: "Edit payments" },
{ action: "delete", label: "Remove payments" }
] as const;
const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "read", label: "View & Apply" },
{ action: "create", label: "Create" },
@@ -52,18 +45,16 @@ const PROJECT_TEMPLATES_PERMISSIONS = [
{ action: "delete", label: "Remove" }
] as const;
const getPermissionList = (option: string) => {
switch (option) {
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
case "billing":
return BILLING_PERMISSIONS;
case "incident-contact":
return INCIDENT_CONTACTS_PERMISSIONS;
const getPermissionList = (formName: Props["formName"]) => {
switch (formName) {
case "member":
return MEMBERS_PERMISSIONS;
case OrgPermissionSubjects.ProjectTemplates:
return PROJECT_TEMPLATES_PERMISSIONS;
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
case "incident-contact":
return INCIDENT_CONTACTS_PERMISSIONS;
default:
return PERMISSIONS;
}
@@ -74,7 +65,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share"
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share" | "billing"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;

View File

@@ -14,6 +14,7 @@ import {
TFormSchema
} from "../OrgRoleModifySection.utils";
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgPermissionBillingRow } from "./OrgPermissionBillingRow";
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
@@ -27,10 +28,6 @@ const SIMPLE_PERMISSION_OPTIONS = [
title: "User Management",
formName: "member"
},
{
title: "Usage & Billing",
formName: "billing"
},
{
title: "Role Management",
formName: "role"
@@ -186,6 +183,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionBillingRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionSecretShareRow
control={control}
setValue={setValue}