mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: added inverted to project permission
This commit is contained in:
@@ -39,7 +39,8 @@ export const UnpackedPermissionSchema = z.object({
|
|||||||
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||||
.optional(),
|
.optional(),
|
||||||
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
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) =>
|
const unpackPermissions = (permissions: unknown) =>
|
||||||
|
|||||||
@@ -309,12 +309,6 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"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({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
@@ -344,6 +338,7 @@ const GeneralPermissionSchema = [
|
|||||||
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
@@ -353,12 +348,14 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
@@ -369,6 +366,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
|||||||
export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
@@ -378,6 +376,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
@@ -387,6 +386,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretImports).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
@@ -396,6 +396,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.DynamicSecrets).describe("The entity this permission pertains to."),
|
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(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Props = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
|
containerClassName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
position?: "item-aligned" | "popper";
|
position?: "item-aligned" | "popper";
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
@@ -31,12 +32,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
dropdownContainerClassName,
|
dropdownContainerClassName,
|
||||||
position,
|
position,
|
||||||
|
containerClassName,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className={twMerge("flex items-center space-x-2", containerClassName)}>
|
||||||
<SelectPrimitive.Root
|
<SelectPrimitive.Root
|
||||||
{...props}
|
{...props}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type TPermission = {
|
|||||||
|
|
||||||
export type TProjectPermission = {
|
export type TProjectPermission = {
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
|
inverted?: boolean;
|
||||||
action: string | string[];
|
action: string | string[];
|
||||||
subject: string | string[];
|
subject: string | string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,21 +73,25 @@ export const formSchema = z.object({
|
|||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||||
|
inverted: z.boolean().optional(),
|
||||||
conditions: ConditionSchema
|
conditions: ConditionSchema
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
[ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({
|
[ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({
|
||||||
|
inverted: z.boolean().optional(),
|
||||||
conditions: ConditionSchema
|
conditions: ConditionSchema
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
[ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({
|
[ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({
|
||||||
|
inverted: z.boolean().optional(),
|
||||||
conditions: ConditionSchema
|
conditions: ConditionSchema
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
[ProjectPermissionSub.DynamicSecrets]: GeneralPolicyActionSchema.extend({
|
[ProjectPermissionSub.DynamicSecrets]: GeneralPolicyActionSchema.extend({
|
||||||
|
inverted: z.boolean().optional(),
|
||||||
conditions: ConditionSchema
|
conditions: ConditionSchema
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
@@ -164,7 +168,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
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;
|
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -208,7 +212,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
create: canCreate,
|
create: canCreate,
|
||||||
edit: canEdit,
|
edit: canEdit,
|
||||||
delete: canDelete,
|
delete: canDelete,
|
||||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : []
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||||
|
inverted
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// deduplicate multiple rules for other policies
|
// deduplicate multiple rules for other policies
|
||||||
@@ -287,7 +292,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
||||||
rules.forEach((actions) => {
|
rules.forEach((actions) => {
|
||||||
const caslActions = Object.keys(actions).filter(
|
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 =
|
const caslConditions =
|
||||||
"conditions" in actions
|
"conditions" in actions
|
||||||
@@ -297,6 +302,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
permissions.push({
|
permissions.push({
|
||||||
action: caslActions,
|
action: caslActions,
|
||||||
subject,
|
subject,
|
||||||
|
inverted: (actions as { inverted?: boolean })?.inverted,
|
||||||
conditions: caslConditions
|
conditions: caslConditions
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { cloneElement } from "react";
|
import { cloneElement } from "react";
|
||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
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 { ProjectPermissionSub } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
@@ -95,6 +101,36 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isConditionalSubjects(subject) && (
|
||||||
|
<div className="mt-4 flex w-full items-center text-gray-300">
|
||||||
|
<div className="w-1/4">Effect</div>
|
||||||
|
<div className="mr-4 w-1/4">
|
||||||
|
<Controller
|
||||||
|
defaultValue={false as any}
|
||||||
|
name={`permissions.${subject}.${rootIndex}.inverted`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
value={String(field.value)}
|
||||||
|
onValueChange={(val) => field.onChange(val === "true")}
|
||||||
|
containerClassName="w-full"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value="false">Allow</SelectItem>
|
||||||
|
<SelectItem value="true">Disallow</SelectItem>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
asChild
|
||||||
|
content="Whether to allow or forbid. Forbid rules must be added after allow rules."
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{children &&
|
{children &&
|
||||||
cloneElement(children, {
|
cloneElement(children, {
|
||||||
position: rootIndex
|
position: rootIndex
|
||||||
|
|||||||
Reference in New Issue
Block a user