mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 16:52:38 +00:00
Merge remote-tracking branch 'origin/main' into ENG-2661
This commit is contained in:
@@ -15,7 +15,10 @@ export const AlertingPage = () => {
|
||||
<title>{t("common.head-title", { title: "Alerting" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Alerting" />
|
||||
<PageHeader
|
||||
title="Alerting"
|
||||
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
|
||||
@@ -63,7 +63,7 @@ const Page = () => {
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
|
||||
+4
-1
@@ -15,7 +15,10 @@ export const CertificateAuthoritiesPage = () => {
|
||||
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Certificate Authorities" />
|
||||
<PageHeader
|
||||
title="Certificate Authorities"
|
||||
description="Manage internal private certificate authorities for issuing and signing certificates, including root and intermediate CAs."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
|
||||
@@ -32,7 +32,10 @@ export const CertificatesPage = () => {
|
||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Certificates" />
|
||||
<PageHeader
|
||||
title="Certificates"
|
||||
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
||||
/>
|
||||
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
|
||||
{(canAccessCerts || canAccessPkiColl) && (
|
||||
<ProjectPermissionCan
|
||||
|
||||
@@ -71,7 +71,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Friendly Name</Th>
|
||||
<Th>Common Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Not Before</Th>
|
||||
<Th>Not After</Th>
|
||||
@@ -85,7 +85,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.friendlyName}</Td>
|
||||
<Td>{certificate.commonName}</Td>
|
||||
<Td>
|
||||
{certificate.status === CertStatus.REVOKED ? (
|
||||
<Badge variant="danger">Revoked</Badge>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CertificatesPage } from "./CertificatesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview"
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates"
|
||||
)({
|
||||
component: CertificatesPage
|
||||
});
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ export const PkiCollectionPage = () => {
|
||||
});
|
||||
handlePopUpClose("deletePkiCollection");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/overview` as const,
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/certificates` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Collections",
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/overview",
|
||||
to: "/cert-manager/$projectId/certificates",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
PageHeader,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal";
|
||||
import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from "./components";
|
||||
|
||||
const Page = () => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const subscriberName = useParams({
|
||||
from: ROUTE_PATHS.CertManager.PkiSubscriberDetailsByIDPage.id,
|
||||
select: (el) => el.subscriberName
|
||||
});
|
||||
const { data } = useGetPkiSubscriber({
|
||||
subscriberName,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"pkiSubscriber",
|
||||
"deletePkiSubscriber"
|
||||
] as const);
|
||||
|
||||
const onRemoveSubscriberSubmit = async (subscriberNameToDelete: string) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
await deletePkiSubscriber({ subscriberName: subscriberNameToDelete, projectId });
|
||||
|
||||
createNotification({
|
||||
text: "Successfully deleted subscriber",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deletePkiSubscriber");
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/subscribers` as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete subscriber",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<Button variant="outline_bg">More</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deletePkiSubscriber", {
|
||||
subscriberName: data.name
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete PKI Subscriber
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<PkiSubscriberDetailsSection
|
||||
subscriberName={data.name}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<PkiSubscriberCertificatesSection subscriberName={data.name} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<PkiSubscriberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePkiSubscriber.isOpen}
|
||||
title={`Are you sure you want to remove the PKI subscriber: ${
|
||||
(popUp?.deletePkiSubscriber?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePkiSubscriber", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveSubscriberSubmit(
|
||||
(popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PkiSubscriberDetailsByIDPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PKI Subscriber" })}</title>
|
||||
</Helmet>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Read}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
passThrough={false}
|
||||
renderGuardBanner
|
||||
>
|
||||
<Page />
|
||||
</ProjectPermissionCan>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { CertificateRevocationModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateRevocationModal";
|
||||
|
||||
import { PkiSubscriberCertificatesTable } from "./PkiSubscriberCertificatesTable";
|
||||
|
||||
type Props = {
|
||||
subscriberName: string;
|
||||
};
|
||||
|
||||
export const PkiSubscriberCertificatesSection = ({ subscriberName }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["revokeCertificate"] as const);
|
||||
|
||||
return (
|
||||
<div className="h-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Certificates</h3>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<PkiSubscriberCertificatesTable
|
||||
subscriberName={subscriberName}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
</div>
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useGetPkiSubscriberCertificates } from "@app/hooks/api";
|
||||
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
subscriberName: string;
|
||||
handlePopUpOpen?: (popUpName: keyof UsePopUpState<["revokeCertificate"]>, data?: object) => void;
|
||||
};
|
||||
|
||||
const PER_PAGE_INIT = 25;
|
||||
|
||||
export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const { permission } = useProjectPermission();
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
||||
|
||||
const { data, isPending } = useGetPkiSubscriberCertificates({
|
||||
subscriberName,
|
||||
projectId,
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage
|
||||
});
|
||||
|
||||
const getCertStatusBadge = (status: string, notAfter: string) => {
|
||||
if (status === CertStatus.REVOKED) {
|
||||
return <Badge variant="danger">Revoked</Badge>;
|
||||
}
|
||||
|
||||
const expiryDate = new Date(notAfter);
|
||||
const now = new Date();
|
||||
const daysUntilExpiry = Math.floor(
|
||||
(expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (daysUntilExpiry < 0) {
|
||||
return <Badge variant="danger">Expired</Badge>;
|
||||
}
|
||||
|
||||
if (daysUntilExpiry < 30) {
|
||||
return <Badge variant="primary">Expiring Soon</Badge>;
|
||||
}
|
||||
|
||||
return <Badge variant="success">Valid</Badge>;
|
||||
};
|
||||
|
||||
const canListPkiSubscriberCerts = permission.can(
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriberName
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Common Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Not Before</Th>
|
||||
<Th>Not After</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={5} innerKey="pki-subscriber-certificates" />}
|
||||
{!isPending &&
|
||||
data?.certificates?.map((certificate) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
||||
<Td>{certificate.commonName}</Td>
|
||||
<Td>{getCertStatusBadge(certificate.status, certificate.notAfter)}</Td>
|
||||
<Td>
|
||||
{certificate.notBefore
|
||||
? format(new Date(certificate.notBefore), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td>
|
||||
{certificate.notAfter
|
||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen &&
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Revoke Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !data?.certificates?.length && (
|
||||
<EmptyState
|
||||
title={`${canListPkiSubscriberCerts ? "No certificates have been issued for this subscriber" : "You do not have permission to view this subscriber's certificates"}`}
|
||||
icon={faCertificate}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, IconButton, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useGetPkiSubscriber, useIssuePkiSubscriberCert } from "@app/hooks/api";
|
||||
import { pkiSubscriberStatusToNameMap } from "@app/hooks/api/pkiSubscriber/constants";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CertificateContent } from "../../CertificatesPage/components/CertificateContent";
|
||||
|
||||
type Props = {
|
||||
subscriberName: string;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, data?: object) => void;
|
||||
};
|
||||
|
||||
type TCertificateDetails = {
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const { permission } = useProjectPermission();
|
||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
||||
subscriberName,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { mutateAsync: issuePkiSubscriberCert, isPending: isIssuingCert } =
|
||||
useIssuePkiSubscriberCert();
|
||||
|
||||
const onIssuePkiSubscriberCert = async () => {
|
||||
try {
|
||||
const response = await issuePkiSubscriberCert({ subscriberName, projectId });
|
||||
|
||||
setCertificateDetails({
|
||||
serialNumber: response.serialNumber,
|
||||
certificate: response.certificate,
|
||||
certificateChain: response.certificateChain,
|
||||
privateKey: response.privateKey
|
||||
});
|
||||
|
||||
setIsModalOpen(true);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully issued certificate",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to issue certificate",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const canIssuePkiSubscriberCert = permission.can(
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: pkiSubscriber?.name ?? ""
|
||||
})
|
||||
);
|
||||
|
||||
return pkiSubscriber ? (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">PKI Subscriber Details</h3>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Edit}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Tooltip content="Edit PKI Subscriber">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("pkiSubscriber", {
|
||||
subscriberName: pkiSubscriber.name
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">PKI Subscriber ID</p>
|
||||
<div className="group flex align-top">
|
||||
<p className="text-sm text-mineshaft-300">{pkiSubscriber.id}</p>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content={copyTextId}>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(pkiSubscriber.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{pkiSubscriber.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{pkiSubscriberStatusToNameMap[pkiSubscriber.status]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-semibold text-mineshaft-300">Common Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{pkiSubscriber.commonName}</p>
|
||||
</div>
|
||||
{canIssuePkiSubscriberCert && (
|
||||
<Button
|
||||
className="mt-4 w-full"
|
||||
colorSchema="primary"
|
||||
type="button"
|
||||
isLoading={isIssuingCert}
|
||||
onClick={() => {
|
||||
onIssuePkiSubscriberCert();
|
||||
}}
|
||||
>
|
||||
Issue Certificate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
setIsModalOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
setCertificateDetails(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Certificate Details">
|
||||
{certificateDetails && (
|
||||
<CertificateContent
|
||||
serialNumber={certificateDetails.serialNumber}
|
||||
certificate={certificateDetails.certificate}
|
||||
certificateChain={certificateDetails.certificateChain}
|
||||
privateKey={certificateDetails.privateKey}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { PkiSubscriberCertificatesSection } from "./PkiSubscriberCertificatesSection";
|
||||
export { PkiSubscriberDetailsSection } from "./PkiSubscriberDetailsSection";
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
)({
|
||||
component: PkiSubscriberDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
return {
|
||||
breadcrumbs: [
|
||||
...context.breadcrumbs,
|
||||
{
|
||||
label: "Subscribers",
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/subscribers",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
|
||||
import { PkiSubscriberSection } from "./components";
|
||||
|
||||
export const PkiSubscribersPage = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "PKI Subscribers" })}</title>
|
||||
</Helmet>
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Subscribers"
|
||||
description="Manage subscribers that request and receive certificates, including user devices, servers, and services."
|
||||
/>
|
||||
<PkiSubscriberSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,428 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useCreatePkiSubscriber,
|
||||
useGetPkiSubscriber,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspacePkiSubscribers,
|
||||
useUpdatePkiSubscriber
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
EXTENDED_KEY_USAGES_OPTIONS,
|
||||
KEY_USAGES_OPTIONS
|
||||
} from "@app/hooks/api/certificates/constants";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["pkiSubscriber"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1, "Name is required"),
|
||||
caId: z.string().min(1, "Issuing CA is required"),
|
||||
commonName: z.string().trim().min(1, "Common Name is required"),
|
||||
subjectAlternativeNames: z.string(),
|
||||
ttl: z.string().trim(),
|
||||
keyUsages: z.object({
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||
}),
|
||||
extendedKeyUsages: z.object({
|
||||
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||
})
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
|
||||
const { data: cas } = useListWorkspaceCas({
|
||||
projectSlug: currentWorkspace?.slug ?? "",
|
||||
status: CaStatus.ACTIVE
|
||||
});
|
||||
|
||||
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
||||
subscriberName:
|
||||
(popUp?.pkiSubscriber?.data as { subscriberName: string })?.subscriberName || "",
|
||||
projectId
|
||||
});
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreatePkiSubscriber();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdatePkiSubscriber();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
caId: "",
|
||||
commonName: "",
|
||||
subjectAlternativeNames: "",
|
||||
ttl: "",
|
||||
keyUsages: {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||
},
|
||||
extendedKeyUsages: {}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pkiSubscriber) {
|
||||
reset({
|
||||
name: pkiSubscriber.name,
|
||||
caId: pkiSubscriber.caId || "",
|
||||
commonName: pkiSubscriber.commonName,
|
||||
subjectAlternativeNames: pkiSubscriber.subjectAlternativeNames.join(", ") || "",
|
||||
ttl: pkiSubscriber.ttl || "",
|
||||
keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])),
|
||||
extendedKeyUsages: Object.fromEntries(
|
||||
(pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true])
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
caId: "",
|
||||
commonName: "",
|
||||
subjectAlternativeNames: "",
|
||||
ttl: "",
|
||||
keyUsages: {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||
},
|
||||
extendedKeyUsages: {}
|
||||
});
|
||||
}
|
||||
}, [pkiSubscriber, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cas?.length) {
|
||||
setValue("caId", cas[0].id);
|
||||
}
|
||||
}, [cas, setValue]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
caId,
|
||||
commonName,
|
||||
subjectAlternativeNames,
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
|
||||
if (!caId) {
|
||||
createNotification({
|
||||
text: "Please select an Issuing CA",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there is already a different subscriber with the same name
|
||||
const existingNames =
|
||||
subscribers?.filter((s) => s.id !== pkiSubscriber?.id).map((s) => s.name) || [];
|
||||
|
||||
if (existingNames.includes(name.trim())) {
|
||||
createNotification({
|
||||
text: "A subscriber with this name already exists.",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keyUsagesList = Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage);
|
||||
|
||||
const extendedKeyUsagesList = Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage);
|
||||
|
||||
const subjectAlternativeNamesList = subjectAlternativeNames
|
||||
.split(",")
|
||||
.map((san) => san.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (pkiSubscriber) {
|
||||
await updateMutateAsync({
|
||||
subscriberName: pkiSubscriber.name,
|
||||
projectId,
|
||||
name,
|
||||
caId,
|
||||
commonName,
|
||||
subjectAlternativeNames: subjectAlternativeNamesList,
|
||||
ttl,
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
projectId,
|
||||
name,
|
||||
caId,
|
||||
commonName,
|
||||
subjectAlternativeNames: subjectAlternativeNamesList,
|
||||
ttl,
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("pkiSubscriber", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${pkiSubscriber ? "updated" : "added"} PKI subscriber`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${pkiSubscriber ? "update" : "add"} PKI subscriber`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.pkiSubscriber?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
reset();
|
||||
handlePopUpToggle("pkiSubscriber", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{pkiSubscriber && (
|
||||
<FormControl label="Subscriber ID">
|
||||
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subscriber Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="web-service" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caId"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Issuing CA"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(cas || []).map(({ id, dn }) => (
|
||||
<SelectItem value={id} key={`ca-${id}`}>
|
||||
{dn}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="commonName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Common Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<Input {...field} placeholder="web.example.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="subjectAlternativeNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Subject Alternative Names (SANs)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Key Usage</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="keyUsages"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Key Usage"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{KEY_USAGES_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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="extendedKeyUsages"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
label="Extended Key Usage"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||
{EXTENDED_KEY_USAGES_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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{pkiSubscriber ? "Update" : "Add"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("pkiSubscriber", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
|
||||
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { PkiSubscriberModal } from "./PkiSubscriberModal";
|
||||
import { PkiSubscribersTable } from "./PkiSubscribersTable";
|
||||
|
||||
export const PkiSubscriberSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectId = currentWorkspace.id;
|
||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"pkiSubscriber",
|
||||
"pkiSubscriberStatus", // enable / disable
|
||||
"deletePkiSubscriber"
|
||||
] as const);
|
||||
|
||||
const onRemovePkiSubscriberSubmit = async (subscriberName: string) => {
|
||||
try {
|
||||
const subscriber = await deletePkiSubscriber({ subscriberName, projectId });
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted PKI subscriber: ${subscriber.name}`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deletePkiSubscriber");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete PKI subscriber",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdatePkiSubscriberStatus = async ({
|
||||
subscriberName,
|
||||
status
|
||||
}: {
|
||||
subscriberName: string;
|
||||
status: PkiSubscriberStatus;
|
||||
}) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
await updatePkiSubscriber({ subscriberName, projectId, status });
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${status === PkiSubscriberStatus.ACTIVE ? "enabled" : "disabled"} subscriber`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("pkiSubscriberStatus");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${status === PkiSubscriberStatus.ACTIVE ? "enable" : "disable"} subscriber`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const subscriberStatusData = popUp?.pkiSubscriberStatus?.data as {
|
||||
status: PkiSubscriberStatus;
|
||||
subscriberName: string;
|
||||
};
|
||||
|
||||
const isEnabling = subscriberStatusData?.status === PkiSubscriberStatus.ACTIVE;
|
||||
const subscriberName = subscriberStatusData?.subscriberName || "";
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Subscribers</p>
|
||||
<div className="flex w-full justify-end">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
|
||||
>
|
||||
<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>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Create}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("pkiSubscriber")}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-4"
|
||||
>
|
||||
Add Subscriber
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<PkiSubscribersTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<PkiSubscriberModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.pkiSubscriberStatus.isOpen}
|
||||
title={`Are you sure you want to ${isEnabling ? "enable" : "disable"} the subscriber ${subscriberName}?`}
|
||||
subTitle={
|
||||
isEnabling
|
||||
? "This action will allow issuing certificates for this subscriber again."
|
||||
: "This action will prevent issuing certificates for this subscriber."
|
||||
}
|
||||
onChange={(isOpen) => handlePopUpToggle("pkiSubscriberStatus", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonColorSchema={isEnabling ? "primary" : "danger"}
|
||||
buttonText={isEnabling ? "Enable" : "Disable"}
|
||||
onDeleteApproved={() => onUpdatePkiSubscriberStatus(subscriberStatusData)}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePkiSubscriber.isOpen}
|
||||
title="Are you sure you want to remove the PKI subscriber?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePkiSubscriber", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemovePkiSubscriberSubmit(
|
||||
(popUp?.deletePkiSubscriber?.data as { subscriberName: string })?.subscriberName
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
faBan,
|
||||
faEllipsis,
|
||||
faPencil,
|
||||
faTrash,
|
||||
faUserShield
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
|
||||
import {
|
||||
getPkiSubscriberStatusBadgeVariant,
|
||||
PkiSubscriberStatus,
|
||||
pkiSubscriberStatusToNameMap
|
||||
} from "@app/hooks/api/pkiSubscriber/constants";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deletePkiSubscriber", "pkiSubscriber", "pkiSubscriberStatus"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useListWorkspacePkiSubscribers(currentWorkspace?.id || "");
|
||||
return (
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table className="w-full table-fixed">
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Common Name</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="pki-subscribers" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map((subscriber) => {
|
||||
return (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`pki-subscriber-${subscriber.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberName` as const,
|
||||
params: {
|
||||
projectId: currentWorkspace.id,
|
||||
subscriberName: subscriber.name
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{subscriber.name}</Td>
|
||||
<Td>
|
||||
<Badge variant={getPkiSubscriberStatusBadgeVariant(subscriber.status)}>
|
||||
{pkiSubscriberStatusToNameMap[subscriber.status]}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{subscriber.commonName}</Td>
|
||||
<Td className="text-right align-middle">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Edit}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("pkiSubscriber", {
|
||||
subscriberName: subscriber.name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faPencil} />}
|
||||
>
|
||||
Edit Subscriber
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Edit}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("pkiSubscriberStatus", {
|
||||
subscriberName: subscriber.name,
|
||||
status:
|
||||
subscriber.status === PkiSubscriberStatus.ACTIVE
|
||||
? PkiSubscriberStatus.DISABLED
|
||||
: PkiSubscriberStatus.ACTIVE
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
{`${subscriber.status === PkiSubscriberStatus.ACTIVE ? "Disable" : "Enable"} Subscriber`}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deletePkiSubscriber", {
|
||||
subscriberName: subscriber.name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete Subscriber
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && data?.length === 0 && (
|
||||
<EmptyState title="No PKI subscribers have been added" icon={faUserShield} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PkiSubscriberSection } from "./PkiSubscriberSection";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { PkiSubscribersPage } from "./PkiSubscribersPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/"
|
||||
)({
|
||||
component: PkiSubscribersPage
|
||||
});
|
||||
@@ -31,7 +31,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: project.name,
|
||||
link: linkOptions({
|
||||
to: "/cert-manager/$projectId/overview",
|
||||
to: "/cert-manager/$projectId/subscribers",
|
||||
params: { projectId: project.id }
|
||||
})
|
||||
}
|
||||
|
||||
+9
@@ -58,6 +58,15 @@ export const OrgAdminProjects = withPermission(
|
||||
await orgAdminAccessProject.mutateAsync({
|
||||
projectId
|
||||
});
|
||||
if (type === ProjectType.CertificateManager) {
|
||||
await navigate({
|
||||
to: "/cert-manager/$projectId/subscribers" as const,
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
await navigate({
|
||||
to: `/${type}/$projectId/overview` as const,
|
||||
params: {
|
||||
|
||||
@@ -1,37 +1,33 @@
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import {
|
||||
createFileRoute,
|
||||
linkOptions,
|
||||
stripSearchParams,
|
||||
} from '@tanstack/react-router'
|
||||
import { zodValidator } from '@tanstack/zod-adapter'
|
||||
import { z } from 'zod'
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SettingsPage } from './SettingsPage'
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
|
||||
const SettingsPageQueryParams = z.object({
|
||||
selectedTab: z.string().catch(''),
|
||||
})
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/settings/',
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings/"
|
||||
)({
|
||||
component: SettingsPage,
|
||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: '' })],
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: 'Home',
|
||||
label: "Home",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||
link: linkOptions({ to: '/' }),
|
||||
link: linkOptions({ to: "/" })
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
PermissionConditionOperators,
|
||||
ProjectPermissionSub
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
|
||||
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
||||
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
position?: number;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors }
|
||||
} = useFormContext<TFormSchema>();
|
||||
|
||||
const permissionSubject = ProjectPermissionSub.PkiSubscribers;
|
||||
const items = useFieldArray({
|
||||
control,
|
||||
name: `permissions.${permissionSubject}.${position}.conditions`
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||
<p className="mt-2 text-gray-300">Conditions</p>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||
</p>
|
||||
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||
All conditions must evaluate to true for the policy to take effect.
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col space-y-2">
|
||||
{items.fields.map((el, index) => {
|
||||
const condition =
|
||||
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
|
||||
lhs: string;
|
||||
rhs: string;
|
||||
operator: string;
|
||||
}) || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value="name">Name</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-36 items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => field.onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
|
||||
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
asChild
|
||||
content={getConditionOperatorHelperInfo(
|
||||
condition?.operator as PermissionConditionOperators
|
||||
)}
|
||||
className="max-w-xs"
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0 flex-grow"
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
ariaLabel="plus"
|
||||
variant="outline_bg"
|
||||
className="p-2.5"
|
||||
onClick={() => items.remove(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
|
||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
variant="star"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() =>
|
||||
items.append({
|
||||
lhs: "name",
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
rhs: ""
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+58
-1
@@ -17,6 +17,7 @@ import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
@@ -131,6 +132,15 @@ const SshHostPolicyActionSchema = z.object({
|
||||
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PkiSubscriberPolicyActionSchema = z.object({
|
||||
[ProjectPermissionPkiSubscriberActions.Read]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.Create]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.Edit]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.Delete]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.IssueCert]: z.boolean().optional(),
|
||||
[ProjectPermissionPkiSubscriberActions.ListCerts]: z.boolean().optional()
|
||||
});
|
||||
|
||||
const SecretRollbackPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
@@ -230,6 +240,12 @@ export const projectRoleFormSchema = z.object({
|
||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({
|
||||
inverted: z.boolean().optional(),
|
||||
conditions: ConditionSchema
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||
@@ -271,6 +287,7 @@ type TConditionalFields =
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| ProjectPermissionSub.SecretImports
|
||||
| ProjectPermissionSub.DynamicSecrets
|
||||
| ProjectPermissionSub.PkiSubscribers
|
||||
| ProjectPermissionSub.SshHosts
|
||||
| ProjectPermissionSub.SecretRotation
|
||||
| ProjectPermissionSub.Identity;
|
||||
@@ -284,7 +301,8 @@ export const isConditionalSubjects = (
|
||||
subject === ProjectPermissionSub.SecretFolders ||
|
||||
subject === ProjectPermissionSub.Identity ||
|
||||
subject === ProjectPermissionSub.SshHosts ||
|
||||
subject === ProjectPermissionSub.SecretRotation;
|
||||
subject === ProjectPermissionSub.SecretRotation ||
|
||||
subject === ProjectPermissionSub.PkiSubscribers;
|
||||
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||
@@ -715,6 +733,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
inverted
|
||||
});
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
||||
if (!formVal[subject]) formVal[subject] = [];
|
||||
|
||||
formVal[subject]!.push({
|
||||
[ProjectPermissionPkiSubscriberActions.Edit]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Edit
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.Delete]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Delete
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.Create]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Create
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.Read]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.Read
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert
|
||||
),
|
||||
[ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes(
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts
|
||||
),
|
||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||
inverted
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return formVal;
|
||||
@@ -1104,6 +1149,17 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||
{ label: "Remove", value: "delete" }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiSubscribers]: {
|
||||
title: "PKI Subscribers",
|
||||
actions: [
|
||||
{ label: "Read", value: ProjectPermissionPkiSubscriberActions.Read },
|
||||
{ label: "Create", value: ProjectPermissionPkiSubscriberActions.Create },
|
||||
{ label: "Modify", value: ProjectPermissionPkiSubscriberActions.Edit },
|
||||
{ label: "Remove", value: ProjectPermissionPkiSubscriberActions.Delete },
|
||||
{ label: "Issue Certificate", value: ProjectPermissionPkiSubscriberActions.IssueCert },
|
||||
{ label: "List Certificates", value: ProjectPermissionPkiSubscriberActions.ListCerts }
|
||||
]
|
||||
},
|
||||
[ProjectPermissionSub.PkiCollections]: {
|
||||
title: "PKI Collections",
|
||||
actions: [
|
||||
@@ -1233,6 +1289,7 @@ const KmsPermissionSubjects = (enabled = false) => ({
|
||||
const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
||||
[ProjectPermissionSub.PkiCollections]: enabled,
|
||||
[ProjectPermissionSub.PkiAlerts]: enabled,
|
||||
[ProjectPermissionSub.PkiSubscribers]: enabled,
|
||||
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
||||
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
||||
[ProjectPermissionSub.Certificates]: enabled
|
||||
|
||||
@@ -23,6 +23,7 @@ import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
isConditionalSubjects,
|
||||
@@ -59,6 +60,10 @@ export const renderConditionalComponents = (
|
||||
return <SshHostPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
||||
return <PkiSubscriberPermissionConditions isDisabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { ProjectPermissionSshHostActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api";
|
||||
import { LoginMappingSource } from "@app/hooks/api/sshHost/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
@@ -221,7 +221,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
Download User CA Public Key
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
I={ProjectPermissionSshHostActions.Edit}
|
||||
a={ProjectPermissionSub.SshHosts}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
@@ -243,7 +243,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
I={ProjectPermissionSshHostActions.Delete}
|
||||
a={ProjectPermissionSub.SshHosts}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
|
||||
Reference in New Issue
Block a user