mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 18:26:04 +00:00
feat: initial enforcement of template policy
This commit is contained in:
Generated
+24
@@ -75,6 +75,7 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.0.0",
|
||||||
|
"safe-regex": "^2.1.1",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
@@ -106,6 +107,7 @@
|
|||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
|
"@types/safe-regex": "^1.1.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@@ -9774,6 +9776,12 @@
|
|||||||
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
"integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/safe-regex": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-CQ/uPB9fLOPKwDsrTeVbNIkwfUthTWOx0l6uIGwVFjZxv7e68pCW5gtTYFzdJi3EBJp8h8zYhJbTasAbX7gEMQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.6",
|
"version": "7.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||||
@@ -17778,6 +17786,14 @@
|
|||||||
"@babel/runtime": "^7.8.4"
|
"@babel/runtime": "^7.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regexp-tree": {
|
||||||
|
"version": "0.1.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||||
|
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||||
|
"bin": {
|
||||||
|
"regexp-tree": "bin/regexp-tree"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||||
@@ -18094,6 +18110,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-regex": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
|
||||||
|
"dependencies": {
|
||||||
|
"regexp-tree": "~0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-regex-test": {
|
"node_modules/safe-regex-test": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
"@types/prompt-sync": "^4.2.3",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"@types/resolve": "^1.20.6",
|
"@types/resolve": "^1.20.6",
|
||||||
|
"@types/safe-regex": "^1.1.6",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
"@typescript-eslint/parser": "^6.20.0",
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
@@ -171,6 +172,7 @@
|
|||||||
"pino": "^8.16.2",
|
"pino": "^8.16.2",
|
||||||
"posthog-node": "^3.6.2",
|
"posthog-node": "^3.6.2",
|
||||||
"probot": "^13.0.0",
|
"probot": "^13.0.0",
|
||||||
|
"safe-regex": "^2.1.1",
|
||||||
"smee-client": "^2.0.0",
|
"smee-client": "^2.0.0",
|
||||||
"tedious": "^18.2.1",
|
"tedious": "^18.2.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
|
|||||||
Vendored
+3
-5
@@ -53,6 +53,9 @@ import {
|
|||||||
TCertificateSecretsUpdate,
|
TCertificateSecretsUpdate,
|
||||||
TCertificatesInsert,
|
TCertificatesInsert,
|
||||||
TCertificatesUpdate,
|
TCertificatesUpdate,
|
||||||
|
TCertificateTemplates,
|
||||||
|
TCertificateTemplatesInsert,
|
||||||
|
TCertificateTemplatesUpdate,
|
||||||
TDynamicSecretLeases,
|
TDynamicSecretLeases,
|
||||||
TDynamicSecretLeasesInsert,
|
TDynamicSecretLeasesInsert,
|
||||||
TDynamicSecretLeasesUpdate,
|
TDynamicSecretLeasesUpdate,
|
||||||
@@ -318,11 +321,6 @@ import {
|
|||||||
TWebhooksInsert,
|
TWebhooksInsert,
|
||||||
TWebhooksUpdate
|
TWebhooksUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import {
|
|
||||||
TCertificateTemplates,
|
|
||||||
TCertificateTemplatesInsert,
|
|
||||||
TCertificateTemplatesUpdate
|
|
||||||
} from "@app/db/schemas/certificate-templates";
|
|
||||||
import {
|
import {
|
||||||
TSecretV2TagJunction,
|
TSecretV2TagJunction,
|
||||||
TSecretV2TagJunctionInsert,
|
TSecretV2TagJunctionInsert,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
|||||||
export * from "./certificate-authority-secret";
|
export * from "./certificate-authority-secret";
|
||||||
export * from "./certificate-bodies";
|
export * from "./certificate-bodies";
|
||||||
export * from "./certificate-secrets";
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificate-templates";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
|
|||||||
@@ -1088,6 +1088,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
},
|
},
|
||||||
ISSUE_CERT: {
|
ISSUE_CERT: {
|
||||||
caId: "The ID of the CA to issue the certificate from",
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
|
certificateTemplateId: "The ID of the certificate template to issue the certificate from",
|
||||||
friendlyName: "A friendly name for the certificate",
|
friendlyName: "A friendly name for the certificate",
|
||||||
commonName: "The common name (CN) for the certificate",
|
commonName: "The common name (CN) for the certificate",
|
||||||
altNames:
|
altNames:
|
||||||
|
|||||||
@@ -622,6 +622,7 @@ export const registerRoutes = async (
|
|||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateTemplateDAL,
|
||||||
certificateAuthorityQueue,
|
certificateAuthorityQueue,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CertificatesSchema } from "@app/db/schemas";
|
import { CertificatesSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
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) => {
|
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
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({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:serialNumber/revoke",
|
url: "/:serialNumber/revoke",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
@@ -58,9 +60,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
name: z.string(),
|
name: z.string().min(1),
|
||||||
commonName: z.string(),
|
commonName: validateTemplateRegexField,
|
||||||
ttl: z.string()
|
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -91,9 +93,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
caId: z.string().optional(),
|
caId: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().min(1).optional(),
|
||||||
commonName: z.string().optional(),
|
commonName: validateTemplateRegexField.optional(),
|
||||||
ttl: z.string().optional()
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
params: z.object({
|
params: z.object({
|
||||||
certificateTemplateId: z.string()
|
certificateTemplateId: z.string()
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { z } from "zod";
|
|||||||
import {
|
import {
|
||||||
CertificateAuthoritiesSchema,
|
CertificateAuthoritiesSchema,
|
||||||
CertificatesSchema,
|
CertificatesSchema,
|
||||||
|
CertificateTemplatesSchema,
|
||||||
PkiAlertsSchema,
|
PkiAlertsSchema,
|
||||||
PkiCollectionsSchema,
|
PkiCollectionsSchema,
|
||||||
ProjectKeysSchema
|
ProjectKeysSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { CertificateTemplatesSchema } from "@app/db/schemas/certificate-templates";
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -478,7 +478,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
name: true
|
name: true
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
caName: z.string()
|
caName: z.string(),
|
||||||
|
caId: z.string()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import crypto, { KeyObject } from "crypto";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -16,6 +17,8 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
|
|||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||||
|
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||||
|
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +57,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "create" | "findOne" | "transaction" | "find">;
|
||||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "create" | "findOne">;
|
||||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "create" | "findOne" | "update">;
|
||||||
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getById">;
|
||||||
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "transaction" | "create" | "find">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
@@ -69,6 +73,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateTemplateDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1001,6 +1006,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const issueCertFromCa = async ({
|
const issueCertFromCa = async ({
|
||||||
caId,
|
caId,
|
||||||
|
certificateTemplateId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames,
|
||||||
@@ -1012,8 +1018,25 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TIssueCertFromCaDTO) => {
|
}: TIssueCertFromCaDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
let ca: TCertificateAuthorities | undefined;
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
|
|
||||||
|
if (caId) {
|
||||||
|
ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
} else if (certificateTemplateId) {
|
||||||
|
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||||
|
if (!certificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Certificate template not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ca) {
|
||||||
|
throw new BadRequestError({ message: "CA not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -1073,6 +1096,17 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certificateTemplate) {
|
||||||
|
validateCertificateDetailsAgainstTemplate(
|
||||||
|
{
|
||||||
|
commonName,
|
||||||
|
notBeforeDate,
|
||||||
|
notAfterDate
|
||||||
|
},
|
||||||
|
certificateTemplate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
@@ -1207,6 +1241,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
*/
|
*/
|
||||||
const signCertFromCa = async ({
|
const signCertFromCa = async ({
|
||||||
caId,
|
caId,
|
||||||
|
certificateTemplateId,
|
||||||
csr,
|
csr,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
@@ -1219,8 +1254,25 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TSignCertFromCaDTO) => {
|
}: TSignCertFromCaDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
let ca: TCertificateAuthorities | undefined;
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
|
|
||||||
|
if (caId) {
|
||||||
|
ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
} else if (certificateTemplateId) {
|
||||||
|
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||||
|
if (!certificateTemplate) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Certificate template not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ca = await certificateAuthorityDAL.findById(certificateTemplate.caId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ca) {
|
||||||
|
throw new BadRequestError({ message: "CA not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -1293,6 +1345,17 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (certificateTemplate) {
|
||||||
|
validateCertificateDetailsAgainstTemplate(
|
||||||
|
{
|
||||||
|
commonName: cn,
|
||||||
|
notBeforeDate,
|
||||||
|
notAfterDate
|
||||||
|
},
|
||||||
|
certificateTemplate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { caPrivateKey } = await getCaCredentials({
|
const { caPrivateKey } = await getCaCredentials({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ export type TImportCertToCaDTO = {
|
|||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TIssueCertFromCaDTO = {
|
export type TIssueCertFromCaDTO = {
|
||||||
caId: string;
|
caId?: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
altNames: string;
|
altNames: string;
|
||||||
@@ -96,8 +97,9 @@ export type TIssueCertFromCaDTO = {
|
|||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TSignCertFromCaDTO = {
|
export type TSignCertFromCaDTO = {
|
||||||
caId: string;
|
caId?: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
altNames: string;
|
altNames: string;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
export const validateCertificateDetailsAgainstTemplate = (
|
||||||
|
cert: {
|
||||||
|
commonName: string;
|
||||||
|
notBeforeDate: Date;
|
||||||
|
notAfterDate: Date;
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
commonName: string;
|
||||||
|
ttl: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const commonNameRegex = new RegExp(template.commonName);
|
||||||
|
if (!commonNameRegex.test(cert.commonName)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid common name based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert.notAfterDate.getTime() - cert.notBeforeDate.getTime() > ms(template.ttl)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid validity date based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import safe from "safe-regex";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export const validateTemplateRegexField = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(100)
|
||||||
|
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||||
|
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||||
|
})
|
||||||
|
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||||
|
.refine((v) => safe(v), {
|
||||||
|
message: "Unsafe REGEX pattern"
|
||||||
|
});
|
||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
TRenewCaResponse,
|
TRenewCaResponse,
|
||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TSignIntermediateResponse,
|
TSignIntermediateResponse,
|
||||||
TUpdateCaDTO} from "./types";
|
TUpdateCaDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useCreateCa = () => {
|
export const useCreateCa = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -98,9 +99,9 @@ export const useImportCaCertificate = () => {
|
|||||||
export const useCreateCertificate = () => {
|
export const useCreateCertificate = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
return useMutation<TCreateCertificateResponse, {}, TCreateCertificateDTO>({
|
||||||
mutationFn: async ({ caId, ...body }) => {
|
mutationFn: async (body) => {
|
||||||
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
const { data } = await apiRequest.post<TCreateCertificateResponse>(
|
||||||
`/api/v1/pki/ca/${caId}/issue-certificate`,
|
"/api/v1/pki/certificates/issue-certificate",
|
||||||
body
|
body
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||||
import { CaRenewalType,CaStatus, CaType } from "./enums";
|
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||||
|
|
||||||
export type TCertificateAuthority = {
|
export type TCertificateAuthority = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -78,7 +78,8 @@ export type TImportCaCertificateResponse = {
|
|||||||
|
|
||||||
export type TCreateCertificateDTO = {
|
export type TCreateCertificateDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
caId: string;
|
caId?: string;
|
||||||
|
certificateTemplateId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
altNames: string; // sans
|
altNames: string; // sans
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type TCertificateTemplateListEntry = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
caName: string;
|
caName: string;
|
||||||
|
caId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateTemplate = {
|
export type TCertificateTemplate = {
|
||||||
|
|||||||
+66
-9
@@ -14,13 +14,21 @@ import {
|
|||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { CaStatus, useCreateCertificate, useGetCert, useListWorkspaceCas } from "@app/hooks/api";
|
import {
|
||||||
|
CaStatus,
|
||||||
|
useCreateCertificate,
|
||||||
|
useGetCert,
|
||||||
|
useGetCertTemplate,
|
||||||
|
useListWorkspaceCas,
|
||||||
|
useListWorkspaceCertificateTemplates
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
certificateTemplateId: z.string().optional(),
|
||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
commonName: z.string().trim().min(1),
|
commonName: z.string().trim().min(1),
|
||||||
@@ -42,6 +50,8 @@ type TCertificateDetails = {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CERT_TEMPLATE_NONE_VALUE = "none";
|
||||||
|
|
||||||
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@@ -54,6 +64,10 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
status: CaStatus.ACTIVE
|
status: CaStatus.ACTIVE
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||||
|
workspaceId: currentWorkspace?.id || ""
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificate();
|
const { mutateAsync: createCertificate } = useCreateCertificate();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -61,11 +75,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
setValue
|
setValue,
|
||||||
|
watch
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedCertTemplateId = watch("certificateTemplateId");
|
||||||
|
const hasCertTemplateSelected =
|
||||||
|
selectedCertTemplateId !== "" && selectedCertTemplateId !== CERT_TEMPLATE_NONE_VALUE;
|
||||||
|
|
||||||
|
const { data: selectedCertTemplate } = useGetCertTemplate(
|
||||||
|
hasCertTemplateSelected ? (selectedCertTemplateId as string) : ""
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cert) {
|
if (cert) {
|
||||||
reset({
|
reset({
|
||||||
@@ -81,7 +104,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
commonName: "",
|
commonName: "",
|
||||||
altNames: "",
|
altNames: "",
|
||||||
ttl: ""
|
ttl: "",
|
||||||
|
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [cert]);
|
}, [cert]);
|
||||||
@@ -91,8 +115,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
if (!currentWorkspace?.slug) return;
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||||
|
caId: !selectedCertTemplate ? caId : undefined,
|
||||||
|
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||||
projectSlug: currentWorkspace.slug,
|
projectSlug: currentWorkspace.slug,
|
||||||
caId,
|
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames,
|
||||||
@@ -141,11 +166,11 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="caId"
|
name="certificateTemplateId"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Issuing CA"
|
label="Certificate Template"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
@@ -158,15 +183,47 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
isDisabled={Boolean(cert)}
|
isDisabled={Boolean(cert)}
|
||||||
>
|
>
|
||||||
{(cas || []).map(({ id, type, dn }) => (
|
<SelectItem value={CERT_TEMPLATE_NONE_VALUE} key="cert-template-none">
|
||||||
<SelectItem value={id} key={`ca-${id}`}>
|
None
|
||||||
{`${caTypeToNameMap[type]}: ${dn}`}
|
</SelectItem>
|
||||||
|
{(templatesData?.certificateTemplates || []).map(({ id, name }) => (
|
||||||
|
<SelectItem value={id} key={`cert-template-${id}`}>
|
||||||
|
{name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{(!selectedCertTemplateId || selectedCertTemplateId === CERT_TEMPLATE_NONE_VALUE) && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="caId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Issuing CA"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={Boolean(cert)}
|
||||||
|
>
|
||||||
|
{(cas || []).map(({ id, type, dn }) => (
|
||||||
|
<SelectItem value={id} key={`ca-${id}`}>
|
||||||
|
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
|
|||||||
+9
-1
@@ -27,7 +27,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
commonName: z.string().trim().min(1),
|
commonName: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(100)
|
||||||
|
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||||
|
message:
|
||||||
|
"Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||||
|
}),
|
||||||
ttl: z.string().trim().min(1)
|
ttl: z.string().trim().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user