From 67c8a0e1688f99d40a824d945992e7f74bef15a7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 20 Oct 2025 18:44:44 -0700 Subject: [PATCH] fix: add sub org permissions UI --- .../src/context/OrgPermissionContext/types.ts | 11 +- .../components/OrgRoleModifySection.utils.ts | 11 +- .../OrgPermissionSubOrgRow.tsx | 159 ++++++++++++++++++ .../RolePermissionRow.tsx | 1 + .../RolePermissionsSection.tsx | 6 + .../ProjectTemplateRolesSection.tsx | 1 - 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index bcab6169e..87dc40263 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -62,7 +62,8 @@ export enum OrgPermissionSubjects { SecretShare = "secret-share", GithubOrgSync = "github-org-sync", GithubOrgSyncManual = "github-org-sync-manual", - MachineIdentityAuthTemplate = "machine-identity-auth-template" + MachineIdentityAuthTemplate = "machine-identity-auth-template", + SubOrganization = "sub-organization" } export enum OrgPermissionAdminConsoleAction { @@ -112,6 +113,11 @@ export enum OrgPermissionGroupActions { RemoveMembers = "remove-members" } +export enum OrgPermissionSubOrgActions { + Create = "create", + DirectAccess = "direct-access" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -151,6 +157,7 @@ export type OrgPermissionSet = | OrgPermissionSubjects.AppConnections | (ForcedSubject & AppConnectionSubjectFields) ) - ]; + ] + | [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 68da9cad2..a355029ad 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -12,6 +12,7 @@ import { OrgPermissionKmipActions, OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSecretShareAction, + OrgPermissionSubOrgActions, OrgRelayPermissionActions } from "@app/context/OrgPermissionContext/types"; import { TPermission } from "@app/hooks/api/roles/types"; @@ -123,6 +124,13 @@ const secretSharingPermissionSchema = z }) .optional(); +const subOrganizationPermissionSchema = z + .object({ + [OrgPermissionSubOrgActions.Create]: z.boolean().optional(), + [OrgPermissionSubOrgActions.DirectAccess]: z.boolean().optional() + }) + .optional(); + export const formSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), @@ -159,7 +167,8 @@ export const formSchema = z.object({ gateway: orgGatewayPermissionSchema, relay: orgRelayPermissionSchema, "machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema, - "secret-share": secretSharingPermissionSchema + "secret-share": secretSharingPermissionSchema, + "sub-organization": subOrganizationPermissionSchema }) .optional() }); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx new file mode 100644 index 000000000..aa51c6752 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionSubOrgRow.tsx @@ -0,0 +1,159 @@ +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 { OrgPermissionSubOrgActions } from "@app/context/OrgPermissionContext/types"; +import { useToggle } from "@app/hooks"; + +import { TFormSchema } from "../OrgRoleModifySection.utils"; + +const PERMISSION_ACTIONS = [ + { action: OrgPermissionSubOrgActions.Create, label: "Create" }, + { action: OrgPermissionSubOrgActions.DirectAccess, label: "Direct Access" } +] as const; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + FullAccess = "full-access", + Custom = "custom" +} + +export const OrgPermissionSubOrgRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.sub-organization" + }); + + const selectedPermissionCategory = useMemo(() => { + const actions = Object.keys(rule || {}) as Array; + 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; + 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.sub-organization", + { + [OrgPermissionSubOrgActions.Create]: false, + [OrgPermissionSubOrgActions.DirectAccess]: false + }, + { shouldDirty: true } + ); + break; + case Permission.FullAccess: + setValue( + "permissions.sub-organization", + { + [OrgPermissionSubOrgActions.Create]: true, + [OrgPermissionSubOrgActions.DirectAccess]: true + }, + { shouldDirty: true } + ); + break; + default: + setValue( + "permissions.sub-organization", + { + [OrgPermissionSubOrgActions.Create]: true, + [OrgPermissionSubOrgActions.DirectAccess]: true + }, + { shouldDirty: true } + ); + break; + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Sub-Organizations + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.sub-organization.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx index 0e3b48a4e..7fb1a3842 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx @@ -74,6 +74,7 @@ type Props = { | "billing" | "audit-logs" | "machine-identity-auth-template" + | "sub-organization" >; setValue: UseFormSetValue; control: Control; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 1ee3b4d91..02aa0a08c 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -25,6 +25,7 @@ import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow"; import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow"; import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow"; +import { OrgPermissionSubOrgRow } from "./OrgPermissionSubOrgRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; @@ -224,6 +225,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => { setValue={setValue} isEditable={isCustomRole} /> + diff --git a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx index 1df8128f4..d65f7d785 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ProjectTemplatesTab/components/EditProjectTemplateSection/components/ProjectTemplateRolesSection.tsx @@ -1,6 +1,5 @@ import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions";