PKI refine access control

This commit is contained in:
Carlos Monastyrski
2025-12-03 00:05:18 -03:00
parent 6d9a259a8a
commit 71a76bc941
44 changed files with 1459 additions and 676 deletions
@@ -4,6 +4,7 @@ export {
ProjectPermissionActions,
ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -12,7 +12,9 @@ export enum ProjectPermissionCertificateActions {
Create = "create",
Edit = "edit",
Delete = "delete",
ReadPrivateKey = "read-private-key"
List = "list",
ReadPrivateKey = "read-private-key",
Import = "import"
}
export enum ProjectPermissionSecretActions {
@@ -67,6 +69,7 @@ export enum ProjectPermissionPkiSyncActions {
Create = "create",
Edit = "edit",
Delete = "delete",
List = "list",
SyncCertificates = "sync-certificates",
ImportCertificates = "import-certificates",
RemoveCertificates = "remove-certificates"
@@ -128,13 +131,25 @@ export enum ProjectPermissionPkiTemplateActions {
ListCerts = "list-certs"
}
export enum ProjectPermissionCertificateProfileActions {
export enum ProjectPermissionCertificateAuthorityActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
List = "list",
Renew = "renew",
SignIntermediate = "sign-intermediate"
}
export enum ProjectPermissionCertificateProfileActions {
Read = "read",
List = "list",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert",
RevealAcmeEabSecret = "reveal-acme-eab-secret"
RevealAcmeEabSecret = "reveal-acme-eab-secret",
RotateAcmeEabSecret = "rotate-acme-eab-secret"
}
export enum ProjectPermissionSecretRotationActions {
@@ -230,6 +245,9 @@ export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SshHosts
| ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.CertificateAuthorities
| ProjectPermissionSub.Certificates
| ProjectPermissionSub.CertificateProfiles
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation
@@ -361,7 +379,24 @@ export type SecretSyncSubjectFields = {
};
export type PkiSyncSubjectFields = {
subscriberId: string;
subscriberName: string;
name: string;
};
export type CertificateAuthoritySubjectFields = {
name: string;
};
export type CertificateSubjectFields = {
commonName?: string;
altNames?: string;
serialNumber?: string;
friendlyName?: string;
status?: string;
};
export type CertificateProfileSubjectFields = {
slug: string;
};
export type SecretRotationSubjectFields = {
@@ -457,8 +492,21 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionCertificateActions, ProjectPermissionSub.Certificates]
| [
ProjectPermissionCertificateAuthorityActions,
(
| ProjectPermissionSub.CertificateAuthorities
| (ForcedSubject<ProjectPermissionSub.CertificateAuthorities> &
CertificateAuthoritySubjectFields)
)
]
| [
ProjectPermissionCertificateActions,
(
| ProjectPermissionSub.Certificates
| (ForcedSubject<ProjectPermissionSub.Certificates> & CertificateSubjectFields)
)
]
| [
ProjectPermissionPkiTemplateActions,
(
@@ -484,7 +532,14 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
)
]
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles]
| [
ProjectPermissionCertificateProfileActions,
(
| ProjectPermissionSub.CertificateProfiles
| (ForcedSubject<ProjectPermissionSub.CertificateProfiles> &
CertificateProfileSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
+1
View File
@@ -15,6 +15,7 @@ export {
ProjectPermissionActions,
ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
@@ -72,7 +72,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.SIGN_INTERMEDIATE]: "Sign intermediate",
[EventType.IMPORT_CA_CERT]: "Import CA certificate",
[EventType.GET_CA_CRL]: "Get CA CRL",
[EventType.ISSUE_CERT]: "Issue certificate",
[EventType.ISSUE_CERT]: "Request certificate",
[EventType.IMPORT_CERT]: "Import certificate",
[EventType.GET_CERT]: "Get certificate",
[EventType.DELETE_CERT]: "Delete certificate",
@@ -225,7 +225,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_PKI_SUBSCRIBER]: "Update PKI subscriber",
[EventType.DELETE_PKI_SUBSCRIBER]: "Delete PKI subscriber",
[EventType.GET_PKI_SUBSCRIBER]: "Get PKI subscriber",
[EventType.ISSUE_PKI_SUBSCRIBER_CERT]: "Issue PKI subscriber certificate",
[EventType.ISSUE_PKI_SUBSCRIBER_CERT]: "Request PKI subscriber certificate",
[EventType.SIGN_PKI_SUBSCRIBER_CERT]: "Sign PKI subscriber certificate",
[EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT]: "Automated renew PKI subscriber certificate",
[EventType.LIST_PKI_SUBSCRIBER_CERTS]: "List PKI subscriber certificates",
@@ -287,7 +287,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_CERTIFICATE_PROFILE]: "Delete Certificate Profile",
[EventType.GET_CERTIFICATE_PROFILE]: "Get Certificate Profile",
[EventType.LIST_CERTIFICATE_PROFILES]: "List Certificate Profiles",
[EventType.ISSUE_CERTIFICATE_FROM_PROFILE]: "Issue Certificate From Profile",
[EventType.ISSUE_CERTIFICATE_FROM_PROFILE]: "Request Certificate From Profile",
[EventType.SIGN_CERTIFICATE_FROM_PROFILE]: "Sign Certificate From Profile",
[EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile",
[EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]:
@@ -1,4 +1,5 @@
import { Helmet } from "react-helmet";
import { subject } from "@casl/ability";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
@@ -7,6 +8,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
AccessRestrictedBanner,
Button,
DeleteActionModal,
DropdownMenu,
@@ -18,7 +20,7 @@ import {
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub,
useOrganization,
useProject
@@ -88,63 +90,82 @@ const Page = () => {
return (
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities"
params={{
orgId: currentOrg.id,
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Certificate Authorities
</Link>
<PageHeader
scope={ProjectType.CertificateManager}
description="Manage certificate authority"
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={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
<ProjectPermissionCan
I={ProjectPermissionCertificateAuthorityActions.Read}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: data.id,
name: data.name
})}
>
{(isAllowed) =>
isAllowed ? (
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities"
params={{
orgId: currentOrg.id,
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:bg-red-500! hover:text-white!"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => handlePopUpOpen("deleteCa")}
disabled={!isAllowed}
>
Delete CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<CaDetailsSection caId={data.id} handlePopUpOpen={handlePopUpOpen} />
</div>
<div className="w-full">
<CaCertificatesSection caId={data.id} />
<CaCrlsSection caId={data.id} />
</div>
</div>
</div>
<FontAwesomeIcon icon={faChevronLeft} />
Certificate Authorities
</Link>
<PageHeader
scope={ProjectType.CertificateManager}
description="Manage certificate authority"
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={ProjectPermissionCertificateAuthorityActions.Delete}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: data.id,
name: data.name
})}
>
{(canDelete) => (
<DropdownMenuItem
className={twMerge(
canDelete
? "hover:bg-red-500! hover:text-white!"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => handlePopUpOpen("deleteCa")}
disabled={!canDelete}
>
Delete CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</PageHeader>
<div className="flex">
<div className="mr-4 w-96">
<CaDetailsSection caId={data.id} handlePopUpOpen={handlePopUpOpen} />
</div>
<div className="w-full">
<CaCertificatesSection caId={data.id} caName={data.name} />
<CaCrlsSection caId={data.id} />
</div>
</div>
</div>
) : (
<div className="container mx-auto flex h-full items-center justify-center">
<AccessRestrictedBanner />
</div>
)
}
</ProjectPermissionCan>
)}
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -2,9 +2,10 @@ import { CaCertificatesTable } from "./CaCertificatesTable";
type Props = {
caId: string;
caName: string;
};
export const CaCertificatesSection = ({ caId }: Props) => {
export const CaCertificatesSection = ({ caId, caName }: Props) => {
return (
<div className="w-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">
@@ -21,7 +22,7 @@ export const CaCertificatesSection = ({ caId }: Props) => {
</IconButton> */}
</div>
<div className="py-4">
<CaCertificatesTable caId={caId} />
<CaCertificatesTable caId={caId} caName={caName} />
</div>
</div>
);
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faCertificate, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as x509 from "@peculiar/x509";
@@ -22,14 +23,15 @@ import {
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/context";
import { useGetCaCerts } from "@app/hooks/api";
type Props = {
caId: string;
caName: string;
};
export const CaCertificatesTable = ({ caId }: Props) => {
export const CaCertificatesTable = ({ caId, caName }: Props) => {
const { data: caCerts, isPending } = useGetCaCerts(caId);
const downloadTxtFile = (filename: string, content: string) => {
@@ -77,8 +79,10 @@ export const CaCertificatesTable = ({ caId }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity}
I={ProjectPermissionCertificateAuthorityActions.Read}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: caName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -96,8 +100,10 @@ export const CaCertificatesTable = ({ caId }: Props) => {
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Identity}
I={ProjectPermissionCertificateAuthorityActions.Read}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
name: caName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -1,10 +1,11 @@
import { subject } from "@casl/ability";
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectPermissionCertificateAuthorityActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
@@ -39,7 +40,10 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<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-medium text-mineshaft-100">CA Details</h3>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Identity}>
<ProjectPermissionCan
I={ProjectPermissionCertificateAuthorityActions.Edit}
a={subject(ProjectPermissionSub.CertificateAuthorities, { caId: ca.id, name: ca.name })}
>
{(isAllowed) => {
return (
<Tooltip content="Edit CA">
@@ -154,8 +158,8 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
</div>
{ca.status === CaStatus.ACTIVE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Renew}
a={subject(ProjectPermissionSub.CertificateAuthorities, { caId: ca.id, name: ca.name })}
>
{(isAllowed) => {
return (
@@ -190,8 +194,8 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
)}
{ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Create}
a={subject(ProjectPermissionSub.CertificateAuthorities, { caId: ca.id, name: ca.name })}
>
{(isAllowed) => {
return (
@@ -4,7 +4,11 @@ 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 { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import {
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub,
useProject
} from "@app/context";
import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -57,7 +61,7 @@ export const CaSection = () => {
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">Internal Certificate Authorities</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
I={ProjectPermissionCertificateAuthorityActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
@@ -23,10 +24,11 @@ import {
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import {
ProjectPermissionActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub,
useOrganization,
useProject
useProject,
useProjectPermission
} from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import {
@@ -53,6 +55,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const { currentProject } = useProject();
const { permission } = useProjectPermission();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentProject.id);
const cas = data as TInternalCertificateAuthority[];
@@ -75,11 +78,24 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
cas &&
cas.length > 0 &&
cas.map((ca) => {
const canReadCa = permission.can(
ProjectPermissionCertificateAuthorityActions.Read,
subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})
);
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
className={twMerge(
"h-10 transition-colors duration-100",
canReadCa && "cursor-pointer hover:bg-mineshaft-700",
!canReadCa && "cursor-not-allowed opacity-60"
)}
key={`ca-${ca.id}`}
onClick={() =>
canReadCa &&
navigate({
to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId",
params: {
@@ -118,8 +134,11 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
<DropdownMenuContent align="start" className="p-1">
{ca.status === CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Create}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -143,8 +162,11 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)}
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Read}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -168,8 +190,11 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)}
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Edit}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -196,8 +221,11 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Delete}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -4,7 +4,11 @@ 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 { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import {
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub,
useProject
} from "@app/context";
import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -61,7 +65,7 @@ export const ExternalCaSection = () => {
<div className="mb-4 flex justify-between">
<p className="text-xl font-medium text-mineshaft-100">External Certificate Authorities</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
I={ProjectPermissionCertificateAuthorityActions.Create}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import {
faBan,
faCertificate,
@@ -26,7 +27,12 @@ import {
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import {
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionSub,
useProject,
useProjectPermission
} from "@app/context";
import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -45,6 +51,7 @@ type Props = {
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
const { currentProject } = useProject();
const { permission } = useProjectPermission();
const { data, isPending } = useListExternalCasByProjectId(currentProject.id);
return (
@@ -65,17 +72,30 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
data &&
data.length > 0 &&
data.map((ca) => {
const canReadCa = permission.can(
ProjectPermissionCertificateAuthorityActions.Read,
subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})
);
return (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
className={twMerge(
"h-10 transition-colors duration-100",
canReadCa && "cursor-pointer hover:bg-mineshaft-700",
!canReadCa && "cursor-not-allowed opacity-60"
)}
key={`ca-${ca.id}`}
onClick={() => {
onClick={() =>
canReadCa &&
handlePopUpOpen("ca", {
caId: ca.id,
name: ca.name,
type: ca.type
});
}}
})
}
>
<Td>{ca.name}</Td>
<Td>{ca.type}</Td>
@@ -95,8 +115,11 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Edit}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -120,8 +143,11 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan>
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Edit}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -149,8 +175,11 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.CertificateAuthorities}
I={ProjectPermissionCertificateAuthorityActions.Delete}
a={subject(ProjectPermissionSub.CertificateAuthorities, {
caId: ca.id,
name: ca.name
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -321,13 +321,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const getModalTitle = () => {
if (certificateDetails) return "Certificate Created Successfully";
if (cert) return "Certificate Details";
return "Issue New Certificate";
return "Request New Certificate";
};
const getModalSubTitle = () => {
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
if (cert) return "View certificate information";
return "Issue a new certificate using a certificate profile";
return "Request a new certificate using a certificate profile";
};
return (
@@ -498,7 +498,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
isLoading={isSubmitting}
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
>
{cert ? "Update" : "Issue Certificate"}
{cert ? "Update" : "Request Certificate"}
</Button>
<Button
colorSchema="secondary"
@@ -241,7 +241,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle, preselectedTemplate
setCertificateDetails(null);
}}
>
<ModalContent title={`${cert ? "View" : "Issue"} Certificate`}>
<ModalContent title={`${cert ? "View" : "Request"} Certificate`}>
{!certificateDetails ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCertificate,
@@ -268,7 +269,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -291,7 +297,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{isLegacyTemplatesEnabled && (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Read}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -327,7 +338,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => {
return (
@@ -391,7 +407,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -428,7 +449,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -456,7 +482,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
!certificate.renewedByCertificateId && (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Edit}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -492,7 +523,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
return (
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -516,7 +552,12 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
})()}
<ProjectPermissionCan
I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates}
a={subject(ProjectPermissionSub.Certificates, {
commonName: certificate.commonName,
altNames: certificate.altNames,
serialNumber: certificate.serialNumber,
friendlyName: certificate.friendlyName
})}
>
{(isAllowed) => (
<DropdownMenuItem
@@ -117,263 +117,287 @@ export const PkiSyncRow = ({
const destinationDetails = PKI_SYNC_MAP[destination];
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
subscriberId: subscriberId || "",
subscriberName: destinationName,
name
});
return (
<Tr
onClick={() => {
// console.log("PKI Sync navigation:", { syncId: id, projectId });
navigate({
to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path,
params: {
syncId: id,
projectId,
orgId: currentOrg.id
}
});
}}
className={twMerge(
"group h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700",
syncStatus === PkiSyncStatus.Failed && "bg-red/5 hover:bg-red/10"
)}
key={`sync-${id}`}
>
<Td>
<img
alt={`${destinationDetails.name} sync`}
src={`/images/integrations/${destinationDetails.image}`}
className="min-w-7"
/>
</Td>
<Td className="max-w-0 min-w-32!">
<div>
<div className="flex w-full items-center">
<p className="truncate">{name}</p>
{description && (
<Tooltip content={description}>
<FontAwesomeIcon
icon={faInfoCircle}
size="xs"
className="ml-1 text-mineshaft-400"
/>
</Tooltip>
)}
</div>
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
</div>
</Td>
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
{syncStatus && (
<Tooltip
position="left"
className="max-w-sm"
content={
[PkiSyncStatus.Succeeded, PkiSyncStatus.Failed].includes(syncStatus) ? (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{lastSyncedAt && (
<div>
<div
className={`mb-2 flex self-start ${syncStatus === PkiSyncStatus.Failed ? "text-yellow" : "text-green"}`}
>
<FontAwesomeIcon
icon={faCalendarCheck}
className="ml-1 pt-0.5 pr-1.5 text-sm"
/>
<div className="text-xs">Last Synced</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</div>
</div>
)}
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon icon={faXmark} className="ml-1 pt-0.5 pr-1.5 text-sm" />
<div className="text-xs">Failure Reason</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
{failureMessage}
</div>
</div>
)}
</div>
) : undefined
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Read} a={permissionSubject}>
{(isAllowed: boolean) => {
return (
<Tr
onClick={() => {
if (!isAllowed) {
return;
}
>
// console.log("PKI Sync navigation:", { syncId: id, projectId });
navigate({
to: ROUTE_PATHS.CertManager.PkiSyncDetailsByIDPage.path,
params: {
syncId: id,
projectId,
orgId: currentOrg.id
}
});
}}
className={twMerge(
"group h-10 transition-colors duration-100 hover:bg-mineshaft-700",
syncStatus === PkiSyncStatus.Failed && "bg-red/5 hover:bg-red/10",
isAllowed ? "cursor-pointer" : "cursor-not-allowed"
)}
key={`sync-${id}`}
>
<Td>
<img
alt={`${destinationDetails.name} sync`}
src={`/images/integrations/${destinationDetails.image}`}
className="min-w-7"
/>
</Td>
<Td className="max-w-0 min-w-32!">
<div>
<PkiSyncStatusBadge status={syncStatus} />
</div>
</Tooltip>
)}
{!isAutoSyncEnabled && (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
>
<Badge variant="neutral">
<BanIcon />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</Tooltip>
)}
{syncOption?.canImportCertificates && <PkiSyncImportStatusBadge mini pkiSync={pkiSync} />}
<PkiSyncRemoveStatusBadge mini pkiSync={pkiSync} />
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
handleCopyId();
}}
>
Copy Sync ID
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.SyncCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faRotate} />}
onClick={(e) => {
e.stopPropagation();
onTriggerSyncCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Manually trigger a sync for this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span> Trigger Sync</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
<div className="flex w-full items-center">
<p className="truncate">{name}</p>
{description && (
<Tooltip content={description}>
<FontAwesomeIcon
icon={faInfoCircle}
size="xs"
className="ml-1 text-mineshaft-400"
/>
</Tooltip>
</DropdownMenuItem>
)}
</div>
<p className="truncate text-xs leading-4 text-bunker-300">
{destinationDetails.name}
</p>
</div>
</Td>
<PkiSyncDestinationCol pkiSync={pkiSync} />
<Td>
<div className="flex items-center gap-1">
{syncStatus && (
<Tooltip
position="left"
className="max-w-sm"
content={
[PkiSyncStatus.Succeeded, PkiSyncStatus.Failed].includes(syncStatus) ? (
<div className="flex flex-col gap-2 py-1 whitespace-normal">
{lastSyncedAt && (
<div>
<div
className={`mb-2 flex self-start ${syncStatus === PkiSyncStatus.Failed ? "text-yellow" : "text-green"}`}
>
<FontAwesomeIcon
icon={faCalendarCheck}
className="ml-1 pt-0.5 pr-1.5 text-sm"
/>
<div className="text-xs">Last Synced</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
</div>
</div>
)}
{failureMessage && (
<div>
<div className="mb-2 flex self-start text-red">
<FontAwesomeIcon
icon={faXmark}
className="ml-1 pt-0.5 pr-1.5 text-sm"
/>
<div className="text-xs">Failure Reason</div>
</div>
<div className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
{failureMessage}
</div>
</div>
)}
</div>
) : undefined
}
>
<div>
<PkiSyncStatusBadge status={syncStatus} />
</div>
</Tooltip>
)}
</ProjectPermissionCan>
{syncOption?.canImportCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
{!isAutoSyncEnabled && (
<Tooltip
className="text-xs"
content="Auto-Sync is disabled. Certificate changes in the PKI subscriber will not be automatically synced to the destination."
>
<Badge variant="neutral">
<BanIcon />
{!syncStatus && "Auto-Sync Disabled"}
</Badge>
</Tooltip>
)}
{syncOption?.canImportCertificates && (
<PkiSyncImportStatusBadge mini pkiSync={pkiSync} />
)}
<PkiSyncRemoveStatusBadge mini pkiSync={pkiSync} />
</div>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={(e) => {
e.stopPropagation();
onTriggerImportCertificates(pkiSync);
handleCopyId();
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
Copy Sync ID
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={(e) => {
e.stopPropagation();
onTriggerRemoveCertificates(pkiSync);
}}
isDisabled={!isAllowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.SyncCertificates}
a={permissionSubject}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={isAutoSyncEnabled ? faToggleOff : faToggleOn} />}
onClick={(e) => {
e.stopPropagation();
onToggleEnable(pkiSync);
}}
>
{isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Delete}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
onDelete(pkiSync);
}}
>
Delete Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
{(allowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faRotate} />}
onClick={(e) => {
e.stopPropagation();
onTriggerSyncCertificates(pkiSync);
}}
isDisabled={!allowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Manually trigger a sync for this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span> Trigger Sync</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{syncOption?.canImportCertificates && (
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.ImportCertificates}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faDownload} />}
onClick={(e) => {
e.stopPropagation();
onTriggerImportCertificates(pkiSync);
}}
isDisabled={!allowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Import certificates from this ${destinationName} destination into Infisical.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Import Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.RemoveCertificates}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEraser} />}
onClick={(e) => {
e.stopPropagation();
onTriggerRemoveCertificates(pkiSync);
}}
isDisabled={!allowed}
>
<Tooltip
position="left"
sideOffset={42}
content={`Remove certificates synced by Infisical from this ${destinationName} destination.`}
>
<div className="flex h-full w-full items-center justify-between gap-1">
<span>Remove Certificates</span>
<FontAwesomeIcon
className="text-bunker-300"
size="sm"
icon={faInfoCircle}
/>
</div>
</Tooltip>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Edit}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
isDisabled={!allowed}
icon={
<FontAwesomeIcon icon={isAutoSyncEnabled ? faToggleOff : faToggleOn} />
}
onClick={(e) => {
e.stopPropagation();
onToggleEnable(pkiSync);
}}
>
{isAutoSyncEnabled ? "Disable" : "Enable"} Auto-Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiSyncActions.Delete}
a={permissionSubject}
>
{(allowed: boolean) => (
<DropdownMenuItem
isDisabled={!allowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={(e) => {
e.stopPropagation();
onDelete(pkiSync);
}}
>
Delete Sync
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
}}
</ProjectPermissionCan>
);
};
@@ -112,7 +112,9 @@ export const PkiSyncActionTriggers = ({ pkiSync }: Props) => {
}, [updatePkiSyncMutation, id, projectId, pkiSync.isAutoSyncEnabled]);
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
subscriberId: subscriberId || "",
subscriberName: destinationName,
name: pkiSync.name
});
return (
@@ -32,6 +32,7 @@ import {
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api";
import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
@@ -84,8 +85,12 @@ export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
const totalCount = data?.totalCount || 0;
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || ""
subscriberId: pkiSync.subscriberId || "",
subscriberName: destinationName,
name: pkiSync.name
});
const handleRemoveCertificate = async (certificateId: string) => {
@@ -55,7 +55,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
}
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
subscriberId: subscriberId || "",
subscriberName: destinationDetails.name,
name: pkiSync.name
});
return (
@@ -10,6 +10,7 @@ import { PkiSyncStatusBadge } from "@app/components/pki-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({
@@ -50,8 +51,12 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
return null;
}, [syncStatus, lastSyncMessage]);
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriber?.id || subscriberId || ""
subscriberId: subscriber?.id || subscriberId || "",
subscriberName: destinationName,
name: pkiSync.name
});
return (
@@ -7,6 +7,7 @@ import { IconButton } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({
@@ -35,9 +36,12 @@ export const PkiSyncFieldMappingsSection = ({ pkiSync, onEditMappings }: Props)
}
const fieldMappings = pkiSync.syncOptions?.fieldMappings;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || ""
subscriberId: pkiSync.subscriberId || "",
subscriberName: destinationName,
name: pkiSync.name
});
return (
@@ -7,6 +7,7 @@ import { IconButton } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({
@@ -34,8 +35,12 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
syncOptions: { canRemoveCertificates }
} = pkiSync;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: pkiSync.subscriberId || ""
subscriberId: pkiSync.subscriberId || "",
subscriberName: destinationName,
name: pkiSync.name
});
return (
@@ -9,6 +9,7 @@ import { IconButton, Tooltip } from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
@@ -26,8 +27,12 @@ type Props = {
export const PkiSyncSourceSection = ({ pkiSync, onEditSource }: Props) => {
const { subscriberId, subscriber } = pkiSync;
const destinationName = PKI_SYNC_MAP[pkiSync.destination].name;
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
subscriberId: subscriberId || ""
subscriberId: subscriberId || "",
subscriberName: destinationName,
name: pkiSync.name
});
return (
@@ -4,10 +4,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks";
@@ -21,8 +21,6 @@ import { ProfileList } from "./ProfileList";
import { RevealAcmeEabSecretModal } from "./RevealAcmeEabSecretModal";
export const CertificateProfilesTab = () => {
const { permission } = useProjectPermission();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -35,11 +33,6 @@ export const CertificateProfilesTab = () => {
const deleteProfile = useDeleteCertificateProfile();
const canCreateProfile = permission.can(
ProjectPermissionActions.Create,
ProjectPermissionSub.CertificateAuthorities
);
const handleCreateProfile = () => {
setIsCreateModalOpen(true);
};
@@ -84,16 +77,22 @@ export const CertificateProfilesTab = () => {
</p>
</div>
{canCreateProfile && (
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateProfile}
>
Create Profile
</Button>
)}
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.Create}
a={ProjectPermissionSub.CertificateProfiles}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateProfile}
>
Create Profile
</Button>
)}
</ProjectPermissionCan>
</div>
<ProfileList
@@ -1,4 +1,5 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faCheck,
faCircleInfo,
@@ -12,6 +13,7 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
@@ -22,9 +24,7 @@ import {
Tr
} from "@app/components/v2";
import { Badge } from "@app/components/v3";
import { useProjectPermission } from "@app/context";
import {
ProjectPermissionActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
@@ -47,8 +47,6 @@ export const ProfileRow = ({
onRevealProfileAcmeEabSecret,
onDeleteProfile
}: Props) => {
const { permission } = useProjectPermission();
const { data: caData } = useGetInternalCaById(profile.caId ?? "");
const { popUp, handlePopUpToggle } = usePopUp(["issueCertificate"] as const);
@@ -71,26 +69,6 @@ export const ProfileRow = ({
templateId: profile.certificateTemplateId
});
const canEditProfile = permission.can(
ProjectPermissionActions.Edit,
ProjectPermissionSub.CertificateAuthorities
);
const canRevealProfileAcmeEabSecret = permission.can(
ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret,
ProjectPermissionSub.CertificateProfiles
);
const canIssueCertificate = permission.can(
ProjectPermissionCertificateProfileActions.IssueCert,
ProjectPermissionSub.CertificateProfiles
);
const canDeleteProfile = permission.can(
ProjectPermissionActions.Delete,
ProjectPermissionSub.CertificateAuthorities
);
const getEnrollmentTypeBadge = (enrollmentType: string) => {
const config = {
api: { variant: "ghost" as const, label: "API" },
@@ -149,50 +127,82 @@ export const ProfileRow = ({
>
Copy Profile ID
</DropdownMenuItem>
{canEditProfile && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditProfile(profile);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.Edit}
a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditProfile(profile);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
>
Edit Profile
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
{profile.enrollmentType === "acme" && (
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret}
a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
>
Edit Profile
</DropdownMenuItem>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onRevealProfileAcmeEabSecret(profile);
}}
icon={<FontAwesomeIcon icon={faEye} className="w-3" />}
>
Reveal ACME EAB
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
)}
{canRevealProfileAcmeEabSecret && profile.enrollmentType === "acme" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onRevealProfileAcmeEabSecret(profile);
}}
icon={<FontAwesomeIcon icon={faEye} className="w-3" />}
{profile.enrollmentType === "api" && (
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.IssueCert}
a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
>
Reveal ACME EAB
</DropdownMenuItem>
)}
{canIssueCertificate && profile.enrollmentType === "api" && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("issueCertificate");
}}
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
>
Issue Certificate
</DropdownMenuItem>
)}
{canDeleteProfile && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteProfile(profile);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
>
Delete Profile
</DropdownMenuItem>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpToggle("issueCertificate");
}}
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
>
Request Certificate
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
)}
<ProjectPermissionCan
I={ProjectPermissionCertificateProfileActions.Delete}
a={subject(ProjectPermissionSub.CertificateProfiles, { slug: profile.slug })}
>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteProfile(profile);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
>
Delete Profile
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
<CertificateIssuanceModal
@@ -3,8 +3,8 @@ import { 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 { useProjectPermission } from "@app/context";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
@@ -16,8 +16,6 @@ import { CreateTemplateModal } from "./CreateTemplateModal";
import { TemplateList } from "./TemplateList";
export const CertificateTemplatesV2Tab = () => {
const { permission } = useProjectPermission();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -26,11 +24,6 @@ export const CertificateTemplatesV2Tab = () => {
const deleteTemplateV2 = useDeleteCertificateTemplateV2WithPolicies();
const canCreateTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionSub.CertificateTemplates
);
const handleCreateTemplate = () => {
setIsCreateModalOpen(true);
};
@@ -70,16 +63,22 @@ export const CertificateTemplatesV2Tab = () => {
</p>
</div>
{canCreateTemplate && (
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateTemplate}
>
Create Template
</Button>
)}
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Create}
a={ProjectPermissionSub.CertificateTemplates}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateTemplate}
>
Create Template
</Button>
)}
</ProjectPermissionCan>
</div>
<TemplateList onEditTemplate={handleEditTemplate} onDeleteTemplate={handleDeleteTemplate} />
@@ -1,6 +1,17 @@
import { faCircleInfo, faEdit, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faCheck,
faCircleInfo,
faCopy,
faEdit,
faEllipsis,
faTrash
} 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 {
DropdownMenu,
DropdownMenuContent,
@@ -17,11 +28,12 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context";
import { useProject } from "@app/context";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
@@ -31,8 +43,8 @@ interface Props {
}
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
const { permission } = useProjectPermission();
const { currentProject } = useProject();
const [isIdCopied, setIsIdCopied] = useToggle(false);
const { data, isLoading } = useListCertificateTemplatesV2({
projectId: currentProject?.id || "",
@@ -42,20 +54,25 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
const templates = data?.certificateTemplates || [];
const handleCopyId = useCallback(
(templateId: string) => {
setIsIdCopied.on();
navigator.clipboard.writeText(templateId);
createNotification({
text: "Template ID copied to clipboard",
type: "info"
});
setTimeout(() => setIsIdCopied.off(), 2000);
},
[setIsIdCopied]
);
if (!currentProject?.id) {
return null;
}
const canEditTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const canDeleteTemplate = permission.can(
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString();
};
@@ -110,28 +127,55 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
{canEditTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditTemplate(template);
}}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)}
{canDeleteTemplate && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleCopyId(template.id);
}}
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
>
Copy Template ID
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Edit}
a={subject(ProjectPermissionSub.CertificateTemplates, {
name: template.name
})}
>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditTemplate(template);
}}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit Template
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionPkiTemplateActions.Delete}
a={subject(ProjectPermissionSub.CertificateTemplates, {
name: template.name
})}
>
{(isAllowed) =>
isAllowed && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteTemplate(template);
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Template
</DropdownMenuItem>
)
}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
@@ -0,0 +1,19 @@
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { ConditionsFields } from "./ConditionsFields";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const CertificateAuthorityPermissionConditions = ({ position = 0, isDisabled }: Props) => {
return (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.CertificateAuthorities}
position={position}
selectOptions={[{ value: "name", label: "Name" }]}
/>
);
};
@@ -0,0 +1,24 @@
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { ConditionsFields } from "./ConditionsFields";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const CertificatePermissionConditions = ({ position = 0, isDisabled }: Props) => {
return (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.Certificates}
position={position}
selectOptions={[
{ value: "id", label: "Certificate ID" },
{ value: "commonName", label: "Common Name" },
{ value: "altNames", label: "Subject Alternative Names" },
{ value: "serialNumber", label: "Serial Number" }
]}
/>
);
};
@@ -0,0 +1,19 @@
import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { ConditionsFields } from "./ConditionsFields";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const CertificateProfilePermissionConditions = ({ position = 0, isDisabled }: Props) => {
return (
<ConditionsFields
isDisabled={isDisabled}
subject={ProjectPermissionSub.CertificateProfiles}
position={position}
selectOptions={[{ value: "slug", label: "Name" }]}
/>
);
};
@@ -13,7 +13,7 @@ export const PkiSyncPermissionConditions = ({ position = 0, isDisabled }: Props)
isDisabled={isDisabled}
subject={ProjectPermissionSub.PkiSyncs}
position={position}
selectOptions={[{ value: "subscriberName", label: "Subscriber Name" }]}
selectOptions={[{ value: "name", label: "Sync Name" }]}
/>
);
};
@@ -7,6 +7,7 @@ import { Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
@@ -56,7 +57,19 @@ const CertificatePolicyActionSchema = z.object({
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Read]: z.boolean().optional(),
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional()
[ProjectPermissionCertificateActions.List]: z.boolean().optional(),
[ProjectPermissionCertificateActions.ReadPrivateKey]: z.boolean().optional(),
[ProjectPermissionCertificateActions.Import]: z.boolean().optional()
});
const CertificateAuthorityPolicyActionSchema = z.object({
[ProjectPermissionCertificateAuthorityActions.Create]: z.boolean().optional(),
[ProjectPermissionCertificateAuthorityActions.Delete]: z.boolean().optional(),
[ProjectPermissionCertificateAuthorityActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateAuthorityActions.Read]: z.boolean().optional(),
[ProjectPermissionCertificateAuthorityActions.List]: z.boolean().optional(),
[ProjectPermissionCertificateAuthorityActions.Renew]: z.boolean().optional(),
[ProjectPermissionCertificateAuthorityActions.SignIntermediate]: z.boolean().optional()
});
const SecretPolicyActionSchema = z.object({
@@ -110,6 +123,7 @@ const PkiSyncPolicyActionSchema = z.object({
[ProjectPermissionPkiSyncActions.Create]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.Edit]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.Delete]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.List]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.SyncCertificates]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.ImportCertificates]: z.boolean().optional(),
[ProjectPermissionPkiSyncActions.RemoveCertificates]: z.boolean().optional()
@@ -208,7 +222,6 @@ const PkiSubscriberPolicyActionSchema = z.object({
[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()
});
@@ -217,15 +230,17 @@ const PkiTemplatePolicyActionSchema = z.object({
[ProjectPermissionPkiTemplateActions.Create]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Edit]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.Delete]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
});
const CertificateProfilePolicyActionSchema = z.object({
[ProjectPermissionCertificateProfileActions.Read]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.List]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.Delete]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.IssueCert]: z.boolean().optional()
[ProjectPermissionCertificateProfileActions.IssueCert]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret]: z.boolean().optional(),
[ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret]: z.boolean().optional()
});
const SecretEventsPolicyActionSchema = z.object({
@@ -381,8 +396,18 @@ export const projectRoleFormSchema = z.object({
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.AuditLogs]: AuditLogsPolicyActionSchema.array().default([]),
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateAuthorities]: CertificateAuthorityPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.PkiSubscribers]: PkiSubscriberPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
@@ -397,8 +422,12 @@ export const projectRoleFormSchema = z.object({
})
.array()
.default([]),
[ProjectPermissionSub.CertificateProfiles]:
CertificateProfilePolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
[]
),
@@ -458,6 +487,9 @@ type TConditionalFields =
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.CertificateAuthorities
| ProjectPermissionSub.Certificates
| ProjectPermissionSub.CertificateProfiles
| ProjectPermissionSub.SshHosts
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity
@@ -479,6 +511,9 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.SecretRotation ||
subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.CertificateAuthorities ||
subject === ProjectPermissionSub.Certificates ||
subject === ProjectPermissionSub.CertificateProfiles ||
subject === ProjectPermissionSub.SecretSyncs ||
subject === ProjectPermissionSub.PkiSyncs ||
subject === ProjectPermissionSub.SecretEvents ||
@@ -661,6 +696,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canEdit = action.includes(ProjectPermissionPkiSyncActions.Edit);
const canDelete = action.includes(ProjectPermissionPkiSyncActions.Delete);
const canCreate = action.includes(ProjectPermissionPkiSyncActions.Create);
const canList = action.includes(ProjectPermissionPkiSyncActions.List);
const canSyncCertificates = action.includes(
ProjectPermissionPkiSyncActions.SyncCertificates
);
@@ -679,6 +715,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionPkiSyncActions.Create]: canCreate,
[ProjectPermissionPkiSyncActions.Edit]: canEdit,
[ProjectPermissionPkiSyncActions.Delete]: canDelete,
[ProjectPermissionPkiSyncActions.List]: canList,
[ProjectPermissionPkiSyncActions.SyncCertificates]: canSyncCertificates,
[ProjectPermissionPkiSyncActions.ImportCertificates]: canImportCertificates,
[ProjectPermissionPkiSyncActions.RemoveCertificates]: canRemoveCertificates,
@@ -822,6 +859,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.CertificateAuthorities) {
const canRead = action.includes(ProjectPermissionCertificateAuthorityActions.Read);
const canCreate = action.includes(ProjectPermissionCertificateAuthorityActions.Create);
const canEdit = action.includes(ProjectPermissionCertificateAuthorityActions.Edit);
const canDelete = action.includes(ProjectPermissionCertificateAuthorityActions.Delete);
const canList = action.includes(ProjectPermissionCertificateAuthorityActions.List);
const canRenew = action.includes(ProjectPermissionCertificateAuthorityActions.Renew);
const canSignIntermediate = action.includes(
ProjectPermissionCertificateAuthorityActions.SignIntermediate
);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
[ProjectPermissionCertificateAuthorityActions.Read]: canRead,
[ProjectPermissionCertificateAuthorityActions.Create]: canCreate,
[ProjectPermissionCertificateAuthorityActions.Edit]: canEdit,
[ProjectPermissionCertificateAuthorityActions.Delete]: canDelete,
[ProjectPermissionCertificateAuthorityActions.List]: canList,
[ProjectPermissionCertificateAuthorityActions.Renew]: canRenew,
[ProjectPermissionCertificateAuthorityActions.SignIntermediate]: canSignIntermediate,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
// for other subjects
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
@@ -873,17 +937,24 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canEdit = action.includes(ProjectPermissionCertificateActions.Edit);
const canDelete = action.includes(ProjectPermissionCertificateActions.Delete);
const canCreate = action.includes(ProjectPermissionCertificateActions.Create);
const canList = action.includes(ProjectPermissionCertificateActions.List);
const canImport = action.includes(ProjectPermissionCertificateActions.Import);
const canReadPrivateKey = action.includes(ProjectPermissionCertificateActions.ReadPrivateKey);
if (!formVal[subject]) formVal[subject] = [{}];
if (!formVal[subject]) formVal[subject] = [];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0].read = true;
if (canEdit) formVal[subject]![0].edit = true;
if (canCreate) formVal[subject]![0].create = true;
if (canDelete) formVal[subject]![0].delete = true;
if (canReadPrivateKey)
formVal[subject]![0][ProjectPermissionCertificateActions.ReadPrivateKey] = true;
formVal[subject]!.push({
[ProjectPermissionCertificateActions.Read]: canRead,
[ProjectPermissionCertificateActions.Edit]: canEdit,
[ProjectPermissionCertificateActions.Create]: canCreate,
[ProjectPermissionCertificateActions.Delete]: canDelete,
[ProjectPermissionCertificateActions.List]: canList,
[ProjectPermissionCertificateActions.ReadPrivateKey]: canReadPrivateKey,
[ProjectPermissionCertificateActions.Import]: canImport,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
@@ -1129,9 +1200,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionPkiSubscriberActions.Read]: action.includes(
ProjectPermissionPkiSubscriberActions.Read
),
[ProjectPermissionPkiSubscriberActions.IssueCert]: action.includes(
ProjectPermissionPkiSubscriberActions.IssueCert
),
[ProjectPermissionPkiSubscriberActions.ListCerts]: action.includes(
ProjectPermissionPkiSubscriberActions.ListCerts
),
@@ -1157,9 +1225,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionPkiTemplateActions.Read]: action.includes(
ProjectPermissionPkiTemplateActions.Read
),
[ProjectPermissionPkiTemplateActions.IssueCert]: action.includes(
ProjectPermissionPkiTemplateActions.IssueCert
),
[ProjectPermissionPkiTemplateActions.ListCerts]: action.includes(
ProjectPermissionPkiTemplateActions.ListCerts
),
@@ -1185,9 +1250,20 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionCertificateProfileActions.Read]: action.includes(
ProjectPermissionCertificateProfileActions.Read
),
[ProjectPermissionCertificateProfileActions.List]: action.includes(
ProjectPermissionCertificateProfileActions.List
),
[ProjectPermissionCertificateProfileActions.IssueCert]: action.includes(
ProjectPermissionCertificateProfileActions.IssueCert
)
),
[ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret]: action.includes(
ProjectPermissionCertificateProfileActions.RevealAcmeEabSecret
),
[ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret]: action.includes(
ProjectPermissionCertificateProfileActions.RotateAcmeEabSecret
),
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
@@ -1196,7 +1272,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (subject === ProjectPermissionSub.PamAccounts) {
if (!formVal[subject]) formVal[subject] = [];
formVal[subject].push({
formVal[subject]!.push({
[ProjectPermissionPamAccountActions.Access]: action.includes(
ProjectPermissionPamAccountActions.Access
),
@@ -1552,18 +1628,26 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.CertificateAuthorities]: {
title: "Certificate Authorities",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
{ label: "Read", value: ProjectPermissionCertificateAuthorityActions.Read },
{ label: "List", value: ProjectPermissionCertificateAuthorityActions.List },
{ label: "Create", value: ProjectPermissionCertificateAuthorityActions.Create },
{ label: "Modify", value: ProjectPermissionCertificateAuthorityActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateAuthorityActions.Delete },
{ label: "Renew", value: ProjectPermissionCertificateAuthorityActions.Renew },
{
label: "Sign Intermediate",
value: ProjectPermissionCertificateAuthorityActions.SignIntermediate
}
]
},
[ProjectPermissionSub.Certificates]: {
title: "Certificates",
actions: [
{ label: "Read", value: ProjectPermissionCertificateActions.Read },
{ label: "List", value: ProjectPermissionCertificateActions.List },
{ label: "Read Private Key", value: ProjectPermissionCertificateActions.ReadPrivateKey },
{ label: "Create", value: ProjectPermissionCertificateActions.Create },
// { label: "Create", value: ProjectPermissionCertificateActions.Create }, // Hidden from UI - kept for backend compatibility
{ label: "Import", value: ProjectPermissionCertificateActions.Import },
{ label: "Modify", value: ProjectPermissionCertificateActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateActions.Delete }
]
@@ -1575,7 +1659,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionPkiTemplateActions.Create },
{ label: "Modify", value: ProjectPermissionPkiTemplateActions.Edit },
{ label: "Remove", value: ProjectPermissionPkiTemplateActions.Delete },
{ label: "Issue Certificates", value: ProjectPermissionPkiTemplateActions.IssueCert },
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
]
},
@@ -1583,10 +1666,11 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
title: "Certificate Profiles",
actions: [
{ label: "Read", value: ProjectPermissionCertificateProfileActions.Read },
{ label: "List", value: ProjectPermissionCertificateProfileActions.List },
{ label: "Create", value: ProjectPermissionCertificateProfileActions.Create },
{ label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit },
{ label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete },
{ label: "Issue Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert }
{ label: "Request Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert }
]
},
[ProjectPermissionSub.SshCertificateAuthorities]: {
@@ -1642,7 +1726,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ 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 }
]
},
@@ -1716,6 +1799,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
title: "Certificate Syncs",
actions: [
{ label: "Read", value: ProjectPermissionPkiSyncActions.Read },
{ label: "List", value: ProjectPermissionPkiSyncActions.List },
{ label: "Create", value: ProjectPermissionPkiSyncActions.Create },
{ label: "Modify", value: ProjectPermissionPkiSyncActions.Edit },
{ label: "Remove", value: ProjectPermissionPkiSyncActions.Delete },
@@ -21,6 +21,9 @@ import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { AddPoliciesButton } from "./AddPoliciesButton";
import { AppConnectionPermissionConditions } from "./AppConnectionPermissionConditions";
import { CertificateAuthorityPermissionConditions } from "./CertificateAuthorityPermissionConditions";
import { CertificatePermissionConditions } from "./CertificatePermissionConditions";
import { CertificateProfilePermissionConditions } from "./CertificateProfilePermissionConditions";
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
@@ -96,6 +99,18 @@ export const renderConditionalComponents = (
return <PamAccountPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.CertificateAuthorities) {
return <CertificateAuthorityPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.Certificates) {
return <CertificatePermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.CertificateProfiles) {
return <CertificateProfilePermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}