diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index f5c1c857a..7456648bb 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -39,7 +39,8 @@ export const UnpackedPermissionSchema = z.object({ .transform((el) => (typeof el !== "string" ? el[0] : el)) .optional(), action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), - conditions: z.unknown().optional() + conditions: z.unknown().optional(), + inverted: z.boolean().optional() }); const unpackPermissions = (permissions: unknown) => diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 9ead7f12e..92cc723fe 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -309,12 +309,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -344,6 +338,7 @@ const GeneralPermissionSchema = [ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), @@ -353,12 +348,14 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe( "Describe what action an entity can take." ) }), z.object({ subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe( "Describe what action an entity can take." ) @@ -369,6 +366,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ z.object({ subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), @@ -378,6 +376,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), @@ -387,6 +386,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(ProjectPermissionSub.SecretImports).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), @@ -396,6 +396,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(ProjectPermissionSub.DynamicSecrets).describe("The entity this permission pertains to."), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ), diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index dcc7da62c..ce3f03d01 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -12,6 +12,7 @@ type Props = { placeholder?: string; className?: string; dropdownContainerClassName?: string; + containerClassName?: string; isLoading?: boolean; position?: "item-aligned" | "popper"; isDisabled?: boolean; @@ -31,12 +32,13 @@ export const Select = forwardRef( isDisabled, dropdownContainerClassName, position, + containerClassName, ...props }, ref ): JSX.Element => { return ( -
+
{ diff --git a/frontend/src/hooks/api/roles/types.ts b/frontend/src/hooks/api/roles/types.ts index da28d9ca8..50cdf5a22 100644 --- a/frontend/src/hooks/api/roles/types.ts +++ b/frontend/src/hooks/api/roles/types.ts @@ -40,6 +40,7 @@ export type TPermission = { export type TProjectPermission = { conditions?: Record; + inverted?: boolean; action: string | string[]; subject: string | string[]; }; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts index 03a56db36..fc620cacc 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -73,21 +73,25 @@ export const formSchema = z.object({ permissions: z .object({ [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ + inverted: z.boolean().optional(), conditions: ConditionSchema }) .array() .default([]), [ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({ + inverted: z.boolean().optional(), conditions: ConditionSchema }) .array() .default([]), [ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({ + inverted: z.boolean().optional(), conditions: ConditionSchema }) .array() .default([]), [ProjectPermissionSub.DynamicSecrets]: GeneralPolicyActionSchema.extend({ + inverted: z.boolean().optional(), conditions: ConditionSchema }) .array() @@ -164,7 +168,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const formVal: Partial = {}; permissions.forEach((permission) => { - const { subject: caslSub, action, conditions } = permission; + const { subject: caslSub, action, conditions, inverted } = permission; const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub; if ( @@ -208,7 +212,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { create: canCreate, edit: canEdit, delete: canDelete, - conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [] + conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], + inverted }); } else { // deduplicate multiple rules for other policies @@ -287,7 +292,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { Object.entries(formVal || {}).forEach(([subject, rules]) => { rules.forEach((actions) => { const caslActions = Object.keys(actions).filter( - (el) => actions?.[el as keyof typeof actions] && el !== "conditions" + (el) => actions?.[el as keyof typeof actions] && el !== "conditions" && el !== "inverted" ); const caslConditions = "conditions" in actions @@ -297,6 +302,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { permissions.push({ action: caslActions, subject, + inverted: (actions as { inverted?: boolean })?.inverted, conditions: caslConditions }); }); diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx index a85015508..5a7a0fdef 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx @@ -1,10 +1,16 @@ import { cloneElement } from "react"; import { Controller, useFieldArray, useFormContext } from "react-hook-form"; -import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { + faChevronDown, + faChevronRight, + faInfoCircle, + faPlus, + faTrash +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button, Checkbox, Tag } from "@app/components/v2"; +import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; @@ -95,6 +101,36 @@ export const GeneralPermissionPolicies =
+ {isConditionalSubjects(subject) && ( +
+
Effect
+
+ ( + + )} + /> +
+
+ + + +
+
+ )} {children && cloneElement(children, { position: rootIndex