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
@@ -94,6 +94,11 @@ export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.PkiSubscriber)) { if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.string("ttl").nullable().alter(); t.string("ttl").nullable().alter();
t.boolean("enableAutoRenewal").notNullable().defaultTo(false);
t.integer("autoRenewalPeriodInDays");
t.datetime("lastAutoRenewAt");
t.string("lastOperationStatus"); t.string("lastOperationStatus");
t.text("lastOperationMessage"); t.text("lastOperationMessage");
t.dateTime("lastOperationAt"); t.dateTime("lastOperationAt");
@@ -188,6 +193,10 @@ export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.PkiSubscriber)) { if (await knex.schema.hasTable(TableName.PkiSubscriber)) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.dropColumn("enableAutoRenewal");
t.dropColumn("autoRenewalPeriodInDays");
t.dropColumn("lastAutoRenewAt");
t.dropColumn("lastOperationStatus"); t.dropColumn("lastOperationStatus");
t.dropColumn("lastOperationMessage"); t.dropColumn("lastOperationMessage");
t.dropColumn("lastOperationAt"); t.dropColumn("lastOperationAt");
@@ -20,6 +20,9 @@ export const PkiSubscribersSchema = z.object({
keyUsages: z.string().array(), keyUsages: z.string().array(),
extendedKeyUsages: z.string().array(), extendedKeyUsages: z.string().array(),
status: z.string(), status: z.string(),
enableAutoRenewal: z.boolean().default(false),
autoRenewalPeriodInDays: z.number().nullable().optional(),
lastAutoRenewAt: z.date().nullable().optional(),
lastOperationStatus: z.string().nullable().optional(), lastOperationStatus: z.string().nullable().optional(),
lastOperationMessage: z.string().nullable().optional(), lastOperationMessage: z.string().nullable().optional(),
lastOperationAt: z.date().nullable().optional() lastOperationAt: z.date().nullable().optional()
@@ -268,6 +268,7 @@ export enum EventType {
GET_PKI_SUBSCRIBER = "get-pki-subscriber", GET_PKI_SUBSCRIBER = "get-pki-subscriber",
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert", ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-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", LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle", GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
CREATE_KMS = "create-kms", 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 { interface SignPkiSubscriberCert {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT; type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
metadata: { metadata: {
@@ -3103,6 +3112,7 @@ export type Event =
| GetPkiSubscriber | GetPkiSubscriber
| IssuePkiSubscriberCert | IssuePkiSubscriberCert
| SignPkiSubscriberCert | SignPkiSubscriberCert
| AutomatedRenewPkiSubscriberCert
| ListPkiSubscriberCerts | ListPkiSubscriberCerts
| GetSubscriberActiveCertBundle | GetSubscriberActiveCertBundle
| CreateKmsEvent | CreateKmsEvent
+6 -2
View File
@@ -1811,7 +1811,9 @@ export const PKI_SUBSCRIBERS = {
subjectAlternativeNames: 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.", "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.", 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: { UPDATE: {
projectId: "The ID of the project to update the PKI subscriber in.", 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.", "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.", keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.",
extendedKeyUsages: 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: { DELETE: {
subscriberName: "The name of the PKI subscriber to delete.", subscriberName: "The name of the PKI subscriber to delete.",
+7 -1
View File
@@ -37,6 +37,7 @@ export enum QueueName {
AuditLogPrune = "audit-log-prune", AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup", DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSubscriber = "pki-subscriber",
TelemetryInstanceStats = "telemtry-self-hosted-stats", TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations", IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook", SecretWebhook = "secret-webhook",
@@ -87,7 +88,8 @@ export enum QueueJobs {
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
InvalidateCache = "invalidate-cache", InvalidateCache = "invalidate-cache",
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber" CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@@ -255,6 +257,10 @@ export type TQueueJobTypes = {
caType: CaType; caType: CaType;
}; };
}; };
[QueueName.PkiSubscriber]: {
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
payload: undefined;
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
+27 -1
View File
@@ -134,6 +134,7 @@ import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-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 { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-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 { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; 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 { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { projectDALFactory } from "@app/services/project/project-dal"; import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectQueueFactory } from "@app/services/project/project-queue";
@@ -1700,6 +1702,28 @@ export const registerRoutes = async (
pkiSubscriberDAL 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({ const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL, pkiSubscriberDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -1712,7 +1736,8 @@ export const registerRoutes = async (
projectDAL, projectDAL,
kmsService, kmsService,
permissionService, permissionService,
certificateAuthorityQueue certificateAuthorityQueue,
internalCaFns
}); });
await secretRotationV2QueueServiceFactory({ await secretRotationV2QueueServiceFactory({
@@ -1735,6 +1760,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp(); await dailyResourceCleanUp.startCleanUp();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await dailyExpiringPkiItemAlert.startSendingAlerts();
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await kmsService.startService(); await kmsService.startService();
await microsoftTeamsService.start(); await microsoftTeamsService.start();
@@ -110,7 +110,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.array() .array()
.default([]) .default([])
.transform((arr) => Array.from(new Set(arr))) .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: { response: {
200: sanitizedPkiSubscriber 200: sanitizedPkiSubscriber
@@ -195,7 +197,9 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.array() .array()
.transform((arr) => Array.from(new Set(arr))) .transform((arr) => Array.from(new Set(arr)))
.optional() .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: { response: {
200: sanitizedPkiSubscriber 200: sanitizedPkiSubscriber
@@ -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
};
};
@@ -15,7 +15,10 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
extendedKeyUsages: true, extendedKeyUsages: true,
lastOperationStatus: true, lastOperationStatus: true,
lastOperationMessage: true, lastOperationMessage: true,
lastOperationAt: true lastOperationAt: true,
enableAutoRenewal: true,
autoRenewalPeriodInDays: true,
lastAutoRenewAt: true
}).extend({ }).extend({
supportsImmediateCertIssuance: z.boolean().optional() supportsImmediateCertIssuance: z.boolean().optional()
}); });
@@ -78,6 +78,7 @@ type TPkiSubscriberServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
internalCaFns: ReturnType<typeof InternalCertificateAuthorityFns>;
}; };
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>; export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
@@ -94,20 +95,9 @@ export const pkiSubscriberServiceFactory = ({
projectDAL, projectDAL,
kmsService, kmsService,
permissionService, permissionService,
certificateAuthorityQueue certificateAuthorityQueue,
internalCaFns
}: TPkiSubscriberServiceFactoryDep) => { }: TPkiSubscriberServiceFactoryDep) => {
const internalCaFns = InternalCertificateAuthorityFns({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService
});
const createSubscriber = async ({ const createSubscriber = async ({
name, name,
commonName, commonName,
@@ -117,6 +107,8 @@ export const pkiSubscriberServiceFactory = ({
subjectAlternativeNames, subjectAlternativeNames,
keyUsages, keyUsages,
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays,
projectId, projectId,
actorId, actorId,
actorAuthMethod, 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({ const newSubscriber = await pkiSubscriberDAL.create({
caId, caId,
projectId, projectId,
@@ -148,7 +146,9 @@ export const pkiSubscriberServiceFactory = ({
ttl, ttl,
subjectAlternativeNames, subjectAlternativeNames,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
}); });
return newSubscriber; return newSubscriber;
@@ -210,6 +210,8 @@ export const pkiSubscriberServiceFactory = ({
subjectAlternativeNames, subjectAlternativeNames,
keyUsages, keyUsages,
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, 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, { const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, {
caId, caId,
name, name,
@@ -245,7 +253,9 @@ export const pkiSubscriberServiceFactory = ({
ttl, ttl,
subjectAlternativeNames, subjectAlternativeNames,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
}); });
return updatedSubscriber; return updatedSubscriber;
@@ -16,6 +16,8 @@ export type TCreatePkiSubscriberDTO = {
subjectAlternativeNames: string[]; subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[]; keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
} & TProjectPermission; } & TProjectPermission;
export type TGetPkiSubscriberDTO = { export type TGetPkiSubscriberDTO = {
@@ -32,6 +34,8 @@ export type TUpdatePkiSubscriberDTO = {
subjectAlternativeNames?: string[]; subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
} & TProjectPermission; } & TProjectPermission;
export type TDeletePkiSubscriberDTO = { export type TDeletePkiSubscriberDTO = {
@@ -22,6 +22,8 @@ export type TPkiSubscriber = {
keyUsages: CertKeyUsage[]; keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
supportsImmediateCertIssuance?: boolean; supportsImmediateCertIssuance?: boolean;
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
lastOperationStatus?: SubscriberOperationStatus; lastOperationStatus?: SubscriberOperationStatus;
lastOperationMessage?: string; lastOperationMessage?: string;
lastOperationAt?: string; lastOperationAt?: string;
@@ -36,6 +38,8 @@ export type TCreatePkiSubscriberDTO = {
subjectAlternativeNames: string[]; subjectAlternativeNames: string[];
keyUsages: CertKeyUsage[]; keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
}; };
export type TUpdatePkiSubscriberDTO = { export type TUpdatePkiSubscriberDTO = {
@@ -49,6 +53,8 @@ export type TUpdatePkiSubscriberDTO = {
subjectAlternativeNames?: string[]; subjectAlternativeNames?: string[];
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number;
}; };
export type TDeletePkiSubscriberDTO = { export type TDeletePkiSubscriberDTO = {
@@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@@ -16,7 +16,11 @@ import {
Modal, Modal,
ModalContent, ModalContent,
Select, Select,
SelectItem SelectItem,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { import {
@@ -39,6 +43,11 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiSubscriber"]>, state?: boolean) => void;
}; };
enum FormTab {
Configuration = "configuration",
Advanced = "advanced"
}
const schema = z const schema = z
.object({ .object({
name: z.string().trim().min(1, "Name is required"), name: z.string().trim().min(1, "Name is required"),
@@ -64,7 +73,9 @@ const schema = z
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(), [CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(), [CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional() [CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
}) }),
enableAutoRenewal: z.boolean().optional().default(false),
autoRenewalPeriodInDays: z.number().min(1).optional()
}) })
.required(); .required();
@@ -75,6 +86,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
const projectId = currentWorkspace.id; const projectId = currentWorkspace.id;
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId); const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
const { data: cas } = useListCasByProjectId(projectId); const { data: cas } = useListCasByProjectId(projectId);
const [tabValue, setTabValue] = useState<FormTab>(FormTab.Configuration);
const { data: pkiSubscriber } = useGetPkiSubscriber({ const { data: pkiSubscriber } = useGetPkiSubscriber({
subscriberName: subscriberName:
@@ -104,12 +116,15 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
[CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true [CertKeyUsage.KEY_ENCIPHERMENT]: true
}, },
extendedKeyUsages: {} extendedKeyUsages: {},
enableAutoRenewal: false,
autoRenewalPeriodInDays: 7
} }
}); });
const selectedCaId = watch("caId"); const selectedCaId = watch("caId");
const selectedCa = cas?.find((ca) => ca.id === selectedCaId); const selectedCa = cas?.find((ca) => ca.id === selectedCaId);
const selectedAutoRenewalState = watch("enableAutoRenewal");
useEffect(() => { useEffect(() => {
if (pkiSubscriber) { if (pkiSubscriber) {
@@ -122,7 +137,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])), keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])),
extendedKeyUsages: Object.fromEntries( extendedKeyUsages: Object.fromEntries(
(pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true]) (pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true])
) ),
enableAutoRenewal: pkiSubscriber.enableAutoRenewal || false,
autoRenewalPeriodInDays: pkiSubscriber.autoRenewalPeriodInDays || 7
}); });
} else { } else {
reset({ reset({
@@ -135,7 +152,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
[CertKeyUsage.DIGITAL_SIGNATURE]: true, [CertKeyUsage.DIGITAL_SIGNATURE]: true,
[CertKeyUsage.KEY_ENCIPHERMENT]: true [CertKeyUsage.KEY_ENCIPHERMENT]: true
}, },
extendedKeyUsages: {} extendedKeyUsages: {},
enableAutoRenewal: false,
autoRenewalPeriodInDays: 7
}); });
} }
}, [pkiSubscriber, reset]); }, [pkiSubscriber, reset]);
@@ -153,7 +172,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
subjectAlternativeNames, subjectAlternativeNames,
ttl, ttl,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
enableAutoRenewal,
autoRenewalPeriodInDays
}: FormData) => { }: FormData) => {
try { try {
if (!projectId) return; if (!projectId) return;
@@ -201,7 +222,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
subjectAlternativeNames: subjectAlternativeNamesList, subjectAlternativeNames: subjectAlternativeNamesList,
ttl, ttl,
keyUsages: keyUsagesList, keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList extendedKeyUsages: extendedKeyUsagesList,
enableAutoRenewal,
autoRenewalPeriodInDays
}); });
} else { } else {
await createMutateAsync({ await createMutateAsync({
@@ -212,7 +235,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
subjectAlternativeNames: subjectAlternativeNamesList, subjectAlternativeNames: subjectAlternativeNamesList,
ttl, ttl,
keyUsages: keyUsagesList, keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList extendedKeyUsages: extendedKeyUsagesList,
enableAutoRenewal,
autoRenewalPeriodInDays
}); });
} }
@@ -241,7 +266,23 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
}} }}
> >
<ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}> <ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}>
<form onSubmit={handleSubmit(onFormSubmit)}> <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 && ( {pkiSubscriber && (
<FormControl label="Subscriber ID"> <FormControl label="Subscriber ID">
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" /> <Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
@@ -386,7 +427,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)} isError={Boolean(error)}
> >
<div className="mb-7 mt-2 grid grid-cols-2 gap-2"> <div className="mb-7 mt-2 grid grid-cols-2 gap-2">
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { {EXTENDED_KEY_USAGES_OPTIONS.map(
({ label, value: optionValue }) => {
return ( return (
<Checkbox <Checkbox
id={optionValue} id={optionValue}
@@ -403,7 +445,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
{label} {label}
</Checkbox> </Checkbox>
); );
})} }
)}
</div> </div>
</FormControl> </FormControl>
); );
@@ -413,6 +456,55 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
</AccordionItem> </AccordionItem>
</Accordion> </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"> <div className="mt-4 flex items-center">
<Button <Button
className="mr-4" className="mr-4"