mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
feat: added validation to check dedupe operators, loading indicator, string required rhs
This commit is contained in:
@@ -11,31 +11,37 @@ export enum PermissionConditionOperators {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PermissionConditionSchema = {
|
export const PermissionConditionSchema = {
|
||||||
[PermissionConditionOperators.$IN]: z.string().array(),
|
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
|
||||||
[PermissionConditionOperators.$ALL]: z.string().array(),
|
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
|
||||||
[PermissionConditionOperators.$REGEX]: z.string().refine(
|
[PermissionConditionOperators.$REGEX]: z
|
||||||
(el) => {
|
.string()
|
||||||
try {
|
.min(1)
|
||||||
// eslint-disable-next-line no-new
|
.refine(
|
||||||
new RegExp(el);
|
(el) => {
|
||||||
return true;
|
try {
|
||||||
} catch {
|
// eslint-disable-next-line no-new
|
||||||
return false;
|
new RegExp(el);
|
||||||
}
|
return true;
|
||||||
},
|
} catch {
|
||||||
{ message: "Invalid regex pattern" }
|
return false;
|
||||||
),
|
}
|
||||||
[PermissionConditionOperators.$EQ]: z.string(),
|
},
|
||||||
[PermissionConditionOperators.$NEQ]: z.string(),
|
{ message: "Invalid regex pattern" }
|
||||||
[PermissionConditionOperators.$GLOB]: z.string().refine(
|
),
|
||||||
(el) => {
|
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||||
try {
|
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||||
picomatch.parse([el]);
|
[PermissionConditionOperators.$GLOB]: z
|
||||||
return true;
|
.string()
|
||||||
} catch {
|
.min(1)
|
||||||
return false;
|
.refine(
|
||||||
}
|
(el) => {
|
||||||
},
|
try {
|
||||||
{ message: "Invalid glob pattern" }
|
picomatch.parse([el]);
|
||||||
)
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ message: "Invalid glob pattern" }
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { EmptyState } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
// This is made into seperate component because watch subscribes to all permissions
|
||||||
|
// thus keeping in top level casues render on all ones
|
||||||
|
export const PermissionEmptyState = () => {
|
||||||
|
const { watch } = useFormContext<TFormSchema>();
|
||||||
|
const isNotEmptyPermissions = Object.entries(watch("permissions") || {}).some(
|
||||||
|
([key, value]) => key && value?.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNotEmptyPermissions) return <div />;
|
||||||
|
|
||||||
|
return <EmptyState title="No policies applied" className="py-8" />;
|
||||||
|
};
|
||||||
+18
-2
@@ -32,7 +32,7 @@ const WorkspacePolicyActionSchema = z.object({
|
|||||||
const ConditionSchema = z.object({
|
const ConditionSchema = z.object({
|
||||||
operator: z.string(),
|
operator: z.string(),
|
||||||
lhs: z.string(),
|
lhs: z.string(),
|
||||||
rhs: z.string()
|
rhs: z.string().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
@@ -46,7 +46,23 @@ export const formSchema = z.object({
|
|||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||||
conditions: ConditionSchema.array().optional().default([])
|
conditions: ConditionSchema.array()
|
||||||
|
.optional()
|
||||||
|
.default([])
|
||||||
|
.refine(
|
||||||
|
(el) => {
|
||||||
|
const lhsOperatorSet = new Set<string>();
|
||||||
|
for (let i = 0; i < el.length; i += 1) {
|
||||||
|
const { lhs, operator } = el[i];
|
||||||
|
if (lhsOperatorSet.has(`${lhs}-${operator}`)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lhsOperatorSet.add(`${lhs}-${operator}`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{ message: "Duplicate operator found for a condition" }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
|
|||||||
+43
-30
@@ -1,10 +1,10 @@
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faCancel, faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, EmptyState, Modal, ModalContent, ModalTrigger } from "@app/components/v2";
|
import { Button, Modal, ModalContent, ModalTrigger, Spinner } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||||
@@ -12,6 +12,7 @@ import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
|||||||
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions";
|
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions";
|
||||||
import { NewPermissionRule } from "./components/NewPermissionRule";
|
import { NewPermissionRule } from "./components/NewPermissionRule";
|
||||||
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
||||||
|
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
formSchema,
|
formSchema,
|
||||||
@@ -38,6 +39,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty, isSubmitting },
|
formState: { isDirty, isSubmitting },
|
||||||
@@ -81,6 +83,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
isDisabled={isSubmitting || !isDirty}
|
isDisabled={isSubmitting || !isDirty}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
@@ -90,6 +93,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
isDisabled={isSubmitting || !isDirty}
|
isDisabled={isSubmitting || !isDirty}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
onClick={() => reset()}
|
onClick={() => reset()}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCancel} />}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -97,35 +101,44 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-4">
|
{isSubmitting ? (
|
||||||
{!isLoading && !role?.permissions?.length && <EmptyState title="No policies applied" />}
|
<div className="flex items-center justify-center space-x-4 pt-16">
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
<Spinner size="lg" />
|
||||||
<GeneralPermissionOptions
|
<p className="text-sm text-gray-400">Saving your policy.</p>
|
||||||
subject={subject}
|
</div>
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
) : (
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
<>
|
||||||
key={`project-permission-${subject}`}
|
<div className="py-4">
|
||||||
isDisabled={isDisabled}
|
{!isLoading && <PermissionEmptyState />}
|
||||||
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
|
<GeneralPermissionOptions
|
||||||
|
subject={subject}
|
||||||
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
|
key={`project-permission-${subject}`}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
{subject === ProjectPermissionSub.Secrets ? (
|
||||||
|
<SecretPermissionConditions isDisabled={isDisabled} />
|
||||||
|
) : undefined}
|
||||||
|
</GeneralPermissionOptions>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.createPolicy.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||||
>
|
>
|
||||||
{subject === ProjectPermissionSub.Secrets ? (
|
<ModalTrigger asChild disabled={isDisabled}>
|
||||||
<SecretPermissionConditions isDisabled={isDisabled} />
|
<Button isDisabled={isDisabled} leftIcon={<FontAwesomeIcon icon={faPlus} />}>
|
||||||
) : undefined}
|
Add Policy
|
||||||
</GeneralPermissionOptions>
|
</Button>
|
||||||
))}
|
</ModalTrigger>
|
||||||
</div>
|
<ModalContent title="New Policy" subTitle="Policies grant additional permissions.">
|
||||||
<Modal
|
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||||
isOpen={popUp.createPolicy.isOpen}
|
</ModalContent>
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
</Modal>
|
||||||
>
|
</>
|
||||||
<ModalTrigger asChild disabled={isDisabled}>
|
)}
|
||||||
<Button isDisabled={isDisabled} leftIcon={<FontAwesomeIcon icon={faPlus} />}>
|
|
||||||
Add Policy
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent title="New Policy" subTitle="Policies grant additional permissions.">
|
|
||||||
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -125,7 +125,7 @@ export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema
|
|||||||
}}
|
}}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
>
|
>
|
||||||
Add rule
|
Add policy
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isDisabled && (
|
{!isDisabled && (
|
||||||
@@ -137,7 +137,7 @@ export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema
|
|||||||
onClick={() => items.remove(rootIndex)}
|
onClick={() => items.remove(rootIndex)}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
>
|
>
|
||||||
Remove Rule
|
Remove policy
|
||||||
</Button>
|
</Button>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+21
-4
@@ -1,6 +1,7 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
||||||
@@ -19,14 +20,23 @@ const getValueLabel = (type: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||||
const { control, watch } = useFormContext<TFormSchema>();
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<TFormSchema>();
|
||||||
const items = useFieldArray({
|
const items = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: `permissions.secrets.${position}.conditions`
|
name: `permissions.secrets.${position}.conditions`
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 border-t border-t-gray-800 bg-mineshaft-800 pt-2">
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"mt-6 bg-mineshaft-800",
|
||||||
|
items.fields.length && " border-t border-t-gray-800 pt-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
{items.fields.map((el, index) => {
|
{items.fields.map((el, index) => {
|
||||||
const lhs = watch(`permissions.secrets.${position}.conditions.${index}.lhs`);
|
const lhs = watch(`permissions.secrets.${position}.conditions.${index}.lhs`);
|
||||||
@@ -51,7 +61,7 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
onValueChange={(e) => field.onChange(e)}
|
onValueChange={(e) => field.onChange(e)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<SelectItem value="environment">Environment</SelectItem>
|
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -114,6 +124,13 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{errors?.permissions?.secrets?.[position]?.conditions?.message && (
|
||||||
|
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||||
|
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{}</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user