feat: added subscriber cert auto-renewal

This commit is contained in:
Sheen Capadngan
2025-05-23 00:53:50 +08:00
parent e8d00161eb
commit 64f724ed95
13 changed files with 564 additions and 201 deletions

View File

@@ -94,6 +94,11 @@ export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.string("ttl").nullable().alter();
t.boolean("enableAutoRenewal").notNullable().defaultTo(false);
t.integer("autoRenewalPeriodInDays");
t.datetime("lastAutoRenewAt");
t.string("lastOperationStatus");
t.text("lastOperationMessage");
t.dateTime("lastOperationAt");
@@ -188,6 +193,10 @@ export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.dropColumn("enableAutoRenewal");
t.dropColumn("autoRenewalPeriodInDays");
t.dropColumn("lastAutoRenewAt");
t.dropColumn("lastOperationStatus");
t.dropColumn("lastOperationMessage");
t.dropColumn("lastOperationAt");

View File

@@ -20,6 +20,9 @@ export const PkiSubscribersSchema = z.object({
keyUsages: z.string().array(),
extendedKeyUsages: z.string().array(),
status: z.string(),
enableAutoRenewal: z.boolean().default(false),
autoRenewalPeriodInDays: z.number().nullable().optional(),
lastAutoRenewAt: z.date().nullable().optional(),
lastOperationStatus: z.string().nullable().optional(),
lastOperationMessage: z.string().nullable().optional(),
lastOperationAt: z.date().nullable().optional()

View File

@@ -268,6 +268,7 @@ export enum EventType {
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
CREATE_KMS = "create-kms",
@@ -2099,6 +2100,14 @@ interface IssuePkiSubscriberCert {
};
}
interface AutomatedRenewPkiSubscriberCert {
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT;
metadata: {
subscriberId: string;
name: string;
};
}
interface SignPkiSubscriberCert {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
metadata: {
@@ -3103,6 +3112,7 @@ export type Event =
| GetPkiSubscriber
| IssuePkiSubscriberCert
| SignPkiSubscriberCert
| AutomatedRenewPkiSubscriberCert
| ListPkiSubscriberCerts
| GetSubscriberActiveCertBundle
| CreateKmsEvent

View File

@@ -1811,7 +1811,9 @@ export const PKI_SUBSCRIBERS = {
subjectAlternativeNames:
"A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
keyUsages: "The key usage extension to be used on certificates issued for this subscriber.",
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber."
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber.",
enableAutoRenewal: "Whether or not to enable auto renewal for the PKI subscriber.",
autoRenewalPeriodInDays: "The period in days to auto renew the PKI subscriber's certificates."
},
UPDATE: {
projectId: "The ID of the project to update the PKI subscriber in.",
@@ -1825,7 +1827,9 @@ export const PKI_SUBSCRIBERS = {
"A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.",
extendedKeyUsages:
"The extended key usage extension to be used on certificates issued for this subscriber to update to."
"The extended key usage extension to be used on certificates issued for this subscriber to update to.",
enableAutoRenewal: "Whether or not to enable auto renewal for the PKI subscriber.",
autoRenewalPeriodInDays: "The period in days to auto renew the PKI subscriber's certificates."
},
DELETE: {
subscriberName: "The name of the PKI subscriber to delete.",

View File

@@ -37,6 +37,7 @@ export enum QueueName {
AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSubscriber = "pki-subscriber",
TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook",
@@ -87,7 +88,8 @@ export enum QueueJobs {
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
InvalidateCache = "invalidate-cache",
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber"
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal"
}
export type TQueueJobTypes = {
@@ -255,6 +257,10 @@ export type TQueueJobTypes = {
caType: CaType;
};
};
[QueueName.PkiSubscriber]: {
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
payload: undefined;
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -134,6 +134,7 @@ import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal";
import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns";
import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
@@ -202,6 +203,7 @@ import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collec
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue";
@@ -1700,6 +1702,28 @@ export const registerRoutes = async (
pkiSubscriberDAL
});
const internalCaFns = InternalCertificateAuthorityFns({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService
});
const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({
queueService,
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityQueue,
certificateDAL,
auditLogService,
internalCaFns
});
const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL,
certificateAuthorityDAL,
@@ -1712,7 +1736,8 @@ export const registerRoutes = async (
projectDAL,
kmsService,
permissionService,
certificateAuthorityQueue
certificateAuthorityQueue,
internalCaFns
});
await secretRotationV2QueueServiceFactory({
@@ -1735,6 +1760,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts();
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await kmsService.startService();
await microsoftTeamsService.start();

View File

@@ -110,7 +110,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.array()
.default([])
.transform((arr) => Array.from(new Set(arr)))
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages)
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal),
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays)
}),
response: {
200: sanitizedPkiSubscriber
@@ -195,7 +197,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.array()
.transform((arr) => Array.from(new Set(arr)))
.optional()
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages)
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal),
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays)
}),
response: {
200: sanitizedPkiSubscriber

View File

@@ -0,0 +1,186 @@
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { CaStatus, CaType } from "../certificate-authority/certificate-authority-enums";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority/certificate-authority-queue";
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
import { TPkiSubscriberDALFactory } from "./pki-subscriber-dal";
import { PkiSubscriberStatus, SubscriberOperationStatus } from "./pki-subscriber-types";
type TPkiSubscriberQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
pkiSubscriberDAL: TPkiSubscriberDALFactory;
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
certificateAuthorityQueue: TCertificateAuthorityQueueFactory;
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
certificateDAL: TCertificateDALFactory;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export const pkiSubscriberQueueServiceFactory = ({
queueService,
pkiSubscriberDAL,
certificateAuthorityDAL,
certificateAuthorityQueue,
internalCaFns,
certificateDAL,
auditLogService
}: TPkiSubscriberQueueServiceFactoryDep) => {
queueService.start(QueueName.PkiSubscriber, async (job) => {
if (job.name === QueueJobs.PkiSubscriberDailyAutoRenewal) {
logger.info(`${QueueJobs.PkiSubscriberDailyAutoRenewal}: queue task started`);
const BATCH_SIZE = 100;
let offset = 0;
let hasMore = true;
while (hasMore) {
// fetch PKI subscribers with auto renewal enabled in batches
// eslint-disable-next-line no-await-in-loop
const pkiSubscribers = await pkiSubscriberDAL.find(
{
enableAutoRenewal: true,
$notNull: ["autoRenewalPeriodInDays"],
status: PkiSubscriberStatus.ACTIVE
},
{
limit: BATCH_SIZE,
offset
}
);
if (pkiSubscribers.length === 0) {
hasMore = false;
break;
}
// Process each subscriber in the batch concurrently
// eslint-disable-next-line no-await-in-loop
await Promise.all(
pkiSubscribers.map(async (subscriber) => {
try {
const cert = await certificateDAL.findLatestActiveCertForSubscriber({ subscriberId: subscriber.id });
let shouldRenew = false;
if (!cert || !cert.notAfter) {
shouldRenew = true;
} else {
const now = new Date();
const expiry = new Date(cert.notAfter);
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
shouldRenew = daysUntilExpiry <= subscriber.autoRenewalPeriodInDays!;
}
if (shouldRenew) {
// Get the CA for the subscriber
if (!subscriber.caId) {
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: "No CA assigned to subscriber",
lastOperationAt: new Date()
});
return;
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (!ca) {
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: "CA not found",
lastOperationAt: new Date()
});
return;
}
// Check if CA is active
if (ca.status !== CaStatus.ACTIVE) {
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: "CA is not active",
lastOperationAt: new Date()
});
return;
}
// Order new certificate based on CA type
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
await certificateAuthorityQueue.orderCertificateForSubscriber({
subscriberId: subscriber.id,
caType: ca.externalCa.type
});
} else if (ca.internalCa?.id) {
// For internal CAs, we can issue certificates directly
await internalCaFns.issueCertificate(subscriber, ca);
}
// Update last auto-renew timestamp
await pkiSubscriberDAL.updateById(subscriber.id, {
lastAutoRenewAt: new Date(),
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
lastOperationMessage: "Triggered certificate auto-renewal",
lastOperationAt: new Date()
});
await auditLogService.createAuditLog({
projectId: subscriber.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.AUTOMATED_RENEW_SUBSCRIBER_CERT,
metadata: {
subscriberId: subscriber.id,
name: subscriber.name
}
}
});
}
} catch (error) {
// Log error and update subscriber status
logger.error(error, `Failed to auto-renew certificate for subscriber ${subscriber.id}`);
await pkiSubscriberDAL.updateById(subscriber.id, {
lastOperationStatus: SubscriberOperationStatus.FAILED,
lastOperationMessage: error instanceof Error ? error.message : "Unknown error",
lastOperationAt: new Date()
});
}
})
);
offset += BATCH_SIZE;
}
logger.info(`${QueueJobs.PkiSubscriberDailyAutoRenewal}: queue task completed`);
}
});
// we do a repeat cron job in utc timezone at 12 Midnight each day
const startDailyAutoRenewalJob = async () => {
// clear previous job
await queueService.stopRepeatableJob(
QueueName.PkiSubscriber,
QueueJobs.PkiSubscriberDailyAutoRenewal,
// { pattern: "0 0 * * *", utc: true },
{ pattern: "*/30 * * * * *", utc: true },
QueueName.PkiSubscriber // just a job id
);
await queueService.queue(QueueName.PkiSubscriber, QueueJobs.PkiSubscriberDailyAutoRenewal, undefined, {
delay: 5000,
jobId: QueueName.PkiSubscriber,
repeat: { pattern: "*/30 * * * * *", utc: true }
});
};
queueService.listen(QueueName.PkiSubscriber, "failed", (_, err) => {
logger.error(err, `${QueueName.PkiSubscriber}: failed`);
});
return {
startDailyAutoRenewalJob
};
};

View File

@@ -15,7 +15,10 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
extendedKeyUsages: true,
lastOperationStatus: true,
lastOperationMessage: true,
lastOperationAt: true
lastOperationAt: true,
enableAutoRenewal: true,
autoRenewalPeriodInDays: true,
lastAutoRenewAt: true
}).extend({
supportsImmediateCertIssuance: z.boolean().optional()
});

View File

@@ -78,6 +78,7 @@ type TPkiSubscriberServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
};
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
@@ -94,20 +95,9 @@ export const pkiSubscriberServiceFactory = ({
projectDAL,
kmsService,
permissionService,
certificateAuthorityQueue
certificateAuthorityQueue,
internalCaFns
}: TPkiSubscriberServiceFactoryDep) => {
const internalCaFns = InternalCertificateAuthorityFns({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService
});
const createSubscriber = async ({
name,
commonName,
@@ -117,6 +107,8 @@ export const pkiSubscriberServiceFactory = ({
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays,
projectId,
actorId,
actorAuthMethod,
@@ -139,6 +131,12 @@ export const pkiSubscriberServiceFactory = ({
})
);
if (enableAutoRenewal) {
if (!autoRenewalPeriodInDays) {
throw new BadRequestError({ message: "autoRenewalPeriodInDays is required when enableAutoRenewal is true" });
}
}
const newSubscriber = await pkiSubscriberDAL.create({
caId,
projectId,
@@ -148,7 +146,9 @@ export const pkiSubscriberServiceFactory = ({
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
});
return newSubscriber;
@@ -210,6 +210,8 @@ export const pkiSubscriberServiceFactory = ({
subjectAlternativeNames,
keyUsages,
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays,
actorId,
actorAuthMethod,
actor,
@@ -237,6 +239,12 @@ export const pkiSubscriberServiceFactory = ({
})
);
if (enableAutoRenewal) {
if (!autoRenewalPeriodInDays && !subscriber.autoRenewalPeriodInDays) {
throw new BadRequestError({ message: "autoRenewalPeriodInDays is required when enableAutoRenewal is true" });
}
}
const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, {
caId,
name,
@@ -245,7 +253,9 @@ export const pkiSubscriberServiceFactory = ({
ttl,
subjectAlternativeNames,
keyUsages,
extendedKeyUsages
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
});
return updatedSubscriber;

View File

@@ -16,6 +16,8 @@ export type TCreatePkiSubscriberDTO = {
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
} & TProjectPermission;
export type TGetPkiSubscriberDTO = {
@@ -32,6 +34,8 @@ export type TUpdatePkiSubscriberDTO = {
subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
} & TProjectPermission;
export type TDeletePkiSubscriberDTO = {

View File

@@ -22,6 +22,8 @@ export type TPkiSubscriber = {
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
supportsImmediateCertIssuance?: boolean;
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
lastOperationStatus?: SubscriberOperationStatus;
lastOperationMessage?: string;
lastOperationAt?: string;
@@ -36,6 +38,8 @@ export type TCreatePkiSubscriberDTO = {
subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
};
export type TUpdatePkiSubscriberDTO = {
@@ -49,6 +53,8 @@ export type TUpdatePkiSubscriberDTO = {
subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
};
export type TDeletePkiSubscriberDTO = {

View File

@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -16,7 +16,11 @@ import {
Modal,
ModalContent,
Select,
SelectItem
SelectItem,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
@@ -39,6 +43,11 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void;
};
enum FormTab {
Configuration = "configuration",
Advanced = "advanced"
}
const schema = z
.object({
name: z.string().trim().min(1, "Name is required"),
@@ -64,7 +73,9 @@ const schema = z
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
})
}),
enableAutoRenewal: z.boolean().optional().default(false),
autoRenewalPeriodInDays: z.number().min(1).optional()
})
.required();
@@ -75,6 +86,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
const projectId = currentWorkspace.id;
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
const { data: cas } = useListCasByProjectId(projectId);
const [tabValue, setTabValue] = useState<FormTab>(FormTab.Configuration);
const { data: pkiSubscriber } = useGetPkiSubscriber({
subscriberName:
@@ -104,12 +116,15 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
},
extendedKeyUsages: {}
extendedKeyUsages: {},
enableAutoRenewal: false,
autoRenewalPeriodInDays: 7
}
});
const selectedCaId = watch("caId");
const selectedCa = cas?.find((ca) => ca.id === selectedCaId);
const selectedAutoRenewalState = watch("enableAutoRenewal");
useEffect(() => {
if (pkiSubscriber) {
@@ -122,7 +137,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])),
extendedKeyUsages: Object.fromEntries(
(pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true])
)
),
enableAutoRenewal: pkiSubscriber.enableAutoRenewal || false,
autoRenewalPeriodInDays: pkiSubscriber.autoRenewalPeriodInDays || 7
});
} else {
reset({
@@ -135,7 +152,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true
},
extendedKeyUsages: {}
extendedKeyUsages: {},
enableAutoRenewal: false,
autoRenewalPeriodInDays: 7
});
}
}, [pkiSubscriber, reset]);
@@ -153,7 +172,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
subjectAlternativeNames,
ttl,
keyUsages,
extendedKeyUsages
extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
}: FormData) => {
try {
if (!projectId) return;
@@ -201,7 +222,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
subjectAlternativeNames: subjectAlternativeNamesList,
ttl,
keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList
extendedKeyUsages: extendedKeyUsagesList,
enableAutoRenewal,
autoRenewalPeriodInDays
});
} else {
await createMutateAsync({
@@ -212,7 +235,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
subjectAlternativeNames: subjectAlternativeNamesList,
ttl,
keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList
extendedKeyUsages: extendedKeyUsagesList,
enableAutoRenewal,
autoRenewalPeriodInDays
});
}
@@ -241,178 +266,245 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
}}
>
<ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
{pkiSubscriber && (
<FormControl label="Subscriber ID">
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subscriber Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="web-service" />
</FormControl>
)}
/>
<Controller
control={control}
name="caId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || []).map(({ id, name, type, configuration }) => {
const displayName =
type === CaType.INTERNAL ? `${name} (${configuration.dn})` : name;
return (
<SelectItem value={id} key={`ca-${id}`}>
{displayName}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="web.example.com" />
</FormControl>
)}
/>
<Controller
control={control}
name="subjectAlternativeNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject Alternative Names (SANs)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
</FormControl>
)}
/>
{selectedCa?.type !== CaType.ACME && (
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["name", "caId", "commonName", "subjectAlternativeNames", "ttl"].includes(
Object.keys(fields)[0]
)
? FormTab.Configuration
: FormTab.Advanced
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as FormTab)}>
<TabList>
<Tab value={FormTab.Configuration}>Configuration</Tab>
<Tab value={FormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={FormTab.Configuration}>
{pkiSubscriber && (
<FormControl label="Subscriber ID">
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
/>
)}
{selectedCa?.type !== CaType.ACME && (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Key Usage</div>
</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="keyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
<Controller
control={control}
name="extendedKeyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Extended Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subscriber Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="web-service" />
</FormControl>
)}
/>
<Controller
control={control}
name="caId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Issuing CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || []).map(({ id, name, type, configuration }) => {
const displayName =
type === CaType.INTERNAL ? `${name} (${configuration.dn})` : name;
return (
<SelectItem value={id} key={`ca-${id}`}>
{displayName}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="commonName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Common Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="web.example.com" />
</FormControl>
)}
/>
<Controller
control={control}
name="subjectAlternativeNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject Alternative Names (SANs)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
</FormControl>
)}
/>
{selectedCa?.type !== CaType.ACME && (
<Controller
control={control}
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
</FormControl>
)}
/>
)}
{selectedCa?.type !== CaType.ACME && (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Key Usage</div>
</AccordionTrigger>
<AccordionContent>
<Controller
control={control}
name="keyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</div>
</FormControl>
);
}}
/>
<Controller
control={control}
name="extendedKeyUsages"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Extended Key Usage"
errorText={error?.message}
isError={Boolean(error)}
>
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{EXTENDED_KEY_USAGES_OPTIONS.map(
({ label, value: optionValue }) => {
return (
<Checkbox
id={optionValue}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
}
)}
</div>
</FormControl>
);
}}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</TabPanel>
<TabPanel value={FormTab.Advanced}>
<Controller
control={control}
name="enableAutoRenewal"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
tooltipText="If enabled, a new certificate will be issued automatically X days before the current certificate expires."
>
<Checkbox
id="enableAutoRenewal"
isChecked={value}
onCheckedChange={onChange}
className="data-[state=checked]:bg-primary"
>
Enable Auto Renewal
</Checkbox>
</FormControl>
)}
/>
{selectedAutoRenewalState && (
<Controller
control={control}
name="autoRenewalPeriodInDays"
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl
label="Renew X days before expiry"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="A new certificate will be issued this many days before the current certificate expires."
>
<Input
value={value}
onChange={(e) => onChange(Number(e.target.value))}
type="number"
min="1"
step="1"
placeholder="7"
/>
</FormControl>
);
}}
/>
)}
</TabPanel>
</Tabs>
<div className="mt-4 flex items-center">
<Button
className="mr-4"