merge main

This commit is contained in:
Scott Wilson
2025-02-21 07:39:24 -08:00
96 changed files with 5468 additions and 317 deletions
@@ -40,7 +40,8 @@ enum TabSections {
Auth = "auth",
RateLimit = "rate-limit",
Integrations = "integrations",
Users = "users"
Users = "users",
Kmip = "kmip"
}
enum SignUpModes {
@@ -33,6 +33,7 @@ export const LoginPage = () => {
console.log("Error - Not logged in yet");
}
};
if (isLoggedIn()) {
handleRedirects();
}
@@ -250,20 +250,24 @@ export const SignupInvitePage = () => {
setStep(2);
} else {
const redirectExistingUser = async () => {
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
organizationId
});
try {
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
organizationId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
toggleShowMfa.on();
setMfaSuccessCallback(() => redirectExistingUser);
return;
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
toggleShowMfa.on();
setMfaSuccessCallback(() => redirectExistingUser);
return;
}
// user will be redirected to dashboard
// if not logged in gets kicked out to login
await navigateUserToOrg(navigate, organizationId);
} catch (err) {
navigate({ to: "/login" });
}
// user will be redirected to dashboard
// if not logged in gets kicked out to login
await navigateUserToOrg(navigate, organizationId);
};
await redirectExistingUser();
@@ -0,0 +1,36 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context";
import { KmipClientTable } from "./components/KmipClientTable";
export const KmipPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "KMS" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="KMIP"
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
/>
<ProjectPermissionCan
passThrough={false}
renderGuardBanner
I={ProjectPermissionKmipActions.ReadClients}
a={ProjectPermissionSub.Kmip}
>
<KmipClientTable />
</ProjectPermissionCan>
</div>
</div>
</div>
);
};
@@ -0,0 +1,147 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { useGenerateKmipClientCertificate } from "@app/hooks/api/kmip";
import { KmipClientCertificate, TKmipClient } from "@app/hooks/api/kmip/types";
const formSchema = z.object({
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
ttl: z.string()
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
kmipClient?: TKmipClient | null;
displayNewClientCertificate: (certificate: KmipClientCertificate) => void;
};
type FormProps = Pick<Props, "kmipClient" | "displayNewClientCertificate"> & {
onComplete: () => void;
};
const KmipClientCertificateForm = ({
displayNewClientCertificate,
kmipClient,
onComplete
}: FormProps) => {
const { mutateAsync: createKmipClientCertificate } = useGenerateKmipClientCertificate();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const handleKmipClientSubmit = async (payload: FormData) => {
if (!kmipClient) {
return;
}
const certificate = await createKmipClientCertificate({
...payload,
clientId: kmipClient?.id
});
createNotification({
text: "Successfully created KMIP client certificate",
type: "success"
});
displayNewClientCertificate(certificate);
onComplete();
};
return (
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl label="TTL" isError={Boolean(error)} errorText={error?.message} isRequired>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
helperText="This defines the key algorithm to use for signing the client certificate."
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Generate Client Certificate
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const CreateKmipClientCertificateModal = ({
isOpen,
onOpenChange,
kmipClient,
displayNewClientCertificate
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="Generate KMIP client certificate">
<KmipClientCertificateForm
onComplete={() => onOpenChange(false)}
displayNewClientCertificate={displayNewClientCertificate}
kmipClient={kmipClient}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,53 @@
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal } from "@app/components/v2";
import { useDeleteKmipClients } from "@app/hooks/api/kmip";
import { TKmipClient } from "@app/hooks/api/kmip/types";
type Props = {
kmipClient: TKmipClient;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
const deleteKmipClients = useDeleteKmipClients();
if (!kmipClient) return null;
const { id, projectId, name } = kmipClient;
const handleDeleteKmipClient = async () => {
try {
await deleteKmipClients.mutateAsync({
id,
projectId
});
createNotification({
text: "KMIP client successfully deleted",
type: "success"
});
onOpenChange(false);
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete KMIP client";
createNotification({
text,
type: "error"
});
}
};
return (
<DeleteActionModal
isOpen={isOpen}
title={`Are you sure want to delete ${name}?`}
onChange={onOpenChange}
deleteKey="confirm"
onDeleteApproved={handleDeleteKmipClient}
/>
);
};
@@ -0,0 +1,19 @@
import { Modal, ModalContent } from "@app/components/v2";
import { KmipClientCertificate } from "@app/hooks/api/kmip/types";
import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
certificate: KmipClientCertificate;
};
export const KmipClientCertificateModal = ({ isOpen, onOpenChange, certificate }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title="KMIP client certificate">
<CertificateContent {...certificate} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,194 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip";
import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types";
const KMIP_PERMISSIONS_OPTIONS = [
{ value: KmipPermission.Check, label: "Check" },
{ value: KmipPermission.Create, label: "Create" },
{ value: KmipPermission.Get, label: "Get" },
{ value: KmipPermission.Locate, label: "Locate" },
{ value: KmipPermission.Destroy, label: "Destroy" },
{ value: KmipPermission.Activate, label: "Activate" },
{ value: KmipPermission.Revoke, label: "Revoke" },
{ value: KmipPermission.GetAttributes, label: "Get Attributes" },
{ value: KmipPermission.Register, label: "Register" }
] as const;
const formSchema = z.object({
name: z.string().trim().min(1),
description: z.string().max(500).optional(),
permissions: z.object({
[KmipPermission.Check]: z.boolean().optional(),
[KmipPermission.Create]: z.boolean().optional(),
[KmipPermission.Get]: z.boolean().optional(),
[KmipPermission.Locate]: z.boolean().optional(),
[KmipPermission.Destroy]: z.boolean().optional(),
[KmipPermission.Activate]: z.boolean().optional(),
[KmipPermission.GetAttributes]: z.boolean().optional(),
[KmipPermission.Revoke]: z.boolean().optional(),
[KmipPermission.Register]: z.boolean().optional()
})
});
export type FormData = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
kmipClient?: TKmipClient | null;
};
type FormProps = Pick<Props, "kmipClient"> & {
onComplete: () => void;
};
const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
const createKmipClient = useCreateKmipClient();
const updateKmipClient = useUpdateKmipClient();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const isUpdate = !!kmipClient;
const {
control,
handleSubmit,
register,
formState: { isSubmitting, errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: kmipClient?.name,
description: kmipClient?.description,
permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true]))
}
});
const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => {
const mutation = isUpdate
? updateKmipClient.mutateAsync({
id: kmipClient.id,
projectId,
name,
description,
permissions: Object.entries(permissions)
.filter(([, value]) => value)
.map(([key]) => key as KmipPermission)
})
: createKmipClient.mutateAsync({
projectId,
name,
description,
permissions: Object.entries(permissions)
.filter(([, value]) => value)
.map(([key]) => key as KmipPermission)
});
try {
await mutation;
createNotification({
text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`,
type: "success"
});
onComplete();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
<FormControl
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="My KMIP Client" {...register("name")} />
</FormControl>
<FormControl
label="Description (optional)"
errorText={errors.description?.message}
isError={Boolean(errors.description?.message)}
>
<TextArea
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
{...register("description")}
/>
</FormControl>
<Controller
control={control}
name="permissions"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl label="Permissions" errorText={error?.message} isError={Boolean(error)}>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KMIP_PERMISSIONS_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"} KMIP client
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
export const KmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent title={`${kmipClient ? "Update" : "Add"} KMIP Client`}>
<KmipClientForm onComplete={() => onOpenChange(false)} kmipClient={kmipClient} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,338 @@
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faCertificate,
faEdit,
faEllipsis,
faMagnifyingGlass,
faPlus,
faTrash,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
ProjectPermissionKmipActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip";
import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types";
import { CreateKmipClientCertificateModal } from "./CreateKmipClientCertificateModal";
import { DeleteKmipClientModal } from "./DeleteKmipClientModal";
import { KmipClientCertificateModal } from "./KmipClientCertificateModal";
import { KmipClientModal } from "./KmipClientModal";
export const KmipClientTable = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id ?? "";
const {
offset,
limit,
orderBy,
orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination(KmipClientOrderBy.Name);
const { data, isPending, isFetching } = useGetKmipClientsByProjectId({
projectId,
offset,
limit,
search: debouncedSearch,
orderBy,
orderDirection
});
const { permission } = useProjectPermission();
const { subscription } = useSubscription();
const { kmipClients = [], totalCount = 0 } = data ?? {};
useResetPageHelper({
totalCount,
offset,
setPage
});
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upsertKmipClient",
"deleteKmipClient",
"generateKmipClientCert",
"displayKmipClientCert",
"upgradePlan"
] as const);
const handleSort = () => {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
};
const cannotEditKmipClient = permission.cannot(
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionSub.Kmip
);
const cannotDeleteKmipClient = permission.cannot(
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionSub.Kmip
);
const cannotGenerateKmipClientCertificate = permission.cannot(
ProjectPermissionKmipActions.GenerateClientCertificates,
ProjectPermissionSub.Kmip
);
return (
<motion.div
key="kmip-clients-tab"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="whitespace-nowrap text-xl font-semibold text-mineshaft-100">KMIP Clients</p>
<div className="flex w-full justify-end pr-4">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/kms"
>
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</a>
</div>
<ProjectPermissionCan
I={ProjectPermissionKmipActions.CreateClients}
a={ProjectPermissionSub.Kmip}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("upsertKmipClient", null);
}}
isDisabled={!isAllowed}
>
Add KMIP Client
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
containerClassName="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search clients by name..."
/>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={handleSort}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Description</Th>
<Th>Permissions</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="project-kmip-clients" />}
{!isPending &&
kmipClients.length > 0 &&
kmipClients.map((kmipClient) => {
const { name, id, description, permissions } = kmipClient;
return (
<Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}>
<Td>{name}</Td>
<Td className="max-w-80 break-all">{description}</Td>
<Td className="max-w-40">{[permissions.join(", ")]}</Td>
<Td className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
variant="plain"
colorSchema="primary"
className="ml-4 p-0 data-[state=open]:text-primary-400"
ariaLabel="More options"
>
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[160px]">
<Tooltip
content={
cannotGenerateKmipClientCertificate ? "Access Restricted" : ""
}
position="left"
>
<div>
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("generateKmipClientCert", kmipClient)
}
icon={<FontAwesomeIcon icon={faCertificate} />}
iconPos="left"
isDisabled={cannotGenerateKmipClientCertificate}
>
Generate Certificate
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotEditKmipClient ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("upsertKmipClient", kmipClient)}
icon={<FontAwesomeIcon icon={faEdit} />}
iconPos="left"
isDisabled={cannotEditKmipClient}
>
Edit KMIP Client
</DropdownMenuItem>
</div>
</Tooltip>
<Tooltip
content={cannotDeleteKmipClient ? "Access Restricted" : ""}
position="left"
>
<div>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteKmipClient", kmipClient)}
icon={<FontAwesomeIcon icon={faTrash} />}
iconPos="left"
isDisabled={cannotDeleteKmipClient}
>
Delete KMIP Client
</DropdownMenuItem>
</div>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{!isPending && kmipClients.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No KMIP clients match search filter"
: "No KMIP clients have been added to this project"
}
icon={faUser}
/>
)}
</TableContainer>
<DeleteKmipClientModal
isOpen={popUp.deleteKmipClient.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("deleteKmipClient", isOpen)}
kmipClient={popUp.deleteKmipClient.data as TKmipClient}
/>
<KmipClientModal
isOpen={popUp.upsertKmipClient.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)}
kmipClient={popUp.upsertKmipClient.data as TKmipClient | null}
/>
<CreateKmipClientCertificateModal
isOpen={popUp.generateKmipClientCert.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("generateKmipClientCert", isOpen)}
kmipClient={popUp.generateKmipClientCert.data as TKmipClient | null}
displayNewClientCertificate={(certificate) =>
handlePopUpOpen("displayKmipClientCert", certificate)
}
/>
<KmipClientCertificateModal
isOpen={popUp.displayKmipClientCert.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("displayKmipClientCert", isOpen)}
certificate={popUp.displayKmipClientCert.data}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="KMIP requires an enterprise plan."
/>
</div>
</motion.div>
);
};
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { KmipPage } from "./KmipPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip"
)({
component: KmipPage
});
@@ -1,16 +1,18 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { createNotification } from "@app/components/notifications";
import { ROUTE_PATHS } from "@app/const/routes";
import { userKeys } from "@app/hooks/api";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { clearSession, fetchUserDetails, logoutUser } from "@app/hooks/api/users/queries";
export const Route = createFileRoute("/_authenticate")({
beforeLoad: async ({ context, location }) => {
if (!context.serverConfig.initialized) {
throw redirect({ to: "/admin/signup" });
}
const data = await context.queryClient
.ensureQueryData({
queryKey: authKeys.getAuthToken,
@@ -27,14 +29,37 @@ export const Route = createFileRoute("/_authenticate")({
});
});
if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) {
if (
!data.organizationId &&
location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path &&
location.pathname !== "/organization/none"
) {
throw redirect({ to: "/login/select-organization" });
}
const user = await context.queryClient.ensureQueryData({
queryKey: userKeys.getUser,
queryFn: fetchUserDetails
});
const user = await context.queryClient
.ensureQueryData({
queryKey: userKeys.getUser,
queryFn: fetchUserDetails
})
.catch(async (error) => {
const err = error as AxiosError;
if (err.response?.status === 403) {
// (dangtony98): this edge-case can occur if the user's token corresponds to an organization
// that has been deleted for which we must clear the refresh token in http-only cookie
createNotification({
type: "error",
title: "Access Denied",
text: "Something went wrong with your session. Please log in again."
});
}
clearSession(true);
await logoutUser();
throw redirect({
to: "/login"
});
});
return { organizationId: data.organizationId as string, isAuthenticated: true, user };
}
@@ -94,6 +94,15 @@ export const LogsFilter = ({
{actor.metadata.name}
</SelectItem>
);
case ActorType.KMIP_CLIENT:
return (
<SelectItem
value={`${actor.type}-${actor.metadata.clientId}`}
key={`kmip-client-filter-${actor.metadata.clientId}`}
>
{actor.metadata.name}
</SelectItem>
);
default:
return (
<SelectItem value="actor-none" key="actor-none">
@@ -46,6 +46,13 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
<p>Platform</p>
</Td>
);
case ActorType.KMIP_CLIENT:
return (
<Td>
<p>{actor.metadata.name}</p>
<p>KMIP Client</p>
</Td>
);
case ActorType.UNKNOWN_USER:
return (
<Td>
@@ -20,7 +20,7 @@ export const NoOrgPage = () => {
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="min-h-screen bg-bunker-800">
<CreateOrgModal
isOpen={popUp.createOrg.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
@@ -2,8 +2,6 @@ import { createFileRoute } from "@tanstack/react-router";
import { NoOrgPage } from "./NoOrgPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/none"
)({
export const Route = createFileRoute("/_authenticate/organization/none")({
component: NoOrgPage
});
@@ -2,7 +2,10 @@
import { z } from "zod";
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import {
OrgPermissionAppConnectionActions,
OrgPermissionKmipActions
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
const generalPermissionSchema = z
@@ -24,6 +27,13 @@ const appConnectionsPermissionSchema = z
})
.optional();
const kmipPermissionSchema = z
.object({
[OrgPermissionKmipActions.Proxy]: z.boolean().optional(),
[OrgPermissionKmipActions.Setup]: z.boolean().optional()
})
.optional();
const adminConsolePermissionSchmea = z
.object({
"access-all-projects": z.boolean().optional()
@@ -61,7 +71,8 @@ export const formSchema = z.object({
"organization-admin-console": adminConsolePermissionSchmea,
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
"app-connections": appConnectionsPermissionSchema
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema
})
.optional()
});
@@ -0,0 +1,133 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [
{ action: "proxy", label: "Proxy KMIP requests" },
{ action: "setup", label: "Setup KMIP" }
] as const;
export const OrgPermissionKmipRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.kmip"
});
const selectedPermissionCategory = useMemo(() => {
if (rule?.proxy || rule?.setup) {
return Permission.Custom;
}
return Permission.NoAccess;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
if (val === Permission.NoAccess) {
setValue("permissions.kmip", { proxy: false, setup: false }, { shouldDirty: true });
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>KMIP</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.kmip.${action}`}
key={`permissions.kmip.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.kmip.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -74,7 +74,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console"
"workspace" | "organization-admin-console" | "kmip"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
@@ -14,6 +14,7 @@ import {
TFormSchema
} from "../OrgRoleModifySection.utils";
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@@ -180,6 +181,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionKmipRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
</TBody>
</Table>
</TableContainer>
@@ -0,0 +1,268 @@
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 { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
IconButton,
Modal,
ModalContent,
Select,
SelectItem,
Spinner,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { downloadTxtFile } from "@app/helpers/download";
import { usePopUp, useTimedReset } from "@app/hooks";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { useGetOrgKmipConfig, useSetupOrgKmip } from "@app/hooks/api/kmip";
import { OrgKmipConfig } from "@app/hooks/api/kmip/types";
const orgConfigFormSchema = z.object({
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
});
type TKmipOrgConfigForm = z.infer<typeof orgConfigFormSchema>;
const OrgConfigSection = ({
kmipConfig,
isKmipConfigLoading
}: {
kmipConfig?: OrgKmipConfig;
isKmipConfigLoading: boolean;
}) => {
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"configureKmip",
"upgradePlan"
] as const);
const { subscription } = useSubscription();
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm<TKmipOrgConfigForm>({
resolver: zodResolver(orgConfigFormSchema)
});
const { currentOrg } = useOrganization();
const { mutateAsync: setupOrgKmip } = useSetupOrgKmip(currentOrg.id);
const onFormSubmit = async (formData: TKmipOrgConfigForm) => {
await setupOrgKmip(formData);
createNotification({
type: "success",
text: "Successfully configured KMIP"
});
handlePopUpClose("configureKmip");
};
const [copyTextClientCertificate, isCopyingClientCertificate, setCopyTextClientCertificate] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
const [copyTextServerCertificate, isCopyingServerCertificate, setCopyTextServerCertificate] =
useTimedReset<string>({
initialState: "Copy to clipboard"
});
return (
<>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">KMIP configuration</div>
{isKmipConfigLoading && (
<div className="mt-8 flex justify-center">
<Spinner />
</div>
)}
{!isKmipConfigLoading && kmipConfig && (
<>
<div className="mt-2">
<div className="text-lg">Certificate Chain for KMIP Clients</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
This certificate chain is used by KMIP clients to verify the identity of the KMIP
server. It should be presented by the server during TLS authentication to establish
a secure and encrypted connection.
</div>
<div className="flex max-w-2xl">
<div className="flex w-full justify-end">
<Tooltip content={copyTextClientCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(kmipConfig.serverCertificateChain);
setCopyTextClientCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingClientCertificate ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("ca-chain.pem", kmipConfig.serverCertificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<TextArea
value={kmipConfig.serverCertificateChain}
reSize="none"
className="mt-2 h-48 max-w-2xl"
/>
</div>
<div className="mt-8">
<div className="text-lg">Certificate Chain for KMIP Server</div>
<div className="mt-2 max-w-lg text-sm text-mineshaft-400">
This certificate chain is used by the KMIP server to verify the identity of KMIP
clients. It should be configured on the server to establish trust in client
certificates during mutual TLS authentication.
</div>
<div className="flex max-w-2xl">
<div className="flex w-full justify-end">
<Tooltip content={copyTextServerCertificate}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(kmipConfig.clientCertificateChain);
setCopyTextServerCertificate("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingServerCertificate ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("ca-chain.pem", kmipConfig.clientCertificateChain);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<TextArea
value={kmipConfig.clientCertificateChain}
reSize="none"
className="mt-2 h-48 max-w-2xl"
/>
</div>
</>
)}
{!isKmipConfigLoading && !kmipConfig && (
<div className="mt-2">
<div>KMIP has not yet been configured for the organization.</div>
<Button
className="mt-2"
onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("configureKmip");
}}
>
Setup KMIP
</Button>
</div>
)}
</div>
<Modal
isOpen={popUp.configureKmip.isOpen}
onOpenChange={(state) => handlePopUpToggle("configureKmip", state)}
>
<ModalContent title="Configure KMIP for the organization">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="caKeyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="CA Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
helperText="This defines the key algorithm used for generating the KMIP Root CA and Intermediate CAs, which sign all KMIP server and client certificates."
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="mt-6 flex w-full gap-4">
<Button
className=""
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Continue
</Button>
<Button
className=""
size="sm"
variant="outline_bg"
type="button"
onClick={() => handlePopUpClose("configureKmip")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="KMIP requires an enterprise plan."
/>
</>
);
};
export const KmipTab = () => {
const { currentOrg } = useOrganization();
const { data: kmipConfig, isPending } = useGetOrgKmipConfig(currentOrg.id);
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<OrgConfigSection kmipConfig={kmipConfig} isKmipConfigLoading={isPending} />
</div>
);
};
@@ -6,6 +6,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { ImportTab } from "../ImportTab";
import { KmipTab } from "../KmipTab/OrgKmipTab";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
@@ -27,7 +28,8 @@ export const OrgTabGroup = () => {
},
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
{ name: "Import", key: "tab-import", component: ImportTab },
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab },
{ name: "KMIP", key: "kmip", component: KmipTab }
];
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
@@ -8,6 +8,7 @@ import {
import {
PermissionConditionOperators,
ProjectPermissionDynamicSecretActions,
ProjectPermissionKmipActions,
ProjectPermissionSecretSyncActions,
TPermissionCondition,
TPermissionConditionOperators
@@ -48,6 +49,14 @@ const SecretSyncPolicyActionSchema = z.object({
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
});
const KmipPolicyActionSchema = z.object({
[ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.UpdateClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.DeleteClients]: z.boolean().optional(),
[ProjectPermissionKmipActions.GenerateClientCertificates]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(),
create: z.boolean().optional()
@@ -162,7 +171,8 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([])
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([])
})
.partial()
.optional()
@@ -359,6 +369,28 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.Kmip) {
const canReadClients = action.includes(ProjectPermissionKmipActions.ReadClients);
const canEditClients = action.includes(ProjectPermissionKmipActions.UpdateClients);
const canDeleteClients = action.includes(ProjectPermissionKmipActions.DeleteClients);
const canCreateClients = action.includes(ProjectPermissionKmipActions.CreateClients);
const canGenerateClientCerts = action.includes(
ProjectPermissionKmipActions.GenerateClientCertificates
);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canReadClients) formVal[subject]![0][ProjectPermissionKmipActions.ReadClients] = true;
if (canEditClients) formVal[subject]![0][ProjectPermissionKmipActions.UpdateClients] = true;
if (canCreateClients) formVal[subject]![0][ProjectPermissionKmipActions.CreateClients] = true;
if (canDeleteClients) formVal[subject]![0][ProjectPermissionKmipActions.DeleteClients] = true;
if (canGenerateClientCerts)
formVal[subject]![0][ProjectPermissionKmipActions.GenerateClientCertificates] = true;
return;
}
if (subject === ProjectPermissionSub.SecretSyncs) {
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
@@ -738,5 +770,30 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
value: ProjectPermissionSecretSyncActions.RemoveSecrets
}
]
},
[ProjectPermissionSub.Kmip]: {
title: "KMIP",
actions: [
{
label: "Read clients",
value: ProjectPermissionKmipActions.ReadClients
},
{
label: "Create clients",
value: ProjectPermissionKmipActions.CreateClients
},
{
label: "Modify clients",
value: ProjectPermissionKmipActions.UpdateClients
},
{
label: "Delete clients",
value: ProjectPermissionKmipActions.DeleteClients
},
{
label: "Generate client certificates",
value: ProjectPermissionKmipActions.GenerateClientCertificates
}
]
}
};