mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 12:27:28 +00:00
progress
This commit is contained in:
@@ -37,7 +37,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
secretApproval: false,
|
secretApproval: true,
|
||||||
secretRotation: true,
|
secretRotation: true,
|
||||||
caCrl: false,
|
caCrl: false,
|
||||||
instanceUserManagement: false,
|
instanceUserManagement: false,
|
||||||
|
|||||||
@@ -34,18 +34,19 @@ export const FilterableSelect = <T,>({
|
|||||||
tabSelectsValue={tabSelectsValue}
|
tabSelectsValue={tabSelectsValue}
|
||||||
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
|
||||||
classNames={{
|
classNames={{
|
||||||
container: () => "w-full font-inter",
|
container: () => "w-full text-sm font-inter",
|
||||||
control: ({ isFocused }) =>
|
control: ({ isFocused }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
|
||||||
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
"border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
|
||||||
),
|
),
|
||||||
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
|
placeholder: () =>
|
||||||
|
`${isMulti ? "py-[0.22rem]" : "leading-7"} text-mineshaft-400 text-sm pl-1`,
|
||||||
input: () => "pl-1 py-0.5",
|
input: () => "pl-1 py-0.5",
|
||||||
valueContainer: () =>
|
valueContainer: () =>
|
||||||
`p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`,
|
`p-1 max-h-[14rem] ${isMulti ? "!overflow-y-auto thin-scrollbar" : ""} gap-1`,
|
||||||
singleValue: () => "leading-7 ml-1",
|
singleValue: () => "leading-7 ml-1",
|
||||||
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
|
multiValue: () => "bg-mineshaft-600 text-sm rounded items-center py-0.5 px-2 gap-1.5",
|
||||||
multiValueLabel: () => "leading-6 text-sm",
|
multiValueLabel: () => "leading-6 text-sm",
|
||||||
multiValueRemove: () => "hover:text-red text-bunker-400",
|
multiValueRemove: () => "hover:text-red text-bunker-400",
|
||||||
indicatorsContainer: () => "p-1 gap-1",
|
indicatorsContainer: () => "p-1 gap-1",
|
||||||
@@ -53,7 +54,7 @@ export const FilterableSelect = <T,>({
|
|||||||
indicatorSeparator: () => "bg-bunker-400",
|
indicatorSeparator: () => "bg-bunker-400",
|
||||||
dropdownIndicator: () => "text-bunker-200 p-1",
|
dropdownIndicator: () => "text-bunker-200 p-1",
|
||||||
menu: () =>
|
menu: () =>
|
||||||
"mt-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
"my-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
|
||||||
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
|
||||||
option: ({ isFocused, isSelected }) =>
|
option: ({ isFocused, isSelected }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
|||||||
+1
-1
@@ -175,7 +175,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create policy
|
Create Policy
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
+121
-138
@@ -1,18 +1,12 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
FilterableSelect,
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -46,21 +40,34 @@ type Props = {
|
|||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
environment: z.string(),
|
environment: z.object({ slug: z.string(), name: z.string() }),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().optional(),
|
secretPath: z.string().optional(),
|
||||||
approvals: z.number().min(1),
|
approvals: z.number().min(1),
|
||||||
approvers: z
|
userApprovers: z
|
||||||
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
.object({ type: z.literal(ApproverType.User), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.default([]),
|
||||||
|
groupApprovers: z
|
||||||
|
.object({ type: z.literal(ApproverType.Group), id: z.string() })
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
|
||||||
.default([]),
|
.default([]),
|
||||||
policyType: z.nativeEnum(PolicyType),
|
policyType: z.nativeEnum(PolicyType),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvers, {
|
.superRefine((data, ctx) => {
|
||||||
path: ["approvers"],
|
if (!(data.groupApprovers.length || data.userApprovers.length)) {
|
||||||
message: "At least one approver should be provided."
|
ctx.addIssue({
|
||||||
|
path: ["userApprovers"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "At least one approver should be provided"
|
||||||
|
});
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["groupApprovers"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "At least one approver should be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
@@ -84,8 +91,15 @@ export const AccessPolicyForm = ({
|
|||||||
values: editValues
|
values: editValues
|
||||||
? {
|
? {
|
||||||
...editValues,
|
...editValues,
|
||||||
environment: editValues.environment.slug,
|
environment: editValues.environment,
|
||||||
approvers: editValues?.approvers || [],
|
userApprovers:
|
||||||
|
editValues?.approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.User)
|
||||||
|
.map(({ id, type }) => ({ id, type: type as ApproverType.User })) || [],
|
||||||
|
groupApprovers:
|
||||||
|
editValues?.approvers
|
||||||
|
?.filter((approver) => approver.type === ApproverType.Group)
|
||||||
|
.map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [],
|
||||||
approvals: editValues?.approvals
|
approvals: editValues?.approvals
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
@@ -110,18 +124,27 @@ export const AccessPolicyForm = ({
|
|||||||
|
|
||||||
const approversRequired = watch("approvals") || 1;
|
const approversRequired = watch("approvals") || 1;
|
||||||
|
|
||||||
const handleCreatePolicy = async (data: TFormSchema) => {
|
const handleCreatePolicy = async ({
|
||||||
|
environment,
|
||||||
|
groupApprovers,
|
||||||
|
userApprovers,
|
||||||
|
...data
|
||||||
|
}: TFormSchema) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (data.policyType === PolicyType.ChangePolicy) {
|
if (data.policyType === PolicyType.ChangePolicy) {
|
||||||
await createSecretApprovalPolicy({
|
await createSecretApprovalPolicy({
|
||||||
...data,
|
...data,
|
||||||
|
approvers: [...userApprovers, ...groupApprovers],
|
||||||
|
environment: environment.slug,
|
||||||
workspaceId: currentWorkspace?.id || ""
|
workspaceId: currentWorkspace?.id || ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createAccessApprovalPolicy({
|
await createAccessApprovalPolicy({
|
||||||
...data,
|
...data,
|
||||||
|
approvers: [...userApprovers, ...groupApprovers],
|
||||||
|
environment: environment.slug,
|
||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -139,7 +162,12 @@ export const AccessPolicyForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePolicy = async (data: TFormSchema) => {
|
const handleUpdatePolicy = async ({
|
||||||
|
environment,
|
||||||
|
userApprovers,
|
||||||
|
groupApprovers,
|
||||||
|
...data
|
||||||
|
}: TFormSchema) => {
|
||||||
if (!projectId || !projectSlug) return;
|
if (!projectId || !projectSlug) return;
|
||||||
if (!editValues?.id) return;
|
if (!editValues?.id) return;
|
||||||
|
|
||||||
@@ -148,12 +176,15 @@ export const AccessPolicyForm = ({
|
|||||||
await updateSecretApprovalPolicy({
|
await updateSecretApprovalPolicy({
|
||||||
id: editValues?.id,
|
id: editValues?.id,
|
||||||
...data,
|
...data,
|
||||||
|
approvers: [...userApprovers, ...groupApprovers],
|
||||||
workspaceId: currentWorkspace?.id || ""
|
workspaceId: currentWorkspace?.id || ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await updateAccessApprovalPolicy({
|
await updateAccessApprovalPolicy({
|
||||||
id: editValues?.id,
|
id: editValues?.id,
|
||||||
...data,
|
...data,
|
||||||
|
approvers: [...userApprovers, ...groupApprovers],
|
||||||
|
environment: environment.slug,
|
||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -179,9 +210,35 @@ export const AccessPolicyForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const memberOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
members.map(({ inviteEmail, user: { firstName, lastName, id: userId, username } }) => ({
|
||||||
|
id: userId,
|
||||||
|
type: ApproverType.User,
|
||||||
|
label:
|
||||||
|
firstName || lastName
|
||||||
|
? `${firstName ?? ""} ${lastName ?? ""}`.trim()
|
||||||
|
: username || inviteEmail
|
||||||
|
})),
|
||||||
|
[members]
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
groups?.map(({ group }) => ({
|
||||||
|
id: group.id,
|
||||||
|
type: ApproverType.Group,
|
||||||
|
label: group.name
|
||||||
|
})),
|
||||||
|
[groups]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
<ModalContent title={isEditMode ? `Edit ${policyName}` : "Create Policy"}>
|
<ModalContent
|
||||||
|
bodyClassName="overflow-visible"
|
||||||
|
title={isEditMode ? `Edit ${policyName}` : "Create Policy"}
|
||||||
|
>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -229,7 +286,6 @@ export const AccessPolicyForm = ({
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="environment"
|
name="environment"
|
||||||
defaultValue={environments[0]?.slug}
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Environment"
|
label="Environment"
|
||||||
@@ -238,21 +294,15 @@ export const AccessPolicyForm = ({
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<FilterableSelect
|
||||||
isDisabled={isEditMode}
|
isDisabled={isEditMode}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => onChange(val)}
|
onChange={onChange}
|
||||||
className="w-full border border-mineshaft-500"
|
placeholder="Select environment..."
|
||||||
>
|
options={environments}
|
||||||
{environments.map((sourceEnvironment) => (
|
getOptionValue={(option) => option.slug}
|
||||||
<SelectItem
|
getOptionLabel={(option) => option.name}
|
||||||
value={sourceEnvironment.slug}
|
/>
|
||||||
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
|
||||||
>
|
|
||||||
{sourceEnvironment.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -331,127 +381,60 @@ export const AccessPolicyForm = ({
|
|||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvers"
|
name="userApprovers"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="User Approvers"
|
label="User Approvers"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<DropdownMenu>
|
<FilterableSelect
|
||||||
<DropdownMenuTrigger asChild>
|
menuPlacement="top"
|
||||||
<Input
|
isMulti
|
||||||
isReadOnly
|
placeholder="Select members that are allowed to approve requests..."
|
||||||
value={
|
options={memberOptions}
|
||||||
value?.filter((e) => e.type === ApproverType.User).length
|
getOptionValue={(option) => option.id}
|
||||||
? `${value.filter((e) => e.type === ApproverType.User).length} selected`
|
getOptionLabel={(option) => {
|
||||||
: "None"
|
const member = members?.find((m) => m.user.id === option.id);
|
||||||
}
|
|
||||||
className="text-left"
|
if (!member) return option.id;
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
const {
|
||||||
<DropdownMenuContent
|
inviteEmail,
|
||||||
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
user: { firstName, lastName, username, email }
|
||||||
align="start"
|
} = member;
|
||||||
>
|
|
||||||
<DropdownMenuLabel>
|
return firstName || lastName
|
||||||
Select members that are allowed to approve requests
|
? `${firstName ?? ""} ${lastName ?? ""}`.trim()
|
||||||
</DropdownMenuLabel>
|
: username || email || inviteEmail;
|
||||||
{members.map(({ user }) => {
|
}}
|
||||||
const { id: userId } = user;
|
value={value}
|
||||||
const isChecked =
|
onChange={onChange}
|
||||||
value?.filter(
|
/>
|
||||||
(el: { id: string; type: ApproverType }) =>
|
|
||||||
el.id === userId && el.type === ApproverType.User
|
|
||||||
).length > 0;
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
onChange(
|
|
||||||
isChecked
|
|
||||||
? value?.filter(
|
|
||||||
(el: { id: string; type: ApproverType }) =>
|
|
||||||
el.id !== userId && el.type !== ApproverType.User
|
|
||||||
)
|
|
||||||
: [...(value || []), { id: userId, type: ApproverType.User }]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={`create-policy-members-${userId}`}
|
|
||||||
iconPos="right"
|
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
>
|
|
||||||
{user.username}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvers"
|
name="groupApprovers"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Group Approvers"
|
label="Group Approvers"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<DropdownMenu>
|
<FilterableSelect
|
||||||
<DropdownMenuTrigger asChild>
|
menuPlacement="top"
|
||||||
<Input
|
isMulti
|
||||||
isReadOnly
|
placeholder="Select groups that are allowed to approve requests..."
|
||||||
value={
|
options={groupOptions}
|
||||||
value?.filter((e) => e.type === ApproverType.Group).length
|
getOptionValue={(option) => option.id}
|
||||||
? `${
|
getOptionLabel={(option) =>
|
||||||
value?.filter((e) => e.type === ApproverType.Group).length
|
groups?.find(({ group }) => group.id === option.id)?.group.name ?? option.id
|
||||||
} selected`
|
}
|
||||||
: "None"
|
value={value}
|
||||||
}
|
onChange={onChange}
|
||||||
className="text-left"
|
/>
|
||||||
/>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel>
|
|
||||||
Select groups that are allowed to approve requests
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
{groups &&
|
|
||||||
groups.map(({ group }) => {
|
|
||||||
const { id } = group;
|
|
||||||
const isChecked =
|
|
||||||
value?.filter(
|
|
||||||
(el: { id: string; type: ApproverType }) =>
|
|
||||||
el.id === id && el.type === ApproverType.Group
|
|
||||||
).length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
onChange(
|
|
||||||
isChecked
|
|
||||||
? value?.filter(
|
|
||||||
(el: { id: string; type: ApproverType }) =>
|
|
||||||
el.id !== id && el.type !== ApproverType.Group
|
|
||||||
)
|
|
||||||
: [...(value || []), { id, type: ApproverType.Group }]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
key={`create-policy-members-${id}`}
|
|
||||||
iconPos="right"
|
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user