Merge remote-tracking branch 'origin' into feature/est-simpleenroll

This commit is contained in:
Tuan Dang
2024-08-22 14:54:34 -07:00
65 changed files with 2875 additions and 4735 deletions
@@ -11,7 +11,7 @@ type Props = {
};
const textAreaVariants = cva(
"textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50",
"textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50",
{
variants: {
size: {
@@ -25,13 +25,13 @@ const textAreaVariants = cva(
false: ""
},
variant: {
filled: ["bg-bunker-800", "text-gray-400"],
filled: ["bg-mineshaft-900", "text-gray-400"],
outline: ["bg-transparent"],
plain: "bg-transparent outline-none"
},
isError: {
true: "focus:ring-red/50 placeholder-red-300 border-red",
false: "focus:ring-primary/50 border-mineshaft-400"
false: "focus:ring-primary-400/50 focus:ring-1 border-mineshaft-500"
}
},
compoundVariants: [
@@ -44,6 +44,13 @@ export type ProjectPermissionSet =
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
(
| ProjectPermissionSub.SecretFolders
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]
@@ -16,12 +16,20 @@ export const useCreateAccessApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateAccessPolicyDTO>({
mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath, enforcementLevel }) => {
mutationFn: async ({
environment,
projectSlug,
approvals,
approverUserIds,
name,
secretPath,
enforcementLevel
}) => {
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
environment,
projectSlug,
approvals,
approvers,
approverUserIds,
secretPath,
name,
enforcementLevel
@@ -23,7 +23,14 @@ export type TAccessApprovalRequest = {
id: string;
policyId: string;
privilegeId: string | null;
requestedBy: string;
requestedByUserId: string;
requestedByUser: {
email: string;
firstName?: string;
lastName?: string;
userId: string;
username: string;
};
createdAt: Date;
updatedAt: Date;
isTemporary: boolean;
@@ -123,7 +130,7 @@ export type TCreateAccessPolicyDTO = {
projectSlug: string;
name?: string;
environment: string;
approvers?: string[];
approverUserIds?: string[];
approvals?: number;
secretPath?: string;
enforcementLevel?: EnforcementLevel;
@@ -0,0 +1 @@
export const ERROR_NOT_ALLOWED_READ_SECRETS = "You are not allowed to read on secrets";
+8 -5
View File
@@ -7,6 +7,7 @@ import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { useToggle } from "@app/hooks/useToggle";
import { ERROR_NOT_ALLOWED_READ_SECRETS } from "./constants";
import {
GetSecretVersionsDTO,
SecretType,
@@ -135,11 +136,13 @@ export const useGetProjectSecretsAllEnv = ({
onError: (error: unknown) => {
if (axios.isAxiosError(error) && !isErrorHandled) {
const serverResponse = error.response?.data as { message: string };
createNotification({
title: "Error fetching secrets",
type: "error",
text: serverResponse.message
});
if (serverResponse.message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
createNotification({
title: "Error fetching secrets",
type: "error",
text: serverResponse.message
});
}
setIsErrorHandled.on();
}
+1
View File
@@ -83,6 +83,7 @@ export type TWorkspaceUser = {
publicKey: string;
};
projectId: string;
isGroupMember: boolean;
project: {
id: string;
name: string;
+7 -2
View File
@@ -377,14 +377,19 @@ export const useDeleteWsEnvironment = () => {
});
};
export const useGetWorkspaceUsers = (workspaceId: string) => {
export const useGetWorkspaceUsers = (workspaceId: string, includeGroupMembers?: boolean) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
queryFn: async () => {
const {
data: { users }
} = await apiRequest.get<{ users: TWorkspaceUser[] }>(
`/api/v1/workspace/${workspaceId}/users`
`/api/v1/workspace/${workspaceId}/users`,
{
params: {
includeGroupMembers
}
}
);
return users;
},
@@ -6,7 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { CaStatus, useGetCaById } from "@app/hooks/api";
import { CaStatus, CaType, useGetCaById } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -35,6 +35,10 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA ID</p>
<div className="group flex align-top">
@@ -56,26 +60,30 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
</div>
</div>
</div>
{ca.parentCaId && (
{ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.parentCaId}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextParentId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.parentCaId as string);
setCopyTextParentId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
<p className="text-sm text-mineshaft-300">
{ca.parentCaId ? ca.parentCaId : "N/A - External Parent CA"}
</p>
{ca.parentCaId && (
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextParentId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(ca.parentCaId as string);
setCopyTextParentId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
)}
</div>
</div>
)}
@@ -83,10 +91,6 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
@@ -124,6 +128,15 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
colorSchema="primary"
type="submit"
onClick={() => {
if (ca.type === CaType.INTERMEDIATE && !ca.parentCaId) {
// intermediate CA with external parent CA
handlePopUpOpen("installCaCert", {
caId,
isParentCaExternal: true
});
return;
}
handlePopUpOpen("renewCa", {
caId
});
@@ -1,53 +1,10 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
// DatePicker,
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
import { ExternalCaInstallForm } from "./ExternalCaInstallForm";
import { InternalCaInstallForm } from "./InternalCaInstallForm";
type Props = {
popUp: UsePopUpState<["installCaCert"]>;
@@ -60,234 +17,23 @@ enum ParentCaType {
}
export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const [parentCaType] = useState<ParentCaType>(ParentCaType.Internal);
const { currentWorkspace } = useWorkspace();
const caId = (popUp?.installCaCert?.data as { caId: string })?.caId || "";
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
const popupData = popUp?.installCaCert?.data;
const caId = popupData?.caId || "";
const isParentCaExternal = popupData?.isParentCaExternal || false;
const [parentCaType, setParentCaType] = useState<ParentCaType>(ParentCaType.Internal);
useEffect(() => {
if (cas?.length) {
setValue("parentCaId", cas[0].id);
if (popupData?.isParentCaExternal) {
setParentCaType(ParentCaType.External);
}
}, [cas, setValue]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
}, [popupData]);
const renderForm = (parentCaTypeInput: ParentCaType) => {
switch (parentCaTypeInput) {
case ParentCaType.Internal:
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas =
c.maxPathLength && c.maxPathLength !== 0;
return (
isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas
);
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* <Controller
name="notAfter"
control={control}
defaultValue={getDefaultNotAfterDate()}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Validity"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/> */}
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Path Length"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
return <InternalCaInstallForm caId={caId} handlePopUpToggle={handlePopUpToggle} />;
default:
return <div>External TODO</div>;
return <ExternalCaInstallForm caId={caId} handlePopUpToggle={handlePopUpToggle} />;
}
};
@@ -296,31 +42,32 @@ export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
isOpen={popUp?.installCaCert?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("installCaCert", isOpen);
reset();
}}
>
<ModalContent title="Install Intermediate CA certificate">
{/* <FormControl label="Parent CA Type" className="mt-4">
<ModalContent
title={`${isParentCaExternal ? "Renew" : "Install"} Intermediate CA certificate`}
>
<FormControl label="Parent CA Type">
<Select
defaultValue={ParentCaType.Internal}
value={parentCaType}
onValueChange={(e) => setParentCaType(e as ParentCaType)}
className="w-full"
isDisabled={isParentCaExternal}
>
<SelectItem
value={ParentCaType.Internal}
key={`parent-ca-type-${ParentCaType.Internal}`}
>
Infisical Private CA
Infisical CA
</SelectItem>
<SelectItem
value={ParentCaType.External}
key={`parent-ca-type-${ParentCaType.External}`}
>
External Private CA
External CA
</SelectItem>
</Select>
</FormControl> */}
</FormControl>
{renderForm(parentCaType)}
</ModalContent>
</Modal>
@@ -0,0 +1,172 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton,TextArea, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetCaCsr, useImportCaCertificate } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
certificate: z.string().min(1),
certificateChain: z.string().min(1)
});
export type FormData = z.infer<typeof schema>;
type Props = {
caId: string;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
};
export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const [copyTextCaCsr, isCopyingCaCsr, setCopyTextCaCsr] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
useEffect(() => {
reset();
}, []);
const onFormSubmit = async ({ certificate, certificateChain }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
{csr && (
<>
<div className="my-4 flex items-center justify-between">
<h2>CSR for this CA</h2>
<div className="flex">
<Tooltip content={copyTextCaCsr}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(csr);
setCopyTextCaCsr("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCaCsr ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("csr.pem", csr);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{csr}</p>
</div>
</>
)}
<Controller
control={control}
name="certificate"
render={({ field, fieldState: { error } }) => (
<FormControl label="Certificate Body" errorText={error?.message} isError={Boolean(error)}>
<TextArea
{...field}
placeholder="PEM-encoded certificate..."
reSize="none"
className="h-48"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="certificateChain"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate Chain"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="PEM-encoded certificate chain..."
reSize="none"
className="h-48"
/>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,236 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input,Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
caId: string;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
};
export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
useEffect(() => {
reset();
}, []);
useEffect(() => {
if (cas?.length) {
setValue("parentCaId", cas[0].id);
}
}, [cas, setValue]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
// defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
// defaultValue={field.value}
{...field}
onValueChange={onChange}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas = c.maxPathLength && c.maxPathLength !== 0;
return isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas;
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Path Length" errorText={error?.message} isError={Boolean(error)}>
<Select
// defaultValue={field.value}
{...field}
onValueChange={onChange}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
};
@@ -1,5 +1,5 @@
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { faWarning } from "@fortawesome/free-solid-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { ServiceTokenSection } from "./components";
@@ -14,7 +14,7 @@ export const ServiceTokenTab = () => {
exit={{ opacity: 0, translateX: 30 }}
>
<div className="space-y-3">
<div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
{/* <div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
<FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm">
<span className="mb-4 text-lg font-semibold">Deprecation Notice</span>
@@ -41,7 +41,7 @@ export const ServiceTokenTab = () => {
</a>
</p>
</div>
</div>
</div> */}
<ServiceTokenSection />
</div>
</motion.div>
@@ -151,6 +151,7 @@ export const RowPermissionSecretsRow = ({
<Controller
name={`permissions.${formName}.${slug}.secretPath`}
control={control}
defaultValue="/**"
render={({ field }) => (
/* eslint-disable-next-line no-template-curly-in-string */
<FormControl helperText="Supports glob path pattern string">
@@ -29,6 +29,7 @@ import {
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
@@ -47,7 +48,7 @@ import { queryClient } from "@app/reactQuery";
import { RequestAccessModal } from "./components/RequestAccessModal";
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
const generateRequestText = (request: TAccessApprovalRequest, membershipId: string) => {
const generateRequestText = (request: TAccessApprovalRequest, userId: string) => {
const { isTemporary } = request;
return (
@@ -63,7 +64,7 @@ const generateRequestText = (request: TAccessApprovalRequest, membershipId: stri
</code>
</div>
<div>
{request.requestedBy === membershipId && (
{request.requestedByUserId === userId && (
<span className="text-xs text-gray-500">
<Badge className="ml-1">Requested By You</Badge>
</span>
@@ -81,11 +82,11 @@ export const AccessApprovalRequest = ({
projectId: string;
}) => {
const [selectedRequest, setSelectedRequest] = useState<
(TAccessApprovalRequest & {
user: TWorkspaceUser["user"] | null;
isRequestedByCurrentUser: boolean;
isApprover: boolean;
})
| (TAccessApprovalRequest & {
user: TWorkspaceUser["user"] | null;
isRequestedByCurrentUser: boolean;
isApprover: boolean;
})
| null
>(null);
@@ -94,16 +95,19 @@ export const AccessApprovalRequest = ({
"reviewRequest",
"upgradePlan"
] as const);
const { membership, permission } = useProjectPermission();
const { permission } = useProjectPermission();
const { user } = useUser();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data: members } = useGetWorkspaceUsers(projectId);
const { data: members } = useGetWorkspaceUsers(projectId, true);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(prev, curr) => ({ ...prev, [curr.id]: curr }),
(prev, curr) => ({ ...prev, [curr.user.id]: curr }),
{}
);
console.log("membersGroupById", membersGroupById);
const [statusFilter, setStatusFilter] = useState<"open" | "close">("open");
const [requestedByFilter, setRequestedByFilter] = useState<string | undefined>(undefined);
const [envFilter, setEnvFilter] = useState<string | undefined>(undefined);
@@ -140,19 +144,18 @@ export const AccessApprovalRequest = ({
}, [requests, statusFilter, requestedByFilter, envFilter]);
const generateRequestDetails = (request: TAccessApprovalRequest) => {
const isReviewedByUser =
request.reviewers.findIndex(({ member }) => member === membership.id) !== -1;
console.log(request);
const isReviewedByUser = request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
const isRejectedByAnyone = request.reviewers.some(
({ status }) => status === ApprovalStatus.REJECTED
);
const isApprover = request.policy.approvers.indexOf(membership.id || "") !== -1;
const isApprover = request.policy.approvers.indexOf(user.id || "") !== -1;
const isAccepted = request.isApproved;
const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft;
const isRequestedByCurrentUser = request.requestedBy === membership.id;
const isRequestedByCurrentUser = request.requestedByUserId === user.id;
const userReviewStatus = request.reviewers.find(
({ member }) => member === membership.id
)?.status;
const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status;
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
label: "",
@@ -303,7 +306,7 @@ export const AccessApprovalRequest = ({
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
{members?.map(({ user, id }) => (
{members?.map(({ user: membershipUser, id }) => (
<DropdownMenuItem
onClick={() =>
setRequestedByFilter((state) => (state === id ? undefined : id))
@@ -312,7 +315,7 @@ export const AccessApprovalRequest = ({
icon={requestedByFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
{user.username}
{membershipUser.username}
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -341,22 +344,21 @@ export const AccessApprovalRequest = ({
tabIndex={0}
onClick={() => {
if (
(
!details.isApprover
|| details.isReviewedByUser
|| details.isRejectedByAnyone
|| details.isAccepted
) && !(
details.isSoftEnforcement
&& details.isRequestedByCurrentUser
&& !details.isAccepted
(!details.isApprover ||
details.isReviewedByUser ||
details.isRejectedByAnyone ||
details.isAccepted) &&
!(
details.isSoftEnforcement &&
details.isRequestedByCurrentUser &&
!details.isAccepted
)
)
return;
setSelectedRequest({
...request,
user: membersGroupById?.[request.requestedBy].user!,
user: membersGroupById?.[request.requestedByUserId].user!,
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
isApprover: details.isApprover
});
@@ -373,7 +375,7 @@ export const AccessApprovalRequest = ({
if (evt.key === "Enter") {
setSelectedRequest({
...request,
user: membersGroupById?.[request.requestedBy].user!,
user: membersGroupById?.[request.requestedByUserId].user!,
isRequestedByCurrentUser: details.isRequestedByCurrentUser,
isApprover: details.isApprover
});
@@ -385,16 +387,17 @@ export const AccessApprovalRequest = ({
<div className="flex w-full flex-col justify-between">
<div className="mb-1 flex w-full items-center">
<FontAwesomeIcon icon={faLock} className="mr-2" />
{generateRequestText(request, membership.id)}
{generateRequestText(request, user.id)}
</div>
<div className="flex items-center justify-between">
<div className="text-xs text-gray-500">
{membersGroupById?.[request.requestedBy]?.user && (
{membersGroupById?.[request.requestedByUserId]?.user && (
<>
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
ago by {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "}
{membersGroupById?.[request.requestedBy]?.user?.lastName} (
{membersGroupById?.[request.requestedBy]?.user?.email}){" "}
ago by{" "}
{membersGroupById?.[request.requestedByUserId]?.user?.firstName}{" "}
{membersGroupById?.[request.requestedByUserId]?.user?.lastName} (
{membersGroupById?.[request.requestedByUserId]?.user?.email}){" "}
</>
)}
</div>
@@ -1,5 +1,10 @@
import { useMemo,useState } from "react";
import { faCheckCircle,faChevronDown, faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons";
import { useMemo, useState } from "react";
import {
faCheckCircle,
faChevronDown,
faFileShield,
faPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
@@ -32,7 +37,12 @@ import {
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteAccessApprovalPolicy, useDeleteSecretApprovalPolicy, useGetSecretApprovalPolicies, useGetWorkspaceUsers } from "@app/hooks/api";
import {
useDeleteAccessApprovalPolicy,
useDeleteSecretApprovalPolicy,
useGetSecretApprovalPolicies,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
import { PolicyType } from "@app/hooks/api/policies/enums";
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
@@ -45,27 +55,32 @@ interface IProps {
}
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
const { data: accessPolicies, isLoading: isAccessPoliciesLoading } = useGetAccessApprovalPolicies({
projectSlug: currentWorkspace?.slug as string,
options: {
enabled:
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
!!currentWorkspace?.slug
const { data: accessPolicies, isLoading: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
{
projectSlug: currentWorkspace?.slug as string,
options: {
enabled:
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
!!currentWorkspace?.slug
}
}
});
const { data: secretPolicies, isLoading: isSecretPoliciesLoading } = useGetSecretApprovalPolicies({
workspaceId: currentWorkspace?.id as string,
options: {
enabled:
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
!!currentWorkspace?.id
);
const { data: secretPolicies, isLoading: isSecretPoliciesLoading } = useGetSecretApprovalPolicies(
{
workspaceId: currentWorkspace?.id as string,
options: {
enabled:
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) &&
!!currentWorkspace?.id
}
}
});
);
// merge data sorted by updatedAt
const policies = [
...(accessPolicies?.map(policy => ({ ...policy, policyType: PolicyType.AccessPolicy })) || []),
...(secretPolicies?.map(policy => ({ ...policy, policyType: PolicyType.ChangePolicy })) || [])
...(accessPolicies?.map((policy) => ({ ...policy, policyType: PolicyType.AccessPolicy })) ||
[]),
...(secretPolicies?.map((policy) => ({ ...policy, policyType: PolicyType.ChangePolicy })) || [])
].sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
});
@@ -86,15 +101,16 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const { data: members } = useGetWorkspaceUsers(workspaceId);
const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(permission, currentWorkspace);
const { data: members } = useGetWorkspaceUsers(workspaceId, true);
const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(
permission,
currentWorkspace
);
const [filterType, setFilterType] = useState<string | null>(null);
const filteredPolicies = useMemo(() => {
return filterType
? policies.filter(policy => policy.policyType === filterType)
: policies;
return filterType ? policies.filter((policy) => policy.policyType === filterType) : policies;
}, [policies, filterType]);
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
@@ -177,8 +193,10 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
<Button
variant="plain"
colorSchema="secondary"
className="text-bunker-300 uppercase text-xs font-semibold"
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
className="text-xs font-semibold uppercase text-bunker-300"
rightIcon={
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
}
>
Type
</Button>
@@ -194,14 +212,22 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setFilterType(PolicyType.AccessPolicy)}
icon={filterType === PolicyType.AccessPolicy && <FontAwesomeIcon icon={faCheckCircle} />}
icon={
filterType === PolicyType.AccessPolicy && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
Access Policy
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setFilterType(PolicyType.ChangePolicy)}
icon={filterType === PolicyType.ChangePolicy && <FontAwesomeIcon icon={faCheckCircle} />}
icon={
filterType === PolicyType.ChangePolicy && (
<FontAwesomeIcon icon={faCheckCircle} />
)
}
iconPos="right"
>
Change Policy
@@ -7,363 +7,378 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { policyDetails } from "@app/helpers/policies";
import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
import {
useCreateAccessApprovalPolicy,
useUpdateAccessApprovalPolicy
useCreateAccessApprovalPolicy,
useUpdateAccessApprovalPolicy
} from "@app/hooks/api/accessApproval";
import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types";
import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums";
import { TWorkspaceUser } from "@app/hooks/api/users/types";
type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
members?: TWorkspaceUser[];
projectSlug: string;
editValues?: TAccessApprovalPolicy;
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
members?: TWorkspaceUser[];
projectSlug: string;
editValues?: TAccessApprovalPolicy;
};
const formSchema = z
.object({
environment: z.string(),
name: z.string().optional(),
secretPath: z.string().optional(),
approvals: z.number().min(1),
approvers: z.string().array().min(1),
policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
});
.object({
environment: z.string(),
name: z.string().optional(),
secretPath: z.string().optional(),
approvals: z.number().min(1),
approverUserIds: z.string().array().min(1),
policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
});
type TFormSchema = z.infer<typeof formSchema>;
export const AccessPolicyForm = ({
isOpen,
onToggle,
members = [],
projectSlug,
editValues
isOpen,
onToggle,
members = [],
projectSlug,
editValues
}: Props) => {
const {
control,
handleSubmit,
reset,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: editValues ? {
...editValues,
environment: editValues.environment.slug,
approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
} : undefined
});
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
const isEditMode = Boolean(editValues);
useEffect(() => {
if (!isOpen || !isEditMode) reset({});
}, [isOpen, isEditMode]);
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
const {
control,
handleSubmit,
reset,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: editValues
? {
...editValues,
environment: editValues.environment.slug,
approverUserIds:
editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
}
: undefined
});
const { currentWorkspace } = useWorkspace();
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
const handleCreatePolicy = async (data: TFormSchema) => {
if (!projectSlug) return;
try {
if (data.policyType === PolicyType.ChangePolicy) {
await createSecretApprovalPolicy({
...data,
workspaceId: currentWorkspace?.id || ""
});
} else {
await createAccessApprovalPolicy({
...data,
projectSlug
});
}
createNotification({
type: "success",
text: "Successfully created policy"
});
onToggle(false);
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to create policy"
});
}
};
const handleUpdatePolicy = async (data: TFormSchema) => {
if (!projectSlug) return;
if (!editValues?.id) return;
try {
if (data.policyType === PolicyType.ChangePolicy) {
await updateSecretApprovalPolicy({
id: editValues?.id,
...data,
workspaceId: currentWorkspace?.id || ""
});
} else {
await updateAccessApprovalPolicy({
id: editValues?.id,
...data,
projectSlug
});
}
createNotification({
type: "success",
text: "Successfully updated policy"
});
onToggle(false);
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "failed to update policy"
});
}
};
const handleFormSubmit = async (data: TFormSchema) => {
if (isEditMode) {
await handleUpdatePolicy(data);
} else {
await handleCreatePolicy(data);
}
};
const environments = currentWorkspace?.environments || [];
const isEditMode = Boolean(editValues);
const formatEnforcementLevel = (level: EnforcementLevel) => {
if (level === EnforcementLevel.Hard) return "Hard";
if (level === EnforcementLevel.Soft) return "Soft";
return level;
};
return (
<Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalContent title={isEditMode ? `Edit ${policyName}` : "Create Policy"}>
<div className="flex flex-col space-y-3">
<form onSubmit={handleSubmit(handleFormSubmit)}>
<Controller
control={control}
name="policyType"
defaultValue={PolicyType.ChangePolicy}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Policy Type"
isRequired
isError={Boolean(error)}
tooltipText="Change polices govern secret changes within a given environment and secret path. Access polices allow underprivileged user to request access to environment/secret path."
errorText={error?.message}
>
<Select
isDisabled={isEditMode}
value={value}
onValueChange={(val) => onChange(val as PolicyType)}
className="w-full border border-mineshaft-500"
>
{Object.values(PolicyType).map((policyType) => {
return (
<SelectItem value={policyType} key={`policy-type-${policyType}`}>
{policyDetails[policyType].name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Policy Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
defaultValue={environments[0]?.slug}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
className="mt-4"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
isDisabled={isEditMode}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
>
{environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="approvers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Approvers"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>
Select members that are allowed to approve requests
</DropdownMenuLabel>
{members.map(({ id, user }) => {
const userId = watch("policyType") === PolicyType.ChangePolicy ? user.id : id;
const isChecked = value?.includes(userId);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked ? value?.filter((el: string) => el !== userId) : [...(value || []), userId]
);
}}
key={`create-policy-members-${userId}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{user.username}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
<Controller
control={control}
name="approvals"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Minimum Approvals Required"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="number"
min={1}
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="enforcementLevel"
defaultValue={EnforcementLevel.Hard}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Enforcement Level"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Determines the level of enforcement for required approvers of a request"
helperText={
field.value === EnforcementLevel.Hard
? "All approvers must approve the request."
: "All approvers must approve the request; however, the requester can bypass approval requirements in emergencies."
}
>
<Select
value={field.value}
onValueChange={(val) => field.onChange(val as EnforcementLevel)}
className="w-full border border-mineshaft-500"
>
{Object.values(EnforcementLevel).map((level) => {
return (
<SelectItem value={level} key={`enforcement-level-${level}`} className="text-xs">
{formatEnforcementLevel(level)}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
Save
</Button>
<Button onClick={() => onToggle(false)} variant="outline_bg">
Close
</Button>
</div>
</form>
</div>
</ModalContent>
</Modal>
);
useEffect(() => {
if (!isOpen || !isEditMode) reset({});
}, [isOpen, isEditMode]);
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
const policyName = policyDetails[watch("policyType")]?.name || "Policy";
const handleCreatePolicy = async (data: TFormSchema) => {
if (!projectSlug) return;
try {
if (data.policyType === PolicyType.ChangePolicy) {
await createSecretApprovalPolicy({
...data,
workspaceId: currentWorkspace?.id || ""
});
} else {
await createAccessApprovalPolicy({
...data,
projectSlug
});
}
createNotification({
type: "success",
text: "Successfully created policy"
});
onToggle(false);
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to create policy"
});
}
};
const handleUpdatePolicy = async (data: TFormSchema) => {
if (!projectSlug) return;
if (!editValues?.id) return;
try {
if (data.policyType === PolicyType.ChangePolicy) {
await updateSecretApprovalPolicy({
id: editValues?.id,
...data,
workspaceId: currentWorkspace?.id || ""
});
} else {
await updateAccessApprovalPolicy({
id: editValues?.id,
...data,
projectSlug
});
}
createNotification({
type: "success",
text: "Successfully updated policy"
});
onToggle(false);
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "failed to update policy"
});
}
};
const handleFormSubmit = async (data: TFormSchema) => {
if (isEditMode) {
await handleUpdatePolicy(data);
} else {
await handleCreatePolicy(data);
}
};
const formatEnforcementLevel = (level: EnforcementLevel) => {
if (level === EnforcementLevel.Hard) return "Hard";
if (level === EnforcementLevel.Soft) return "Soft";
return level;
};
return (
<Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalContent title={isEditMode ? `Edit ${policyName}` : "Create Policy"}>
<div className="flex flex-col space-y-3">
<form onSubmit={handleSubmit(handleFormSubmit)}>
<Controller
control={control}
name="policyType"
defaultValue={PolicyType.ChangePolicy}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Policy Type"
isRequired
isError={Boolean(error)}
tooltipText="Change polices govern secret changes within a given environment and secret path. Access polices allow underprivileged user to request access to environment/secret path."
errorText={error?.message}
>
<Select
isDisabled={isEditMode}
value={value}
onValueChange={(val) => onChange(val as PolicyType)}
className="w-full border border-mineshaft-500"
>
{Object.values(PolicyType).map((policyType) => {
return (
<SelectItem value={policyType} key={`policy-type-${policyType}`}>
{policyDetails[policyType].name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Policy Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
defaultValue={environments[0]?.slug}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Environment"
isRequired
className="mt-4"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
isDisabled={isEditMode}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
>
{environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Path"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} value={field.value || ""} />
</FormControl>
)}
/>
<Controller
control={control}
name="approverUserIds"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Approvers"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Input
isReadOnly
value={value?.length ? `${value.length} selected` : "None"}
className="text-left"
/>
</DropdownMenuTrigger>
<DropdownMenuContent
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>
Select members that are allowed to approve requests
</DropdownMenuLabel>
{members.map(({ user }) => {
const { id: userId } = user;
const isChecked = value?.includes(userId);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el: string) => el !== userId)
: [...(value || []), userId]
);
}}
key={`create-policy-members-${userId}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
{user.username}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
<Controller
control={control}
name="approvals"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Minimum Approvals Required"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="number"
min={1}
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="enforcementLevel"
defaultValue={EnforcementLevel.Hard}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Enforcement Level"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="Determines the level of enforcement for required approvers of a request"
helperText={
field.value === EnforcementLevel.Hard
? "All approvers must approve the request."
: "All approvers must approve the request; however, the requester can bypass approval requirements in emergencies."
}
>
<Select
value={field.value}
onValueChange={(val) => field.onChange(val as EnforcementLevel)}
className="w-full border border-mineshaft-500"
>
{Object.values(EnforcementLevel).map((level) => {
return (
<SelectItem
value={level}
key={`enforcement-level-${level}`}
className="text-xs"
>
{formatEnforcementLevel(level)}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
Save
</Button>
<Button onClick={() => onToggle(false)} variant="outline_bg">
Close
</Button>
</div>
</form>
</div>
</ModalContent>
</Modal>
);
};
@@ -405,8 +405,21 @@ export const SecretOverviewPage = () => {
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
console.log(folderName, parentPath);
if (folderName && parentPath) {
const canCreateFolder = permission.rules.some((rule) =>
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
)
? permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: slug,
secretPath: parentPath
})
)
: permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: parentPath })
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
projectId: workspaceId,
environment: slug,
@@ -584,22 +597,14 @@ export const SecretOverviewPage = () => {
</div>
{userAvailableEnvs.length > 0 && (
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
>
{(isAllowed) => (
<Button
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addSecretsInAllEnvs")}
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
</Button>
)}
</ProjectPermissionCan>
Add Secret
</Button>
<DropdownMenu
open={popUp.misc.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("misc", isOpen)}
@@ -1,5 +1,6 @@
import { ClipboardEvent } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -17,7 +18,12 @@ import {
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { useWorkspace } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { getKeyValue } from "@app/helpers/parseEnvVar";
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
@@ -62,6 +68,7 @@ export const CreateSecretForm = ({
const newSecretKey = watch("key");
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
@@ -86,7 +93,24 @@ export const CreateSecretForm = ({
const pathSegment = secretPath.split("/").filter(Boolean);
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
const folderName = pathSegment.at(-1);
if (folderName && parentPath) {
const canCreateFolder = permission.rules.some((rule) =>
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
)
? permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, {
environment: env.slug,
secretPath: parentPath
})
)
: permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: env.slug,
secretPath: parentPath
})
);
if (folderName && parentPath && canCreateFolder) {
await createFolder({
projectId: workspaceId,
path: parentPath,
@@ -186,39 +210,52 @@ export const CreateSecretForm = ({
/>
<FormLabel label="Environments" className="mb-2" />
<div className="thin-scrollbar grid max-h-64 grid-cols-3 gap-4 overflow-auto py-2">
{environments.map((env) => {
return (
<Controller
name={`environments.${env.slug}`}
key={`secret-input-${env.slug}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`secret-input-${env.slug}`}
className="!justify-start"
>
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
<span title={env.name} className="truncate">
{env.name}
{environments
.filter((environmentSlug) =>
permission.can(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: environmentSlug.slug,
secretPath
})
)
)
.map((env) => {
return (
<Controller
name={`environments.${env.slug}`}
key={`secret-input-${env.slug}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`secret-input-${env.slug}`}
className="!justify-start"
>
<span className="flex w-full flex-row items-center justify-start whitespace-pre-wrap">
<span title={env.name} className="truncate">
{env.name}
</span>
<span>
{getSecretByKey(env.slug, newSecretKey) && (
<Tooltip
className="max-w-[150px]"
content="Secret already exists, and it will be overwritten"
>
<FontAwesomeIcon
icon={faWarning}
className="ml-1 text-yellow-400"
/>
</Tooltip>
)}
</span>
</span>
<span>
{getSecretByKey(env.slug, newSecretKey) && (
<Tooltip
className="max-w-[150px]"
content="Secret already exists, and it will be overwritten"
>
<FontAwesomeIcon icon={faWarning} className="ml-1 text-yellow-400" />
</Tooltip>
)}
</span>
</span>
</Checkbox>
)}
/>
);
})}
</Checkbox>
)}
/>
);
})}
</div>
<div className="mt-7 flex items-center">
<Button