diff --git a/backend/src/db/migrations/20250521110635_add-external-ca-pki.ts b/backend/src/db/migrations/20250521110635_add-external-ca-pki.ts index b4e076331..8f84da5e0 100644 --- a/backend/src/db/migrations/20250521110635_add-external-ca-pki.ts +++ b/backend/src/db/migrations/20250521110635_add-external-ca-pki.ts @@ -94,6 +94,11 @@ export async function up(knex: Knex): Promise { 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 { 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"); diff --git a/backend/src/db/schemas/pki-subscribers.ts b/backend/src/db/schemas/pki-subscribers.ts index e27fa0fd3..0cdff4250 100644 --- a/backend/src/db/schemas/pki-subscribers.ts +++ b/backend/src/db/schemas/pki-subscribers.ts @@ -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() diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 8182914e1..225eda4f2 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -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 diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1fe774508..52122230f 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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.", diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 8a0248f38..345c00278 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -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; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f3b265c7b..bec7fcae6 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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(); diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts index b667b5b72..b1b242ba1 100644 --- a/backend/src/server/routes/v1/pki-subscriber-router.ts +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -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 diff --git a/backend/src/services/pki-subscriber/pki-subscriber-queue.ts b/backend/src/services/pki-subscriber/pki-subscriber-queue.ts new file mode 100644 index 000000000..02bf06bb7 --- /dev/null +++ b/backend/src/services/pki-subscriber/pki-subscriber-queue.ts @@ -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; + certificateDAL: TCertificateDALFactory; + auditLogService: Pick; +}; + +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 + }; +}; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts index 5a0eefa57..337f81d8c 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts @@ -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() }); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index eb7fdbdb8..795371c76 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -78,6 +78,7 @@ type TPkiSubscriberServiceFactoryDep = { projectDAL: Pick; kmsService: Pick; permissionService: Pick; + internalCaFns: ReturnType; }; export type TPkiSubscriberServiceFactory = ReturnType; @@ -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; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts index 050c520ad..6881eea74 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-types.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -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 = { diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts index 6d04929e8..628cfec0a 100644 --- a/frontend/src/hooks/api/pkiSubscriber/types.ts +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -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 = { diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx index c2c193e73..f616c311a 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -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.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) => { }} > -
- {pkiSubscriber && ( - - - - )} - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - {selectedCa?.type !== CaType.ACME && ( - ( - - + { + setTabValue( + ["name", "caId", "commonName", "subjectAlternativeNames", "ttl"].includes( + Object.keys(fields)[0] + ) + ? FormTab.Configuration + : FormTab.Advanced + ); + })} + > + setTabValue(value as FormTab)}> + + Configuration + Advanced + + + {pkiSubscriber && ( + + )} - /> - )} - {selectedCa?.type !== CaType.ACME && ( - - - -
Key Usage
-
- - { - return ( - -
- {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} -
-
- ); - }} - /> - { - return ( - -
- {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} -
-
- ); - }} - /> -
-
-
- )} + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {selectedCa?.type !== CaType.ACME && ( + ( + + + + )} + /> + )} + {selectedCa?.type !== CaType.ACME && ( + + + +
Key Usage
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map( + ({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + } + )} +
+
+ ); + }} + /> +
+
+
+ )} +
+ + ( + + + Enable Auto Renewal + + + )} + /> + {selectedAutoRenewalState && ( + { + return ( + + onChange(Number(e.target.value))} + type="number" + min="1" + step="1" + placeholder="7" + /> + + ); + }} + /> + )} + +