Revise PR based on review

This commit is contained in:
Tuan Dang
2025-05-12 12:56:56 -07:00
parent 531607dcb7
commit e8519f6612
14 changed files with 100 additions and 51 deletions

View File

@@ -10,8 +10,8 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("caId").notNullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.uuid("caId").nullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL");
t.string("name").notNullable();
t.string("commonName").notNullable();
t.specificType("subjectAlternativeNames", "text[]").notNullable();

View File

@@ -12,7 +12,7 @@ export const PkiSubscribersSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
caId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
name: z.string(),
commonName: z.string(),
subjectAlternativeNames: z.string().array(),

View File

@@ -2,6 +2,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
@@ -13,6 +14,7 @@ import {
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -262,6 +264,7 @@ export const pkiSubscriberServiceFactory = ({
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
@@ -365,7 +368,32 @@ export const pkiSubscriberServiceFactory = ({
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[];
if (subscriber.extendedKeyUsages.length) {
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
true
);
extensions.push(extendedKeyUsagesExtension);
}
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) {
return { type: "email", value: altName };
}
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({
@@ -421,7 +449,7 @@ export const pkiSubscriberServiceFactory = ({
notBefore: notBeforeDate,
notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
},
tx
);
@@ -470,6 +498,8 @@ export const pkiSubscriberServiceFactory = ({
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
@@ -731,13 +761,11 @@ export const pkiSubscriberServiceFactory = ({
projectId
});
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: ca.projectId,
projectId: subscriber.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager

View File

@@ -1087,17 +1087,14 @@ export const projectServiceFactory = ({
const subscribers = await pkiSubscriberDAL.find({ projectId });
for (const subscriber of subscribers) {
try {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiSubscriberActions.Read,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
const canRead = permission.can(
ProjectPermissionPkiSubscriberActions.Read,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriber.name
})
);
if (canRead) {
allowedSubscribers.push(subscriber);
} catch {
// intentionally ignore subscribers where user lacks access
}
}
@@ -1203,17 +1200,15 @@ export const projectServiceFactory = ({
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
for (const host of hosts) {
try {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Read,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
const canRead = permission.can(
ProjectPermissionSshHostActions.Read,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
if (canRead) {
allowedHosts.push(host);
} catch {
// intentionally ignore projects where user lacks access
}
}

View File

@@ -12,10 +12,9 @@ In Infisical PKI, subscribers are logical representations of entities such as de
```mermaid
graph TD
A[Root CA] --> B[Intermediate CA]
B --> C1[Certificate]
A[Issuing CA] --> C1[Certificate]
C1 --> S1[Subscriber]
B --> C2[Certificate]
A --> C2[Certificate]
C2 --> S2[Subscriber]
```
@@ -25,7 +24,7 @@ graph TD
The typical workflow for managing subscribers consists of the following steps:
1. Creating a subscriber and defining attributes to be included on the X.509 certificates issued for it including common name, subject alternative names, TLL, etc.
1. Creating a subscriber and defining which (issuing) CA will issue X.509 certificates for it as well as attributes to be included on the certificates including common name, subject alternative names, TLL, etc.
2. Requesting for a certificate against the subscriber with or without a certificate signing request (CSR).
3. Managing certificate lifecycle events such as certificate renewal and revocation. As part of the certificate revocation flow,
you can also query for a Certificate Revocation List [CRL](https://en.wikipedia.org/wiki/Certificate_revocation_list), a time-stamped, signed

View File

@@ -61,6 +61,9 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project;
};
export const getProjectHomePage = (workspace: Workspace) => {
if (workspace.type === ProjectType.CertificateManager) {
return `/${workspace.type}/$projectId/subscribers` as const;
}
return `/${workspace.type}/$projectId/overview` as const;
};

View File

@@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { pkiSubscriberKeys } from "../pkiSubscriber/queries";
import { workspaceKeys } from "../workspace";
import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
@@ -42,6 +43,9 @@ export const useRevokeCert = () => {
queryClient.invalidateQueries({
queryKey: workspaceKeys.forWorkspaceCertificates(projectSlug)
});
queryClient.invalidateQueries({
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
});
}
});
};

View File

@@ -77,8 +77,8 @@ export const useGetPkiSubscriberCertificates = ({
queryKey: pkiSubscriberKeys.specificPkiSubscriberCertificates({
subscriberName,
projectId,
offset: 0,
limit: 25
offset,
limit
}),
queryFn: async () => {
const params = new URLSearchParams({

View File

@@ -118,7 +118,9 @@ export const ProjectLayout = () => {
)}
</Link>
<Link
to={`/${ProjectType.CertificateManager}/$projectId/overview` as const}
to={
`/${ProjectType.CertificateManager}/$projectId/certificates` as const
}
params={{
projectId: currentWorkspace.id
}}

View File

@@ -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
});

View File

@@ -1,4 +1,5 @@
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";
@@ -26,6 +27,7 @@ import {
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useGetPkiSubscriberCertificates } from "@app/hooks/api";
@@ -42,6 +44,7 @@ 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);
@@ -74,6 +77,13 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
return <Badge variant="success">Valid</Badge>;
};
const canListPkiSubscriberCerts = permission.can(
ProjectPermissionPkiSubscriberActions.ListCerts,
subject(ProjectPermissionSub.PkiSubscribers, {
name: subscriberName
})
);
return (
<div>
<TableContainer>
@@ -156,7 +166,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
)}
{!isPending && !data?.certificates?.length && (
<EmptyState
title="No certificates have been issued for this subscriber"
title={`${canListPkiSubscriberCerts ? "No certificates have been issued for this subscriber" : "You do not have permission to view this subscriber's certificates"}`}
icon={faCertificate}
/>
)}

View File

@@ -14,6 +14,7 @@ import {
} 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";
@@ -44,6 +45,7 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }:
subscriberName,
projectId
});
const { mutateAsync: issuePkiSubscriberCert, isPending: isIssuingCert } =
useIssuePkiSubscriberCert();
@@ -136,6 +138,12 @@ export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }:
<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>

View File

@@ -964,8 +964,8 @@ const certManagerSettingsPageRouteRoute =
const certManagerCertificatesPageRouteRoute =
certManagerCertificatesPageRouteImport.update({
id: '/overview',
path: '/overview',
id: '/certificates',
path: '/certificates',
getParentRoute: () => certManagerLayoutRoute,
} as any)
@@ -2265,10 +2265,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof certManagerCertificateAuthoritiesPageRouteImport
parentRoute: typeof certManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview'
path: '/overview'
fullPath: '/cert-manager/$projectId/overview'
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates': {
id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates'
path: '/certificates'
fullPath: '/cert-manager/$projectId/certificates'
preLoaderRoute: typeof certManagerCertificatesPageRouteImport
parentRoute: typeof certManagerLayoutImport
}
@@ -4056,7 +4056,7 @@ export interface FileRoutesByFullPath {
'/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute
'/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
'/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
'/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
@@ -4242,7 +4242,7 @@ export interface FileRoutesByTo {
'/organization/ssh/settings': typeof organizationSshSettingsPageRouteRoute
'/cert-manager/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
'/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
'/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
'/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute
'/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
'/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
@@ -4444,7 +4444,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout': typeof sshLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting': typeof certManagerAlertingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificates': typeof certManagerCertificatesPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
@@ -4640,7 +4640,7 @@ export interface FileRouteTypes {
| '/organization/ssh/settings'
| '/cert-manager/$projectId/alerting'
| '/cert-manager/$projectId/certificate-authorities'
| '/cert-manager/$projectId/overview'
| '/cert-manager/$projectId/certificates'
| '/cert-manager/$projectId/settings'
| '/kms/$projectId/kmip'
| '/kms/$projectId/overview'
@@ -4825,7 +4825,7 @@ export interface FileRouteTypes {
| '/organization/ssh/settings'
| '/cert-manager/$projectId/alerting'
| '/cert-manager/$projectId/certificate-authorities'
| '/cert-manager/$projectId/overview'
| '/cert-manager/$projectId/certificates'
| '/cert-manager/$projectId/settings'
| '/kms/$projectId/kmip'
| '/kms/$projectId/overview'
@@ -5025,7 +5025,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities'
| '/_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'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview'
@@ -5575,7 +5575,7 @@ export const routeTree = rootRoute
"children": [
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/alerting",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities",
"/_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",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management",
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers",
@@ -5640,7 +5640,7 @@ export const routeTree = rootRoute
"filePath": "cert-manager/CertificateAuthoritiesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
},
"/_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": {
"filePath": "cert-manager/CertificatesPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout"
},

View File

@@ -292,7 +292,7 @@ const certManagerRoutes = route("/cert-manager/$projectId", [
index("cert-manager/PkiSubscribersPage/route.tsx"),
route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx")
]),
route("/overview", "cert-manager/CertificatesPage/route.tsx"),
route("/certificates", "cert-manager/CertificatesPage/route.tsx"),
route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"),
route("/alerting", "cert-manager/AlertingPage/route.tsx"),
route("/ca/$caId", "cert-manager/CertAuthDetailsByIDPage/route.tsx"),