mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
PKI revamp: frontend implementation
This commit is contained in:
@@ -1,16 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const attributeTypeSchema = z.enum([
|
||||
"common_name",
|
||||
"organization_name",
|
||||
"organization_unit",
|
||||
"locality",
|
||||
"state",
|
||||
"country",
|
||||
"email",
|
||||
"street_address",
|
||||
"postal_code"
|
||||
]);
|
||||
const attributeTypeSchema = z.enum(["common_name"]);
|
||||
|
||||
const includeTypeSchema = z.enum(["mandatory", "optional", "prohibit"]);
|
||||
|
||||
@@ -137,14 +127,6 @@ export const deleteCertificateTemplateV2Schema = z.object({
|
||||
|
||||
export const certificateRequestSchema = z.object({
|
||||
commonName: z.string().optional(),
|
||||
organization: z.string().optional(),
|
||||
organizationUnit: z.string().optional(),
|
||||
locality: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
country: z.string().length(2).optional(),
|
||||
email: z.string().email().optional(),
|
||||
streetAddress: z.string().optional(),
|
||||
postalCode: z.string().optional(),
|
||||
keyUsages: z.array(z.string()).optional(),
|
||||
extendedKeyUsages: z.array(z.string()).optional(),
|
||||
subjectAlternativeNames: z
|
||||
|
||||
+4
-61
@@ -60,14 +60,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
type: "common_name",
|
||||
include: "mandatory",
|
||||
value: ["example.com"]
|
||||
},
|
||||
{
|
||||
type: "organization_name",
|
||||
include: "optional"
|
||||
},
|
||||
{
|
||||
type: "country",
|
||||
include: "prohibit"
|
||||
}
|
||||
],
|
||||
keyUsages: {
|
||||
@@ -416,7 +408,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
describe("validateCertificateRequest", () => {
|
||||
const validRequest: TCertificateRequest = {
|
||||
commonName: "example.com",
|
||||
organization: "Example Corp",
|
||||
keyUsages: ["digital_signature", "key_encipherment"],
|
||||
extendedKeyUsages: ["server_auth"],
|
||||
subjectAlternativeNames: [
|
||||
@@ -458,15 +449,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
expect(result.errors).toContain("common_name is mandatory but not provided in request");
|
||||
});
|
||||
|
||||
it("should detect prohibited attributes", async () => {
|
||||
const invalidRequest = { ...validRequest, country: "US" };
|
||||
|
||||
const result = await service.validateCertificateRequest("template-123", invalidRequest);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain("country is prohibited by template policy");
|
||||
});
|
||||
|
||||
it("should validate attribute values against allowed list", async () => {
|
||||
const invalidRequest = { ...validRequest, commonName: "forbidden.com" };
|
||||
|
||||
@@ -614,14 +596,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should allow optional attributes when not provided", async () => {
|
||||
const requestWithoutOrg = { ...validRequest, organization: undefined };
|
||||
|
||||
const result = await service.validateCertificateRequest("template-123", requestWithoutOrg);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow optional key usages and extended key usages", async () => {
|
||||
const requestWithOptionalUsages = {
|
||||
...validRequest,
|
||||
@@ -805,21 +779,14 @@ describe("CertificateTemplateV2Service", () => {
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle template with all fields prohibited", async () => {
|
||||
it("should handle template with SAN fields prohibited", async () => {
|
||||
const prohibitTemplate = {
|
||||
...sampleTemplate,
|
||||
attributes: [
|
||||
{
|
||||
type: "organization_name",
|
||||
include: "prohibit" as const
|
||||
},
|
||||
{
|
||||
type: "locality",
|
||||
include: "prohibit" as const
|
||||
},
|
||||
{
|
||||
type: "country",
|
||||
include: "prohibit" as const
|
||||
type: "common_name",
|
||||
include: "mandatory" as const,
|
||||
value: ["example.com"]
|
||||
}
|
||||
],
|
||||
keyUsages: {
|
||||
@@ -845,9 +812,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
|
||||
const requestWithProhibited = {
|
||||
commonName: "example.com",
|
||||
organization: "Test Org",
|
||||
locality: "Test City",
|
||||
country: "US",
|
||||
keyUsages: ["digital_signature"],
|
||||
extendedKeyUsages: ["server_auth"],
|
||||
subjectAlternativeNames: [
|
||||
@@ -859,9 +823,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
|
||||
const result = await service.validateCertificateRequest("template-123", requestWithProhibited);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain("organization_name is prohibited by template policy");
|
||||
expect(result.errors).toContain("locality is prohibited by template policy");
|
||||
expect(result.errors).toContain("country is prohibited by template policy");
|
||||
expect(result.errors).toContain("email SAN is prohibited by template policy");
|
||||
expect(result.errors).toContain("uri SAN is prohibited by template policy");
|
||||
});
|
||||
@@ -874,16 +835,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
type: "common_name",
|
||||
include: "mandatory" as const,
|
||||
value: ["example.com", "test.com"]
|
||||
},
|
||||
{
|
||||
type: "organization_name",
|
||||
include: "optional" as const,
|
||||
value: ["Example Corp", "Test Corp"]
|
||||
},
|
||||
{
|
||||
type: "country",
|
||||
include: "mandatory" as const,
|
||||
value: ["US", "CA"]
|
||||
}
|
||||
],
|
||||
subjectAlternativeNames: [
|
||||
@@ -898,8 +849,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
|
||||
const validConstrainedRequest = {
|
||||
commonName: "example.com",
|
||||
organization: "Example Corp",
|
||||
country: "US",
|
||||
keyUsages: ["digital_signature", "key_encipherment"],
|
||||
extendedKeyUsages: ["server_auth"],
|
||||
validity: { ttl: "30d" }
|
||||
@@ -910,8 +859,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
|
||||
const invalidConstrainedRequest = {
|
||||
commonName: "forbidden.com",
|
||||
organization: "Forbidden Corp",
|
||||
country: "FR",
|
||||
keyUsages: ["digital_signature", "key_encipherment"],
|
||||
extendedKeyUsages: ["server_auth"],
|
||||
validity: { ttl: "30d" }
|
||||
@@ -920,10 +867,6 @@ describe("CertificateTemplateV2Service", () => {
|
||||
const invalidResult = await service.validateCertificateRequest("template-123", invalidConstrainedRequest);
|
||||
expect(invalidResult.isValid).toBe(false);
|
||||
expect(invalidResult.errors).toContain("common_name value 'forbidden.com' is not in allowed values list");
|
||||
expect(invalidResult.errors).toContain(
|
||||
"organization_name value 'Forbidden Corp' does not match allowed patterns: Example Corp, Test Corp"
|
||||
);
|
||||
expect(invalidResult.errors).toContain("country value 'FR' is not in allowed values list");
|
||||
});
|
||||
|
||||
it("should validate SAN value constraints with multiple types", async () => {
|
||||
|
||||
@@ -72,22 +72,6 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
switch (attrType) {
|
||||
case "common_name":
|
||||
return request.commonName;
|
||||
case "organization_name":
|
||||
return request.organization;
|
||||
case "organization_unit":
|
||||
return request.organizationUnit;
|
||||
case "locality":
|
||||
return request.locality;
|
||||
case "state":
|
||||
return request.state;
|
||||
case "country":
|
||||
return request.country;
|
||||
case "email":
|
||||
return request.email;
|
||||
case "street_address":
|
||||
return request.streetAddress;
|
||||
case "postal_code":
|
||||
return request.postalCode;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -145,8 +129,25 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
if (!requestValue) {
|
||||
errors.push(`${attrPolicy.type} is mandatory but not provided in request`);
|
||||
} else if (attrPolicy.value && attrPolicy.value.length > 0) {
|
||||
if (!attrPolicy.value.includes(requestValue)) {
|
||||
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
||||
// Check if the request value matches any allowed pattern
|
||||
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
|
||||
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
||||
if (allowedValue.includes("*")) {
|
||||
// Handle wildcard patterns
|
||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||
const regex = new RE2(`^${pattern}$`);
|
||||
return regex.test(requestValue);
|
||||
}
|
||||
return allowedValue === requestValue;
|
||||
});
|
||||
if (!isValidValue) {
|
||||
if (hasWildcards) {
|
||||
errors.push(
|
||||
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
||||
);
|
||||
} else {
|
||||
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,18 +157,24 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
}
|
||||
|
||||
if (attrPolicy.include === "optional" && requestValue && attrPolicy.value && attrPolicy.value.length > 0) {
|
||||
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
|
||||
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
||||
if (allowedValue.includes("*")) {
|
||||
const pattern = allowedValue.replace(/\*/g, "[^.]*");
|
||||
// Handle wildcard patterns - escape dots and replace * with .*
|
||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||
const regex = new RE2(`^${pattern}$`);
|
||||
return regex.test(requestValue);
|
||||
}
|
||||
return allowedValue === requestValue;
|
||||
});
|
||||
if (!isValidValue) {
|
||||
errors.push(
|
||||
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
||||
);
|
||||
if (hasWildcards) {
|
||||
errors.push(
|
||||
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
||||
);
|
||||
} else {
|
||||
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -216,9 +223,25 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
if (requestSans.length === 0) {
|
||||
errors.push(`${sanPolicy.type} SAN is mandatory but not provided in request`);
|
||||
} else if (sanPolicy.value && sanPolicy.value.length > 0) {
|
||||
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
|
||||
requestSans.forEach((san) => {
|
||||
if (!sanPolicy.value!.includes(san.value)) {
|
||||
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
||||
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
||||
if (allowedValue.includes("*")) {
|
||||
// Handle wildcard patterns - escape dots and replace * with .*
|
||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||
const regex = new RE2(`^${pattern}$`);
|
||||
return regex.test(san.value);
|
||||
}
|
||||
return allowedValue === san.value;
|
||||
});
|
||||
if (!isValidValue) {
|
||||
if (hasWildcards) {
|
||||
errors.push(
|
||||
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
||||
);
|
||||
} else {
|
||||
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -229,19 +252,25 @@ export const certificateTemplateV2ServiceFactory = ({
|
||||
}
|
||||
|
||||
if (sanPolicy.include === "optional" && sanPolicy.value && sanPolicy.value.length > 0) {
|
||||
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
|
||||
requestSans.forEach((san) => {
|
||||
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
||||
if (allowedValue.includes("*")) {
|
||||
const pattern = allowedValue.replace(/\*/g, "[^.]*");
|
||||
// Handle wildcard patterns - escape dots and replace * with .*
|
||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||
const regex = new RE2(`^${pattern}$`);
|
||||
return regex.test(san.value);
|
||||
}
|
||||
return allowedValue === san.value;
|
||||
});
|
||||
if (!isValidValue) {
|
||||
errors.push(
|
||||
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
||||
);
|
||||
if (hasWildcards) {
|
||||
errors.push(
|
||||
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
||||
);
|
||||
} else {
|
||||
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,16 +2,7 @@ import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/
|
||||
|
||||
export interface TTemplateV2Policy {
|
||||
attributes: Array<{
|
||||
type:
|
||||
| "common_name"
|
||||
| "organization_name"
|
||||
| "organization_unit"
|
||||
| "locality"
|
||||
| "state"
|
||||
| "country"
|
||||
| "email"
|
||||
| "street_address"
|
||||
| "postal_code";
|
||||
type: "common_name";
|
||||
include: "mandatory" | "optional" | "prohibit";
|
||||
value?: string[];
|
||||
}>;
|
||||
@@ -97,14 +88,6 @@ export type TCertificateTemplateV2Update = Partial<
|
||||
|
||||
export interface TCertificateRequest {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
organizationUnit?: string;
|
||||
locality?: string;
|
||||
state?: string;
|
||||
country?: string;
|
||||
email?: string;
|
||||
streetAddress?: string;
|
||||
postalCode?: string;
|
||||
keyUsages?: string[];
|
||||
extendedKeyUsages?: string[];
|
||||
subjectAlternativeNames?: Array<{
|
||||
|
||||
Reference in New Issue
Block a user