mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 12:56:29 +00:00
feat: add metadata based permissions for dynamic secret
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import {
|
||||
getConditionOperatorHelperInfo,
|
||||
renderOperatorSelectItems
|
||||
} from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions`
|
||||
});
|
||||
|
||||
const conditionErrorMessage =
|
||||
errors?.permissions?.["dynamic-secrets"]?.[position]?.conditions?.message ||
|
||||
errors?.permissions?.["dynamic-secrets"]?.[position]?.conditions?.root?.message;
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
const condition = watch(
|
||||
`permissions.dynamic-secrets.${position}.conditions.${index}`
|
||||
) as {
|
||||
lhs: string;
|
||||
rhs: string;
|
||||
operator: string;
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.dynamic-secrets.${position}.conditions.${index}.lhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
setValue(
|
||||
`permissions.dynamic-secrets.${position}.conditions.${index}.operator`,
|
||||
PermissionConditionOperators.$IN as never
|
||||
);
|
||||
field.onChange(e);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||
<SelectItem value="metadataKey">Metadata Key</SelectItem>
|
||||
<SelectItem value="metadataValue">Metadata Value</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.dynamic-secrets.${position}.conditions.${index}.operator`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{renderOperatorSelectItems(condition.lhs)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.dynamic-secrets.${position}.conditions.${index}.rhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
ariaLabel="plus"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{conditionErrorMessage && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{conditionErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "environment",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+94
-6
@@ -266,11 +266,59 @@ const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition
|
||||
} else {
|
||||
Object.keys(condition).forEach((conditionOperator) => {
|
||||
const rhs = condition[conditionOperator as PermissionConditionOperators];
|
||||
formConditions.push({
|
||||
operator: conditionOperator,
|
||||
lhs: type,
|
||||
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
||||
});
|
||||
if (Array.isArray(rhs) || typeof rhs === "string") {
|
||||
formConditions.push({
|
||||
operator: conditionOperator,
|
||||
lhs: type,
|
||||
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
||||
});
|
||||
} else if (
|
||||
conditionOperator === PermissionConditionOperators.$ELEMENTMATCH &&
|
||||
type === "metadata"
|
||||
) {
|
||||
const deepKeyCondition = rhs.key;
|
||||
if (deepKeyCondition) {
|
||||
if (typeof deepKeyCondition === "string") {
|
||||
formConditions.push({
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
lhs: "metadataKey",
|
||||
rhs: deepKeyCondition
|
||||
});
|
||||
} else {
|
||||
Object.keys(deepKeyCondition).forEach((keyOperator) => {
|
||||
const deepRhs = deepKeyCondition?.[keyOperator as PermissionConditionOperators];
|
||||
if (deepRhs && (Array.isArray(deepRhs) || typeof deepRhs === "string")) {
|
||||
formConditions.push({
|
||||
operator: keyOperator,
|
||||
lhs: "metadataKey",
|
||||
rhs: typeof deepRhs === "string" ? deepRhs : deepRhs.join(",")
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const deepValueCondition = rhs.value;
|
||||
if (deepValueCondition) {
|
||||
if (typeof deepValueCondition === "string") {
|
||||
formConditions.push({
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
lhs: "metadataValue",
|
||||
rhs: deepValueCondition
|
||||
});
|
||||
} else {
|
||||
Object.keys(deepValueCondition).forEach((keyOperator) => {
|
||||
const deepRhs = deepValueCondition?.[keyOperator as PermissionConditionOperators];
|
||||
if (deepRhs && (Array.isArray(deepRhs) || typeof deepRhs === "string")) {
|
||||
formConditions.push({
|
||||
operator: keyOperator,
|
||||
lhs: "metadataValue",
|
||||
rhs: typeof deepRhs === "string" ? deepRhs : deepRhs.join(",")
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -585,7 +633,45 @@ const convertFormOperatorToCaslCondition = (
|
||||
conditions: { lhs: string; rhs: string; operator: string }[]
|
||||
) => {
|
||||
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
|
||||
|
||||
const metadataKeyCondition = conditions.find((condition) => condition.lhs === "metadataKey");
|
||||
const metadataValueCondition = conditions.find((condition) => condition.lhs === "metadataValue");
|
||||
|
||||
if (metadataKeyCondition || metadataValueCondition) {
|
||||
caslCondition.metadata = {
|
||||
[PermissionConditionOperators.$ELEMENTMATCH]: {}
|
||||
};
|
||||
|
||||
if (metadataKeyCondition) {
|
||||
const operator = metadataKeyCondition.operator as PermissionConditionOperators;
|
||||
caslCondition.metadata[PermissionConditionOperators.$ELEMENTMATCH]!.key = {
|
||||
[metadataKeyCondition.operator]: [
|
||||
PermissionConditionOperators.$IN,
|
||||
PermissionConditionOperators.$ALL
|
||||
].includes(operator)
|
||||
? metadataKeyCondition.rhs.split(",")
|
||||
: metadataKeyCondition.rhs
|
||||
};
|
||||
}
|
||||
|
||||
if (metadataValueCondition) {
|
||||
const operator = metadataValueCondition.operator as PermissionConditionOperators;
|
||||
caslCondition.metadata[PermissionConditionOperators.$ELEMENTMATCH]!.value = {
|
||||
[metadataValueCondition.operator]: [
|
||||
PermissionConditionOperators.$IN,
|
||||
PermissionConditionOperators.$ALL
|
||||
].includes(operator)
|
||||
? metadataValueCondition.rhs.split(",")
|
||||
: metadataValueCondition.rhs
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
conditions.forEach((el) => {
|
||||
// these are special fields and handled above
|
||||
if (el.lhs === "metadataKey" || el.lhs === "metadataValue") {
|
||||
return;
|
||||
}
|
||||
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
|
||||
if (
|
||||
el.operator === PermissionConditionOperators.$IN ||
|
||||
@@ -596,7 +682,9 @@ const convertFormOperatorToCaslCondition = (
|
||||
caslCondition[el.lhs][
|
||||
el.operator as Exclude<
|
||||
PermissionConditionOperators,
|
||||
PermissionConditionOperators.$ALL | PermissionConditionOperators.$IN
|
||||
| PermissionConditionOperators.$ALL
|
||||
| PermissionConditionOperators.$IN
|
||||
| PermissionConditionOperators.$ELEMENTMATCH
|
||||
>
|
||||
] = el.rhs;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
|
||||
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
|
||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||
@@ -46,6 +47,9 @@ export const renderConditionalComponents = (
|
||||
if (subject === ProjectPermissionSub.Secrets)
|
||||
return <SecretPermissionConditions isDisabled={isDisabled} />;
|
||||
|
||||
if (subject === ProjectPermissionSub.DynamicSecrets)
|
||||
return <DynamicSecretPermissionConditions isDisabled={isDisabled} />;
|
||||
|
||||
if (isConditionalSubjects(subject)) {
|
||||
if (subject === ProjectPermissionSub.Identity) {
|
||||
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
|
||||
|
||||
Reference in New Issue
Block a user