mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 05:27:48 +00:00
feat: added subscriber cert auto-renewal
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
+271
-179
@@ -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,178 +266,245 @@ 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
|
||||||
{pkiSubscriber && (
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
<FormControl label="Subscriber ID">
|
setTabValue(
|
||||||
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
|
["name", "caId", "commonName", "subjectAlternativeNames", "ttl"].includes(
|
||||||
</FormControl>
|
Object.keys(fields)[0]
|
||||||
)}
|
)
|
||||||
<Controller
|
? FormTab.Configuration
|
||||||
control={control}
|
: FormTab.Advanced
|
||||||
name="name"
|
);
|
||||||
render={({ field, fieldState: { error } }) => (
|
})}
|
||||||
<FormControl
|
>
|
||||||
label="Subscriber Name"
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as FormTab)}>
|
||||||
isError={Boolean(error)}
|
<TabList>
|
||||||
errorText={error?.message}
|
<Tab value={FormTab.Configuration}>Configuration</Tab>
|
||||||
isRequired
|
<Tab value={FormTab.Advanced}>Advanced</Tab>
|
||||||
>
|
</TabList>
|
||||||
<Input {...field} placeholder="web-service" />
|
<TabPanel value={FormTab.Configuration}>
|
||||||
</FormControl>
|
{pkiSubscriber && (
|
||||||
)}
|
<FormControl label="Subscriber ID">
|
||||||
/>
|
<Input value={pkiSubscriber.id} isDisabled className="bg-white/[0.07]" />
|
||||||
<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>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
<Controller
|
||||||
)}
|
control={control}
|
||||||
{selectedCa?.type !== CaType.ACME && (
|
name="name"
|
||||||
<Accordion type="single" collapsible className="w-full">
|
render={({ field, fieldState: { error } }) => (
|
||||||
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
<FormControl
|
||||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
label="Subscriber Name"
|
||||||
<div className="order-1 ml-3">Key Usage</div>
|
isError={Boolean(error)}
|
||||||
</AccordionTrigger>
|
errorText={error?.message}
|
||||||
<AccordionContent>
|
isRequired
|
||||||
<Controller
|
>
|
||||||
control={control}
|
<Input {...field} placeholder="web-service" />
|
||||||
name="keyUsages"
|
</FormControl>
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
)}
|
||||||
return (
|
/>
|
||||||
<FormControl
|
<Controller
|
||||||
label="Key Usage"
|
control={control}
|
||||||
errorText={error?.message}
|
name="caId"
|
||||||
isError={Boolean(error)}
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
>
|
<FormControl
|
||||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
label="Issuing CA"
|
||||||
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
errorText={error?.message}
|
||||||
return (
|
isError={Boolean(error)}
|
||||||
<Checkbox
|
isRequired
|
||||||
id={optionValue}
|
>
|
||||||
key={optionValue}
|
<Select
|
||||||
className="data-[state=checked]:bg-primary"
|
defaultValue={field.value}
|
||||||
isChecked={value[optionValue]}
|
{...field}
|
||||||
onCheckedChange={(state) => {
|
onValueChange={(e) => onChange(e)}
|
||||||
onChange({
|
className="w-full"
|
||||||
...value,
|
>
|
||||||
[optionValue]: state
|
{(cas || []).map(({ id, name, type, configuration }) => {
|
||||||
});
|
const displayName =
|
||||||
}}
|
type === CaType.INTERNAL ? `${name} (${configuration.dn})` : name;
|
||||||
>
|
|
||||||
{label}
|
return (
|
||||||
</Checkbox>
|
<SelectItem value={id} key={`ca-${id}`}>
|
||||||
);
|
{displayName}
|
||||||
})}
|
</SelectItem>
|
||||||
</div>
|
);
|
||||||
</FormControl>
|
})}
|
||||||
);
|
</Select>
|
||||||
}}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
<Controller
|
/>
|
||||||
control={control}
|
<Controller
|
||||||
name="extendedKeyUsages"
|
control={control}
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
name="commonName"
|
||||||
return (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Extended Key Usage"
|
label="Common Name"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
isError={Boolean(error)}
|
errorText={error?.message}
|
||||||
>
|
isRequired
|
||||||
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
>
|
||||||
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
<Input {...field} placeholder="web.example.com" />
|
||||||
return (
|
</FormControl>
|
||||||
<Checkbox
|
)}
|
||||||
id={optionValue}
|
/>
|
||||||
key={optionValue}
|
<Controller
|
||||||
className="data-[state=checked]:bg-primary"
|
control={control}
|
||||||
isChecked={value[optionValue]}
|
name="subjectAlternativeNames"
|
||||||
onCheckedChange={(state) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
onChange({
|
<FormControl
|
||||||
...value,
|
label="Subject Alternative Names (SANs)"
|
||||||
[optionValue]: state
|
isError={Boolean(error)}
|
||||||
});
|
errorText={error?.message}
|
||||||
}}
|
>
|
||||||
>
|
<Input {...field} placeholder="app1.example.com, app2.example.com, ..." />
|
||||||
{label}
|
</FormControl>
|
||||||
</Checkbox>
|
)}
|
||||||
);
|
/>
|
||||||
})}
|
{selectedCa?.type !== CaType.ACME && (
|
||||||
</div>
|
<Controller
|
||||||
</FormControl>
|
control={control}
|
||||||
);
|
name="ttl"
|
||||||
}}
|
render={({ field, fieldState: { error } }) => (
|
||||||
/>
|
<FormControl
|
||||||
</AccordionContent>
|
label="TTL"
|
||||||
</AccordionItem>
|
isError={Boolean(error)}
|
||||||
</Accordion>
|
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">
|
<div className="mt-4 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
Reference in New Issue
Block a user