feat: initial enforcement of template policy

This commit is contained in:
Sheen Capadngan
2024-08-16 16:32:47 +08:00
parent 28a2a6c41a
commit b9bd518aa6
18 changed files with 427 additions and 37 deletions
@@ -1,12 +1,17 @@
import ms from "ms";
import { z } from "zod";
import { CertificatesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATES } from "@app/lib/api-docs";
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CrlReason } from "@app/services/certificate/certificate-types";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
export const registerCertRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -55,6 +60,183 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/issue-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Issue certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
privateKey: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, ca } =
await server.services.certificateAuthority.issueCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.ISSUE_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
privateKey,
serialNumber
};
}
});
server.route({
method: "POST",
url: "/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate",
body: z
.object({
caId: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId),
certificateTemplateId: z
.string()
.trim()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateTemplateId),
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
)
.refine(
(data) =>
(data.caId !== undefined && data.certificateTemplateId === undefined) ||
(data.caId === undefined && data.certificateTemplateId !== undefined),
{
message: "Either CA ID or Certificate Template ID must be present, but not both",
path: ["caId", "certificateTemplateId"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.SIGN_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
server.route({
method: "POST",
url: "/:serialNumber/revoke",
@@ -1,9 +1,11 @@
import ms from "ms";
import { z } from "zod";
import { CertificateTemplatesSchema } from "@app/db/schemas/certificate-templates";
import { CertificateTemplatesSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
id: true,
@@ -58,9 +60,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
schema: {
body: z.object({
caId: z.string(),
name: z.string(),
commonName: z.string(),
ttl: z.string()
name: z.string().min(1),
commonName: validateTemplateRegexField,
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
}),
response: {
200: z.object({
@@ -91,9 +93,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
schema: {
body: z.object({
caId: z.string().optional(),
name: z.string().optional(),
commonName: z.string().optional(),
ttl: z.string().optional()
name: z.string().min(1).optional(),
commonName: validateTemplateRegexField.optional(),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
}),
params: z.object({
certificateTemplateId: z.string()