mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 09:28:06 +00:00
Merge pull request #4972 from Infisical/feat/PKI-55
feature(pki): add external CA support on PKI
This commit is contained in:
@@ -57,3 +57,4 @@ docs/documentation/platform/pki/enrollment-methods/api.mdx:generic-api-key:93
|
|||||||
docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139
|
docs/documentation/platform/pki/enrollment-methods/api.mdx:private-key:139
|
||||||
docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62
|
docs/documentation/platform/pki/certificate-syncs/aws-secrets-manager.mdx:private-key:62
|
||||||
docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61
|
docs/documentation/platform/pki/certificate-syncs/chef.mdx:private-key:61
|
||||||
|
backend/src/services/certificate-request/certificate-request-service.test.ts:private-key:246
|
||||||
Vendored
+2
@@ -65,6 +65,7 @@ import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-a
|
|||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
||||||
import { TCertificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
import { TCertificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
||||||
|
import { TCertificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service";
|
||||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
||||||
@@ -288,6 +289,7 @@ declare module "fastify" {
|
|||||||
auditLogStream: TAuditLogStreamServiceFactory;
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
certificate: TCertificateServiceFactory;
|
certificate: TCertificateServiceFactory;
|
||||||
certificateV3: TCertificateV3ServiceFactory;
|
certificateV3: TCertificateV3ServiceFactory;
|
||||||
|
certificateRequest: TCertificateRequestServiceFactory;
|
||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
certificateTemplateV2: TCertificateTemplateV2ServiceFactory;
|
certificateTemplateV2: TCertificateTemplateV2ServiceFactory;
|
||||||
certificateProfile: TCertificateProfileServiceFactory;
|
certificateProfile: TCertificateProfileServiceFactory;
|
||||||
|
|||||||
Vendored
+10
@@ -573,6 +573,11 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
TCertificateRequests,
|
||||||
|
TCertificateRequestsInsert,
|
||||||
|
TCertificateRequestsUpdate
|
||||||
|
} from "@app/db/schemas/certificate-requests";
|
||||||
import {
|
import {
|
||||||
TAccessApprovalPoliciesEnvironments,
|
TAccessApprovalPoliciesEnvironments,
|
||||||
TAccessApprovalPoliciesEnvironmentsInsert,
|
TAccessApprovalPoliciesEnvironmentsInsert,
|
||||||
@@ -714,6 +719,11 @@ declare module "knex/types/tables" {
|
|||||||
TExternalCertificateAuthoritiesUpdate
|
TExternalCertificateAuthoritiesUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
|
||||||
|
[TableName.CertificateRequests]: KnexOriginal.CompositeTableType<
|
||||||
|
TCertificateRequests,
|
||||||
|
TCertificateRequestsInsert,
|
||||||
|
TCertificateRequestsUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateTemplates,
|
TCertificateTemplates,
|
||||||
TCertificateTemplatesInsert,
|
TCertificateTemplatesInsert,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateRequests))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateRequests, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("status").notNullable();
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("profileId").nullable();
|
||||||
|
t.foreign("profileId").references("id").inTable(TableName.PkiCertificateProfile).onDelete("SET NULL");
|
||||||
|
t.uuid("caId").nullable();
|
||||||
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL");
|
||||||
|
t.uuid("certificateId").nullable();
|
||||||
|
t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
|
||||||
|
t.text("csr").nullable();
|
||||||
|
t.string("commonName").nullable();
|
||||||
|
t.text("altNames").nullable();
|
||||||
|
t.specificType("keyUsages", "text[]").nullable();
|
||||||
|
t.specificType("extendedKeyUsages", "text[]").nullable();
|
||||||
|
t.datetime("notBefore").nullable();
|
||||||
|
t.datetime("notAfter").nullable();
|
||||||
|
t.string("keyAlgorithm").nullable();
|
||||||
|
t.string("signatureAlgorithm").nullable();
|
||||||
|
t.text("errorMessage").nullable();
|
||||||
|
t.text("metadata").nullable();
|
||||||
|
|
||||||
|
t.index(["projectId"]);
|
||||||
|
t.index(["status"]);
|
||||||
|
t.index(["profileId"]);
|
||||||
|
t.index(["caId"]);
|
||||||
|
t.index(["certificateId"]);
|
||||||
|
t.index(["createdAt"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateRequests);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateRequests);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasExternalConfigs = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "externalConfigs");
|
||||||
|
if (!hasExternalConfigs) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.text("externalConfigs").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasExternalConfigs = await knex.schema.hasColumn(TableName.PkiCertificateProfile, "externalConfigs");
|
||||||
|
if (hasExternalConfigs) {
|
||||||
|
await knex.schema.alterTable(TableName.PkiCertificateProfile, (t) => {
|
||||||
|
t.dropColumn("externalConfigs");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateRequestsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
status: z.string(),
|
||||||
|
projectId: z.string(),
|
||||||
|
profileId: z.string().uuid().nullable().optional(),
|
||||||
|
caId: z.string().uuid().nullable().optional(),
|
||||||
|
certificateId: z.string().uuid().nullable().optional(),
|
||||||
|
csr: z.string().nullable().optional(),
|
||||||
|
commonName: z.string().nullable().optional(),
|
||||||
|
altNames: z.string().nullable().optional(),
|
||||||
|
keyUsages: z.string().array().nullable().optional(),
|
||||||
|
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||||
|
notBefore: z.date().nullable().optional(),
|
||||||
|
notAfter: z.date().nullable().optional(),
|
||||||
|
keyAlgorithm: z.string().nullable().optional(),
|
||||||
|
signatureAlgorithm: z.string().nullable().optional(),
|
||||||
|
errorMessage: z.string().nullable().optional(),
|
||||||
|
metadata: z.string().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateRequests = z.infer<typeof CertificateRequestsSchema>;
|
||||||
|
export type TCertificateRequestsInsert = Omit<z.input<typeof CertificateRequestsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateRequestsUpdate = Partial<Omit<z.input<typeof CertificateRequestsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -16,6 +16,7 @@ export * from "./certificate-authority-certs";
|
|||||||
export * from "./certificate-authority-crl";
|
export * from "./certificate-authority-crl";
|
||||||
export * from "./certificate-authority-secret";
|
export * from "./certificate-authority-secret";
|
||||||
export * from "./certificate-bodies";
|
export * from "./certificate-bodies";
|
||||||
|
export * from "./certificate-requests";
|
||||||
export * from "./certificate-secrets";
|
export * from "./certificate-secrets";
|
||||||
export * from "./certificate-syncs";
|
export * from "./certificate-syncs";
|
||||||
export * from "./certificate-template-est-configs";
|
export * from "./certificate-template-est-configs";
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum TableName {
|
|||||||
CertificateAuthorityCrl = "certificate_authority_crl",
|
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||||
Certificate = "certificates",
|
Certificate = "certificates",
|
||||||
CertificateBody = "certificate_bodies",
|
CertificateBody = "certificate_bodies",
|
||||||
|
CertificateRequests = "certificate_requests",
|
||||||
CertificateSecret = "certificate_secrets",
|
CertificateSecret = "certificate_secrets",
|
||||||
CertificateTemplate = "certificate_templates",
|
CertificateTemplate = "certificate_templates",
|
||||||
PkiCertificateTemplateV2 = "pki_certificate_templates_v2",
|
PkiCertificateTemplateV2 = "pki_certificate_templates_v2",
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export const PkiCertificateProfilesSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
acmeConfigId: z.string().uuid().nullable().optional(),
|
acmeConfigId: z.string().uuid().nullable().optional(),
|
||||||
issuerType: z.string().default("ca")
|
issuerType: z.string().default("ca"),
|
||||||
|
externalConfigs: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
|
export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
|
||||||
|
|||||||
@@ -388,6 +388,9 @@ export enum EventType {
|
|||||||
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
|
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
|
||||||
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
|
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
|
||||||
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
|
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
|
||||||
|
CREATE_CERTIFICATE_REQUEST = "create-certificate-request",
|
||||||
|
GET_CERTIFICATE_REQUEST = "get-certificate-request",
|
||||||
|
GET_CERTIFICATE_FROM_REQUEST = "get-certificate-from-request",
|
||||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
@@ -2846,7 +2849,6 @@ interface OrderCertificateFromProfile {
|
|||||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
|
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
orderId: string;
|
|
||||||
profileName: string;
|
profileName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -4196,6 +4198,31 @@ interface DisableCertificateRenewalConfigEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateCertificateRequestEvent {
|
||||||
|
type: EventType.CREATE_CERTIFICATE_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
certificateRequestId: string;
|
||||||
|
profileId?: string;
|
||||||
|
caId?: string;
|
||||||
|
commonName?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertificateRequestEvent {
|
||||||
|
type: EventType.GET_CERTIFICATE_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
certificateRequestId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetCertificateFromRequestEvent {
|
||||||
|
type: EventType.GET_CERTIFICATE_FROM_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
certificateRequestId: string;
|
||||||
|
certificateId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| CreateSubOrganizationEvent
|
| CreateSubOrganizationEvent
|
||||||
| UpdateSubOrganizationEvent
|
| UpdateSubOrganizationEvent
|
||||||
@@ -4575,6 +4602,9 @@ export type Event =
|
|||||||
| PamResourceDeleteEvent
|
| PamResourceDeleteEvent
|
||||||
| UpdateCertificateRenewalConfigEvent
|
| UpdateCertificateRenewalConfigEvent
|
||||||
| DisableCertificateRenewalConfigEvent
|
| DisableCertificateRenewalConfigEvent
|
||||||
|
| CreateCertificateRequestEvent
|
||||||
|
| GetCertificateRequestEvent
|
||||||
|
| GetCertificateFromRequestEvent
|
||||||
| AutomatedRenewCertificate
|
| AutomatedRenewCertificate
|
||||||
| AutomatedRenewCertificateFailed
|
| AutomatedRenewCertificateFailed
|
||||||
| UserLoginEvent
|
| UserLoginEvent
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ import {
|
|||||||
type TPkiAcmeServiceFactoryDep = {
|
type TPkiAcmeServiceFactoryDep = {
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export enum QueueName {
|
|||||||
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
|
DynamicSecretLeaseRevocationFailedEmail = "dynamic-secret-lease-revocation-failed-email",
|
||||||
CaCrlRotation = "ca-crl-rotation",
|
CaCrlRotation = "ca-crl-rotation",
|
||||||
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
||||||
|
CertificateIssuance = "certificate-issuance",
|
||||||
SecretReplication = "secret-replication",
|
SecretReplication = "secret-replication",
|
||||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||||
PkiSync = "pki-sync",
|
PkiSync = "pki-sync",
|
||||||
@@ -128,6 +129,7 @@ export enum QueueJobs {
|
|||||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
||||||
SecretScanningV2SendNotification = "secret-scanning-v2-notification",
|
SecretScanningV2SendNotification = "secret-scanning-v2-notification",
|
||||||
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
|
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
|
||||||
|
CaIssueCertificateFromProfile = "ca-issue-certificate-from-profile",
|
||||||
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal",
|
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal",
|
||||||
TelemetryAggregatedEvents = "telemetry-aggregated-events",
|
TelemetryAggregatedEvents = "telemetry-aggregated-events",
|
||||||
DailyReminders = "daily-reminders",
|
DailyReminders = "daily-reminders",
|
||||||
@@ -355,6 +357,21 @@ export type TQueueJobTypes = {
|
|||||||
caType: CaType;
|
caType: CaType;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
[QueueName.CertificateIssuance]: {
|
||||||
|
name: QueueJobs.CaIssueCertificateFromProfile;
|
||||||
|
payload: {
|
||||||
|
certificateId: string;
|
||||||
|
profileId: string;
|
||||||
|
caId: string;
|
||||||
|
commonName?: string;
|
||||||
|
altNames?: string[];
|
||||||
|
ttl: string;
|
||||||
|
signatureAlgorithm: string;
|
||||||
|
keyAlgorithm: string;
|
||||||
|
keyUsages?: string[];
|
||||||
|
extendedKeyUsages?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
[QueueName.DailyReminders]: {
|
[QueueName.DailyReminders]: {
|
||||||
name: QueueJobs.DailyReminders;
|
name: QueueJobs.DailyReminders;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
|||||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
|
import { certificateIssuanceQueueFactory } from "@app/services/certificate-authority/certificate-issuance-queue";
|
||||||
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 { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns";
|
||||||
@@ -181,6 +182,8 @@ import { internalCertificateAuthorityServiceFactory } from "@app/services/certif
|
|||||||
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
||||||
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
||||||
|
import { certificateRequestDALFactory } from "@app/services/certificate-request/certificate-request-dal";
|
||||||
|
import { certificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service";
|
||||||
import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
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";
|
||||||
@@ -1093,6 +1096,7 @@ export const registerRoutes = async (
|
|||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||||
|
const certificateRequestDAL = certificateRequestDALFactory(db);
|
||||||
const certificateSyncDAL = certificateSyncDALFactory(db);
|
const certificateSyncDAL = certificateSyncDALFactory(db);
|
||||||
|
|
||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
@@ -1188,7 +1192,7 @@ export const registerRoutes = async (
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
externalCertificateAuthorityDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
@@ -2216,6 +2220,31 @@ export const registerRoutes = async (
|
|||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateRequestService = certificateRequestServiceFactory({
|
||||||
|
certificateRequestDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateService,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateIssuanceQueue = certificateIssuanceQueueFactory({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
queueService,
|
||||||
|
pkiSubscriberDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL,
|
||||||
|
certificateRequestService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateV3Service = certificateV3ServiceFactory({
|
const certificateV3Service = certificateV3ServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
@@ -2230,7 +2259,9 @@ export const registerRoutes = async (
|
|||||||
pkiSyncQueue,
|
pkiSyncQueue,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
certificateBodyDAL
|
certificateBodyDAL,
|
||||||
|
certificateIssuanceQueue,
|
||||||
|
certificateRequestService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
@@ -2463,6 +2494,7 @@ export const registerRoutes = async (
|
|||||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||||
await pkiAlertV2Queue.init();
|
await pkiAlertV2Queue.init();
|
||||||
await certificateV3Queue.init();
|
await certificateV3Queue.init();
|
||||||
|
await certificateIssuanceQueue.initializeCertificateIssuanceQueue();
|
||||||
await microsoftTeamsService.start();
|
await microsoftTeamsService.start();
|
||||||
await dynamicSecretQueueService.init();
|
await dynamicSecretQueueService.init();
|
||||||
await eventBusService.init();
|
await eventBusService.init();
|
||||||
@@ -2528,6 +2560,7 @@ export const registerRoutes = async (
|
|||||||
auditLogStream: auditLogStreamService,
|
auditLogStream: auditLogStreamService,
|
||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
certificateV3: certificateV3Service,
|
certificateV3: certificateV3Service,
|
||||||
|
certificateRequest: certificateRequestService,
|
||||||
certificateEstV3: certificateEstV3Service,
|
certificateEstV3: certificateEstV3Service,
|
||||||
sshCertificateAuthority: sshCertificateAuthorityService,
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
sshCertificateTemplate: sshCertificateTemplateService,
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertStatus } from "@app/services/certificate/certificate-types";
|
import { CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
|
import { ExternalConfigUnionSchema } from "@app/services/certificate-profile/certificate-profile-external-config-schemas";
|
||||||
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
|
||||||
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -46,7 +47,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
acmeConfig: z.object({}).optional()
|
acmeConfig: z.object({}).optional(),
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -149,7 +151,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -204,6 +208,15 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfiles: PkiCertificateProfilesSchema.extend({
|
certificateProfiles: PkiCertificateProfilesSchema.extend({
|
||||||
|
certificateAuthority: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
isExternal: z.boolean().optional(),
|
||||||
|
externalType: z.string().nullable().optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
metrics: z
|
metrics: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string(),
|
profileId: z.string(),
|
||||||
@@ -234,7 +247,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
directoryUrl: z.string()
|
directoryUrl: z.string()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional(),
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
}).array(),
|
}).array(),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
@@ -280,12 +294,16 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
|
}).extend({
|
||||||
certificateAuthority: z
|
certificateAuthority: z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
name: z.string()
|
name: z.string(),
|
||||||
|
isExternal: z.boolean().optional(),
|
||||||
|
externalType: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
certificateTemplate: z
|
certificateTemplate: z
|
||||||
@@ -310,7 +328,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
renewBeforeDays: z.number().optional()
|
renewBeforeDays: z.number().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional(),
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -358,7 +377,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -412,7 +433,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional(),
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -434,7 +456,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -479,7 +503,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
|
externalConfigs: ExternalConfigUnionSchema
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,18 +5,14 @@ import { z } from "zod";
|
|||||||
import { CertificatesSchema } from "@app/db/schemas";
|
import { CertificatesSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
|
import { ApiDocsTags, CERTIFICATES } from "@app/lib/api-docs";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
import { addNoCacheHeaders } from "@app/server/lib/caching";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import {
|
import { CertKeyAlgorithm, CertSignatureAlgorithm, CrlReason } from "@app/services/certificate/certificate-types";
|
||||||
ACMESANType,
|
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
CertificateOrderStatus,
|
|
||||||
CertKeyAlgorithm,
|
|
||||||
CertSignatureAlgorithm,
|
|
||||||
CrlReason
|
|
||||||
} from "@app/services/certificate/certificate-types";
|
|
||||||
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
@@ -26,10 +22,23 @@ import {
|
|||||||
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
import { TCertificateFromProfileResponse } from "@app/services/certificate-v3/certificate-v3-types";
|
||||||
|
|
||||||
import { booleanSchema } from "../sanitizedSchemas";
|
import { booleanSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
|
type CertificateServiceResponse = TCertificateFromProfileResponse | Omit<TCertificateFromProfileResponse, "privateKey">;
|
||||||
|
|
||||||
|
const extractCertificateData = (data: CertificateServiceResponse) => ({
|
||||||
|
certificate: data.certificate,
|
||||||
|
issuingCaCertificate: data.issuingCaCertificate,
|
||||||
|
certificateChain: data.certificateChain,
|
||||||
|
privateKey: "privateKey" in data ? data.privateKey : undefined,
|
||||||
|
serialNumber: data.serialNumber,
|
||||||
|
certificateId: data.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
interface CertificateRequestForService {
|
interface CertificateRequestForService {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
keyUsages?: CertKeyUsageType[];
|
keyUsages?: CertKeyUsageType[];
|
||||||
@@ -47,13 +56,32 @@ interface CertificateRequestForService {
|
|||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
|
const validateTtlAndDateFields = (data: {
|
||||||
|
attributes?: { notBefore?: string; notAfter?: string; ttl?: string };
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
ttl?: string;
|
||||||
|
}) => {
|
||||||
|
if (data.attributes) {
|
||||||
|
const hasDateFields = data.attributes.notBefore || data.attributes.notAfter;
|
||||||
|
const hasTtl = data.attributes.ttl;
|
||||||
|
return !(hasDateFields && hasTtl);
|
||||||
|
}
|
||||||
const hasDateFields = data.notBefore || data.notAfter;
|
const hasDateFields = data.notBefore || data.notAfter;
|
||||||
const hasTtl = data.ttl;
|
const hasTtl = data.ttl;
|
||||||
return !(hasDateFields && hasTtl);
|
return !(hasDateFields && hasTtl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
|
const validateDateOrder = (data: {
|
||||||
|
attributes?: { notBefore?: string; notAfter?: string };
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
}) => {
|
||||||
|
if (data.attributes?.notBefore && data.attributes?.notAfter) {
|
||||||
|
const notBefore = new Date(data.attributes.notBefore);
|
||||||
|
const notAfter = new Date(data.attributes.notAfter);
|
||||||
|
return notBefore < notAfter;
|
||||||
|
}
|
||||||
if (data.notBefore && data.notAfter) {
|
if (data.notBefore && data.notAfter) {
|
||||||
const notBefore = new Date(data.notBefore);
|
const notBefore = new Date(data.notBefore);
|
||||||
const notAfter = new Date(data.notAfter);
|
const notAfter = new Date(data.notAfter);
|
||||||
@@ -65,13 +93,279 @@ const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
|
|||||||
export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/issue-certificate",
|
url: "/",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
profileId: z.string().uuid(),
|
||||||
|
csr: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "CSR cannot be empty")
|
||||||
|
.max(4096, "CSR cannot exceed 4096 characters")
|
||||||
|
.optional(),
|
||||||
|
attributes: z
|
||||||
|
.object({
|
||||||
|
commonName: validateTemplateRegexField.optional(),
|
||||||
|
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||||
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||||
|
altNames: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||||
|
value: z.string().min(1, "SAN value cannot be empty")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||||
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional(),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "TTL cannot be empty")
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
|
notBefore: validateCaDateField.optional(),
|
||||||
|
notAfter: validateCaDateField.optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
removeRootsFromChain: booleanSchema.default(false).optional()
|
||||||
|
})
|
||||||
|
.refine(validateTtlAndDateFields, {
|
||||||
|
message:
|
||||||
|
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||||
|
})
|
||||||
|
.refine(validateDateOrder, {
|
||||||
|
message: "notBefore must be earlier than notAfter"
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z
|
||||||
|
.object({
|
||||||
|
certificate: z.string().trim(),
|
||||||
|
issuingCaCertificate: z.string().trim(),
|
||||||
|
certificateChain: z.string().trim(),
|
||||||
|
privateKey: z.string().trim().optional(),
|
||||||
|
serialNumber: z.string().trim(),
|
||||||
|
certificateId: z.string()
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { csr, attributes, ...requestBody } = req.body;
|
||||||
|
const profile = await server.services.certificateProfile.getProfileById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
profileId: requestBody.profileId
|
||||||
|
});
|
||||||
|
|
||||||
|
let useOrderFlow = false;
|
||||||
|
if (profile?.caId) {
|
||||||
|
const ca = await server.services.certificateAuthority.getCaById({
|
||||||
|
caId: profile.caId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
const caType = (ca?.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||||
|
useOrderFlow = caType !== CaType.INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useOrderFlow) {
|
||||||
|
const certificateOrderObject = {
|
||||||
|
altNames: attributes?.altNames || [],
|
||||||
|
validity: { ttl: attributes?.ttl || "" },
|
||||||
|
commonName: attributes?.commonName,
|
||||||
|
keyUsages: attributes?.keyUsages,
|
||||||
|
extendedKeyUsages: attributes?.extendedKeyUsages,
|
||||||
|
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
|
||||||
|
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: attributes?.signatureAlgorithm,
|
||||||
|
keyAlgorithm: attributes?.keyAlgorithm,
|
||||||
|
csr
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
profileId: requestBody.profileId,
|
||||||
|
certificateOrder: certificateOrderObject,
|
||||||
|
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: data.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||||
|
metadata: {
|
||||||
|
certificateProfileId: requestBody.profileId,
|
||||||
|
profileName: data.profileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: null,
|
||||||
|
certificateRequestId: data.certificateRequestId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (csr) {
|
||||||
|
const extractedCsrData = extractCertificateRequestFromCSR(csr);
|
||||||
|
|
||||||
|
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
profileId: requestBody.profileId,
|
||||||
|
csr,
|
||||||
|
validity: { ttl: attributes?.ttl || "" },
|
||||||
|
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
|
||||||
|
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
|
||||||
|
enrollmentType: EnrollmentType.API,
|
||||||
|
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: data.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
|
||||||
|
metadata: {
|
||||||
|
certificateProfileId: requestBody.profileId,
|
||||||
|
certificateId: data.certificateId,
|
||||||
|
profileName: data.profileName,
|
||||||
|
commonName: extractedCsrData.commonName || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
certificate: extractCertificateData(data),
|
||||||
|
certificateRequestId: data.certificateRequestId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateRequestForService: CertificateRequestForService = {
|
||||||
|
commonName: attributes?.commonName,
|
||||||
|
keyUsages: attributes?.keyUsages,
|
||||||
|
extendedKeyUsages: attributes?.extendedKeyUsages,
|
||||||
|
altNames: attributes?.altNames,
|
||||||
|
validity: { ttl: attributes?.ttl || "" },
|
||||||
|
notBefore: attributes?.notBefore ? new Date(attributes.notBefore) : undefined,
|
||||||
|
notAfter: attributes?.notAfter ? new Date(attributes.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: attributes?.signatureAlgorithm,
|
||||||
|
keyAlgorithm: attributes?.keyAlgorithm
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
|
||||||
|
|
||||||
|
const data = await server.services.certificateV3.issueCertificateFromProfile({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
profileId: requestBody.profileId,
|
||||||
|
certificateRequest: mappedCertificateRequest,
|
||||||
|
removeRootsFromChain: requestBody.removeRootsFromChain
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: data.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
|
||||||
|
metadata: {
|
||||||
|
certificateProfileId: requestBody.profileId,
|
||||||
|
certificateId: data.certificateId,
|
||||||
|
commonName: attributes?.commonName || "",
|
||||||
|
profileName: data.profileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
certificate: extractCertificateData(data),
|
||||||
|
certificateRequestId: data.certificateRequestId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/certificate-requests/:requestId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
params: z.object({
|
||||||
|
requestId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
query: z.object({
|
||||||
|
projectId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
status: z.nativeEnum(CertificateRequestStatus),
|
||||||
|
certificate: z.string().nullable(),
|
||||||
|
privateKey: z.string().nullable(),
|
||||||
|
serialNumber: z.string().nullable(),
|
||||||
|
errorMessage: z.string().nullable(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.certificateRequest.getCertificateFromRequest({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: (req.query as { projectId: string }).projectId,
|
||||||
|
certificateRequestId: req.params.requestId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: (req.query as { projectId: string }).projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_CERTIFICATE_REQUEST,
|
||||||
|
metadata: {
|
||||||
|
certificateRequestId: req.params.requestId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/issue-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "This endpoint will be removed in a future version.",
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
@@ -111,7 +405,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
certificateChain: z.string().trim(),
|
certificateChain: z.string().trim(),
|
||||||
privateKey: z.string().trim().optional(),
|
privateKey: z.string().trim().optional(),
|
||||||
serialNumber: z.string().trim(),
|
serialNumber: z.string().trim(),
|
||||||
certificateId: z.string()
|
certificateId: z.string(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,8 +463,10 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "This endpoint will be removed in a future version.",
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
@@ -196,14 +493,13 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
issuingCaCertificate: z.string().trim(),
|
issuingCaCertificate: z.string().trim(),
|
||||||
certificateChain: z.string().trim(),
|
certificateChain: z.string().trim(),
|
||||||
serialNumber: z.string().trim(),
|
serialNumber: z.string().trim(),
|
||||||
certificateId: z.string()
|
certificateId: z.string(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
|
||||||
|
|
||||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -220,6 +516,8 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
removeRootsFromChain: req.body.removeRootsFromChain
|
removeRootsFromChain: req.body.removeRootsFromChain
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: data.projectId,
|
projectId: data.projectId,
|
||||||
@@ -229,7 +527,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId,
|
certificateId: data.certificateId,
|
||||||
profileName: data.profileName,
|
profileName: data.profileName,
|
||||||
commonName: certificateRequest.commonName || ""
|
commonName: certificateRequestData.commonName || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -245,23 +543,23 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "This endpoint will be removed in a future version.",
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
subjectAlternativeNames: z
|
subjectAlternativeNames: z.array(
|
||||||
.array(
|
z.object({
|
||||||
z.object({
|
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||||
type: z.nativeEnum(ACMESANType),
|
value: z
|
||||||
value: z
|
.string()
|
||||||
.string()
|
.trim()
|
||||||
.trim()
|
.min(1, "SAN value cannot be empty")
|
||||||
.min(1, "SAN value cannot be empty")
|
.max(255, "SAN value must be less than 255 characters")
|
||||||
.max(255, "SAN value must be less than 255 characters")
|
})
|
||||||
})
|
),
|
||||||
)
|
|
||||||
.min(1, "At least one subject alternative name must be provided"),
|
|
||||||
ttl: z
|
ttl: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -285,59 +583,34 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
orderId: z.string(),
|
certificate: z.string().optional(),
|
||||||
status: z.nativeEnum(CertificateOrderStatus),
|
certificateRequestId: z.string()
|
||||||
subjectAlternativeNames: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.nativeEnum(ACMESANType),
|
|
||||||
value: z.string(),
|
|
||||||
status: z.nativeEnum(CertificateOrderStatus)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
authorizations: z.array(
|
|
||||||
z.object({
|
|
||||||
identifier: z.object({
|
|
||||||
type: z.nativeEnum(ACMESANType),
|
|
||||||
value: z.string()
|
|
||||||
}),
|
|
||||||
status: z.nativeEnum(CertificateOrderStatus),
|
|
||||||
expires: z.string().optional(),
|
|
||||||
challenges: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
status: z.nativeEnum(CertificateOrderStatus),
|
|
||||||
url: z.string(),
|
|
||||||
token: z.string()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
finalize: z.string(),
|
|
||||||
certificate: z.string().optional()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const certificateOrderObject = {
|
||||||
|
altNames: req.body.subjectAlternativeNames,
|
||||||
|
validity: {
|
||||||
|
ttl: req.body.ttl
|
||||||
|
},
|
||||||
|
commonName: req.body.commonName,
|
||||||
|
keyUsages: req.body.keyUsages,
|
||||||
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
|
||||||
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
profileId: req.body.profileId,
|
profileId: req.body.profileId,
|
||||||
certificateOrder: {
|
certificateOrder: certificateOrderObject,
|
||||||
altNames: req.body.subjectAlternativeNames,
|
|
||||||
validity: {
|
|
||||||
ttl: req.body.ttl
|
|
||||||
},
|
|
||||||
commonName: req.body.commonName,
|
|
||||||
keyUsages: req.body.keyUsages,
|
|
||||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
|
||||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
|
||||||
keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
},
|
|
||||||
removeRootsFromChain: req.body.removeRootsFromChain
|
removeRootsFromChain: req.body.removeRootsFromChain
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -348,7 +621,6 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
orderId: data.orderId,
|
|
||||||
profileName: data.profileName
|
profileName: data.profileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,12 +654,24 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => {
|
|||||||
certificateChain: z.string().trim(),
|
certificateChain: z.string().trim(),
|
||||||
privateKey: z.string().trim().optional(),
|
privateKey: z.string().trim().optional(),
|
||||||
serialNumber: z.string().trim(),
|
serialNumber: z.string().trim(),
|
||||||
certificateId: z.string()
|
certificateId: z.string(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const originalCertificate = await server.services.certificate.getCert({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
if (!originalCertificate) {
|
||||||
|
throw new NotFoundError({ message: "Original certificate not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const data = await server.services.certificateV3.renewCertificate({
|
const data = await server.services.certificateV3.renewCertificate({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -2,16 +2,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import {
|
import { CertKeyAlgorithm, CertSignatureAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
ACMESANType,
|
|
||||||
CertificateOrderStatus,
|
|
||||||
CertKeyAlgorithm,
|
|
||||||
CertSignatureAlgorithm
|
|
||||||
} from "@app/services/certificate/certificate-types";
|
|
||||||
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
@@ -21,6 +17,7 @@ import {
|
|||||||
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
import { booleanSchema } from "../sanitizedSchemas";
|
import { booleanSchema } from "../sanitizedSchemas";
|
||||||
@@ -65,8 +62,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "This endpoint will be removed in a future version.",
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
@@ -106,7 +105,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateChain: z.string().trim(),
|
certificateChain: z.string().trim(),
|
||||||
privateKey: z.string().trim().optional(),
|
privateKey: z.string().trim().optional(),
|
||||||
serialNumber: z.string().trim(),
|
serialNumber: z.string().trim(),
|
||||||
certificateId: z.string()
|
certificateId: z.string(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -138,6 +138,29 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
removeRootsFromChain: req.body.removeRootsFromChain
|
removeRootsFromChain: req.body.removeRootsFromChain
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: data.projectId,
|
||||||
|
profileId: req.body.profileId,
|
||||||
|
commonName: req.body.commonName,
|
||||||
|
altNames: req.body.altNames?.map((altName) => `${altName.type}:${altName.value}`).join(","),
|
||||||
|
keyUsages: req.body.keyUsages,
|
||||||
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.certificateRequest.attachCertificateToRequest({
|
||||||
|
certificateRequestId: certificateRequest.id,
|
||||||
|
certificateId: data.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: data.projectId,
|
projectId: data.projectId,
|
||||||
@@ -152,7 +175,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return {
|
||||||
|
...data,
|
||||||
|
certificateRequestId: certificateRequest.id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,8 +189,10 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "This endpoint will be removed in a future version.",
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
@@ -191,14 +219,13 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
issuingCaCertificate: z.string().trim(),
|
issuingCaCertificate: z.string().trim(),
|
||||||
certificateChain: z.string().trim(),
|
certificateChain: z.string().trim(),
|
||||||
serialNumber: z.string().trim(),
|
serialNumber: z.string().trim(),
|
||||||
certificateId: z.string()
|
certificateId: z.string(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
|
||||||
|
|
||||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -215,6 +242,32 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
removeRootsFromChain: req.body.removeRootsFromChain
|
removeRootsFromChain: req.body.removeRootsFromChain
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateRequestData = extractCertificateRequestFromCSR(req.body.csr);
|
||||||
|
|
||||||
|
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||||
|
actor: req.permission.type,
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: data.projectId,
|
||||||
|
profileId: req.body.profileId,
|
||||||
|
csr: req.body.csr,
|
||||||
|
commonName: certificateRequestData.commonName,
|
||||||
|
altNames: certificateRequestData.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","),
|
||||||
|
keyUsages: certificateRequestData.keyUsages,
|
||||||
|
extendedKeyUsages: certificateRequestData.extendedKeyUsages,
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
keyAlgorithm: certificateRequestData.keyAlgorithm,
|
||||||
|
signatureAlgorithm: certificateRequestData.signatureAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.certificateRequest.attachCertificateToRequest({
|
||||||
|
certificateRequestId: certificateRequest.id,
|
||||||
|
certificateId: data.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: data.projectId,
|
projectId: data.projectId,
|
||||||
@@ -224,12 +277,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId,
|
certificateId: data.certificateId,
|
||||||
profileName: data.profileName,
|
profileName: data.profileName,
|
||||||
commonName: certificateRequest.commonName || ""
|
commonName: certificateRequestData.commonName || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return {
|
||||||
|
...data,
|
||||||
|
certificateRequestId: certificateRequest.id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,23 +296,23 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
|
description: "This endpoint will be removed in a future version.",
|
||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
subjectAlternativeNames: z
|
subjectAlternativeNames: z.array(
|
||||||
.array(
|
z.object({
|
||||||
z.object({
|
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||||
type: z.nativeEnum(ACMESANType),
|
value: z
|
||||||
value: z
|
.string()
|
||||||
.string()
|
.trim()
|
||||||
.trim()
|
.min(1, "SAN value cannot be empty")
|
||||||
.min(1, "SAN value cannot be empty")
|
.max(255, "SAN value must be less than 255 characters")
|
||||||
.max(255, "SAN value must be less than 255 characters")
|
})
|
||||||
})
|
),
|
||||||
)
|
|
||||||
.min(1, "At least one subject alternative name must be provided"),
|
|
||||||
ttl: z
|
ttl: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -280,62 +336,55 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
orderId: z.string(),
|
certificate: z.string().optional(),
|
||||||
status: z.nativeEnum(CertificateOrderStatus),
|
certificateRequestId: z.string()
|
||||||
subjectAlternativeNames: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.nativeEnum(ACMESANType),
|
|
||||||
value: z.string(),
|
|
||||||
status: z.nativeEnum(CertificateOrderStatus)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
authorizations: z.array(
|
|
||||||
z.object({
|
|
||||||
identifier: z.object({
|
|
||||||
type: z.nativeEnum(ACMESANType),
|
|
||||||
value: z.string()
|
|
||||||
}),
|
|
||||||
status: z.nativeEnum(CertificateOrderStatus),
|
|
||||||
expires: z.string().optional(),
|
|
||||||
challenges: z.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
status: z.nativeEnum(CertificateOrderStatus),
|
|
||||||
url: z.string(),
|
|
||||||
token: z.string()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
finalize: z.string(),
|
|
||||||
certificate: z.string().optional()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const certificateOrderObject = {
|
||||||
|
altNames: req.body.subjectAlternativeNames,
|
||||||
|
validity: {
|
||||||
|
ttl: req.body.ttl
|
||||||
|
},
|
||||||
|
commonName: req.body.commonName,
|
||||||
|
keyUsages: req.body.keyUsages,
|
||||||
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
};
|
||||||
|
|
||||||
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
const data = await server.services.certificateV3.orderCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
profileId: req.body.profileId,
|
profileId: req.body.profileId,
|
||||||
certificateOrder: {
|
certificateOrder: certificateOrderObject,
|
||||||
altNames: req.body.subjectAlternativeNames,
|
|
||||||
validity: {
|
|
||||||
ttl: req.body.ttl
|
|
||||||
},
|
|
||||||
commonName: req.body.commonName,
|
|
||||||
keyUsages: req.body.keyUsages,
|
|
||||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
|
||||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
|
||||||
keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
},
|
|
||||||
removeRootsFromChain: req.body.removeRootsFromChain
|
removeRootsFromChain: req.body.removeRootsFromChain
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certificateRequest = await server.services.certificateRequest.createCertificateRequest({
|
||||||
|
status: CertificateRequestStatus.PENDING,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: data.projectId,
|
||||||
|
profileId: req.body.profileId,
|
||||||
|
commonName: req.body.commonName,
|
||||||
|
altNames: req.body.subjectAlternativeNames?.map((san) => `${san.type}:${san.value}`).join(","),
|
||||||
|
keyUsages: req.body.keyUsages,
|
||||||
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||||
|
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||||
|
keyAlgorithm: req.body.keyAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: data.projectId,
|
projectId: data.projectId,
|
||||||
@@ -343,13 +392,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
orderId: data.orderId,
|
|
||||||
profileName: data.profileName
|
profileName: data.profileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return {
|
||||||
|
...data,
|
||||||
|
certificateRequestId: certificateRequest.id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,12 +428,24 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateChain: z.string().trim(),
|
certificateChain: z.string().trim(),
|
||||||
privateKey: z.string().trim().optional(),
|
privateKey: z.string().trim().optional(),
|
||||||
serialNumber: z.string().trim(),
|
serialNumber: z.string().trim(),
|
||||||
certificateId: z.string()
|
certificateId: z.string(),
|
||||||
|
certificateRequestId: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const originalCertificate = await server.services.certificate.getCert({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.certificateId
|
||||||
|
});
|
||||||
|
if (!originalCertificate) {
|
||||||
|
throw new NotFoundError({ message: "Original certificate not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const data = await server.services.certificateV3.renewCertificate({
|
const data = await server.services.certificateV3.renewCertificate({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import acme, { CsrBuffer } from "acme-client";
|
import acme, { CsrBuffer } from "acme-client";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
CertKeyUsage,
|
CertKeyUsage,
|
||||||
CertStatus
|
CertStatus
|
||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||||
@@ -47,6 +49,60 @@ import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-prov
|
|||||||
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
||||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||||
|
|
||||||
|
const parseTtlToDays = (ttl: string): number => {
|
||||||
|
const match = ttl.match(new RE2("^(\\d+)([dhm])$"));
|
||||||
|
if (!match) {
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, value, unit] = match;
|
||||||
|
const num = parseInt(value, 10);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case "d":
|
||||||
|
return num;
|
||||||
|
case "h":
|
||||||
|
return Math.ceil(num / 24);
|
||||||
|
case "m":
|
||||||
|
return Math.ceil(num / (24 * 60));
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL unit: ${unit}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRenewalThreshold = (
|
||||||
|
profileRenewBeforeDays: number | undefined,
|
||||||
|
certificateTtlInDays: number
|
||||||
|
): number | undefined => {
|
||||||
|
if (profileRenewBeforeDays === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileRenewBeforeDays >= certificateTtlInDays) {
|
||||||
|
return Math.max(1, certificateTtlInDays - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileRenewBeforeDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateFinalRenewBeforeDays = (
|
||||||
|
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } } | undefined,
|
||||||
|
ttl: string
|
||||||
|
): number | undefined => {
|
||||||
|
if (!profile?.apiConfig?.autoRenew || !profile.apiConfig.renewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateTtlInDays = parseTtlToDays(ttl);
|
||||||
|
const renewBeforeDays = calculateRenewalThreshold(profile.apiConfig.renewBeforeDays, certificateTtlInDays);
|
||||||
|
|
||||||
|
if (!renewBeforeDays) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renewBeforeDays;
|
||||||
|
};
|
||||||
|
|
||||||
type TAcmeCertificateAuthorityFnsDeps = {
|
type TAcmeCertificateAuthorityFnsDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||||
@@ -55,7 +111,7 @@ type TAcmeCertificateAuthorityFnsDeps = {
|
|||||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
|
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
|
||||||
>;
|
>;
|
||||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
@@ -66,13 +122,14 @@ type TAcmeCertificateAuthorityFnsDeps = {
|
|||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||||
|
certificateProfileDAL?: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TOrderCertificateDeps = {
|
type TOrderCertificateDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
@@ -80,6 +137,7 @@ type TOrderCertificateDeps = {
|
|||||||
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
|
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
|
||||||
>;
|
>;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||||
|
certificateProfileDAL?: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DBConfigurationColumn = {
|
type DBConfigurationColumn = {
|
||||||
@@ -93,7 +151,7 @@ type DBConfigurationColumn = {
|
|||||||
|
|
||||||
export const castDbEntryToAcmeCertificateAuthority = (
|
export const castDbEntryToAcmeCertificateAuthority = (
|
||||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||||
): TAcmeCertificateAuthority & { credentials: unknown } => {
|
): TAcmeCertificateAuthority & { credentials: Buffer | null | undefined } => {
|
||||||
if (!ca.externalCa?.id) {
|
if (!ca.externalCa?.id) {
|
||||||
throw new BadRequestError({ message: "Malformed ACME certificate authority" });
|
throw new BadRequestError({ message: "Malformed ACME certificate authority" });
|
||||||
}
|
}
|
||||||
@@ -141,15 +199,22 @@ const getAcmeChallengeRecord = (
|
|||||||
export const orderCertificate = async (
|
export const orderCertificate = async (
|
||||||
{
|
{
|
||||||
caId,
|
caId,
|
||||||
|
profileId,
|
||||||
subscriberId,
|
subscriberId,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames,
|
||||||
csr,
|
csr,
|
||||||
csrPrivateKey,
|
csrPrivateKey,
|
||||||
keyUsages,
|
keyUsages,
|
||||||
extendedKeyUsages
|
extendedKeyUsages,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId
|
||||||
}: {
|
}: {
|
||||||
caId: string;
|
caId: string;
|
||||||
|
profileId?: string;
|
||||||
subscriberId?: string;
|
subscriberId?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
altNames?: string[];
|
altNames?: string[];
|
||||||
@@ -157,6 +222,11 @@ export const orderCertificate = async (
|
|||||||
csrPrivateKey?: string;
|
csrPrivateKey?: string;
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsage[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
|
ttl?: string;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
|
isRenewal?: boolean;
|
||||||
|
originalCertificateId?: string;
|
||||||
},
|
},
|
||||||
deps: TOrderCertificateDeps,
|
deps: TOrderCertificateDeps,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
@@ -169,7 +239,8 @@ export const orderCertificate = async (
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
certificateProfileDAL
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
|
||||||
@@ -199,7 +270,7 @@ export const orderCertificate = async (
|
|||||||
let accountKey: Buffer | undefined;
|
let accountKey: Buffer | undefined;
|
||||||
if (acmeCa.credentials) {
|
if (acmeCa.credentials) {
|
||||||
const decryptedCredentials = await kmsDecryptor({
|
const decryptedCredentials = await kmsDecryptor({
|
||||||
cipherTextBlob: acmeCa.credentials as Buffer
|
cipherTextBlob: acmeCa.credentials
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
|
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
|
||||||
@@ -364,6 +435,7 @@ export const orderCertificate = async (
|
|||||||
{
|
{
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
pkiSubscriberId: subscriberId,
|
pkiSubscriberId: subscriberId,
|
||||||
|
profileId,
|
||||||
status: CertStatus.ACTIVE,
|
status: CertStatus.ACTIVE,
|
||||||
friendlyName: commonName,
|
friendlyName: commonName,
|
||||||
commonName,
|
commonName,
|
||||||
@@ -373,11 +445,18 @@ export const orderCertificate = async (
|
|||||||
notAfter: certObj.notAfter,
|
notAfter: certObj.notAfter,
|
||||||
keyUsages,
|
keyUsages,
|
||||||
extendedKeyUsages,
|
extendedKeyUsages,
|
||||||
projectId: ca.projectId
|
keyAlgorithm,
|
||||||
|
signatureAlgorithm,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
renewedFromCertificateId: isRenewal && originalCertificateId ? originalCertificateId : null
|
||||||
},
|
},
|
||||||
innerTx
|
innerTx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isRenewal && originalCertificateId) {
|
||||||
|
await certificateDAL.updateById(originalCertificateId, { renewedByCertificateId: cert.id }, innerTx);
|
||||||
|
}
|
||||||
|
|
||||||
await certificateBodyDAL.create(
|
await certificateBodyDAL.create(
|
||||||
{
|
{
|
||||||
certId: cert.id,
|
certId: cert.id,
|
||||||
@@ -397,6 +476,26 @@ export const orderCertificate = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileId && ttl && certificateProfileDAL) {
|
||||||
|
const profile = await certificateProfileDAL.findById(profileId, innerTx);
|
||||||
|
if (profile) {
|
||||||
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
|
profile as { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } },
|
||||||
|
ttl
|
||||||
|
);
|
||||||
|
|
||||||
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
await certificateDAL.updateById(
|
||||||
|
cert.id,
|
||||||
|
{
|
||||||
|
renewBeforeDays: finalRenewBeforeDays
|
||||||
|
},
|
||||||
|
innerTx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cert;
|
return cert;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -413,7 +512,8 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
}: TAcmeCertificateAuthorityFnsDeps) => {
|
}: TAcmeCertificateAuthorityFnsDeps) => {
|
||||||
const createCertificateAuthority = async ({
|
const createCertificateAuthority = async ({
|
||||||
name,
|
name,
|
||||||
@@ -668,10 +768,71 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
|
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const orderCertificateFromProfile = async ({
|
||||||
|
caId,
|
||||||
|
profileId,
|
||||||
|
commonName,
|
||||||
|
altNames = [],
|
||||||
|
csr,
|
||||||
|
csrPrivateKey,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId
|
||||||
|
}: {
|
||||||
|
caId: string;
|
||||||
|
profileId?: string;
|
||||||
|
commonName: string;
|
||||||
|
altNames?: string[];
|
||||||
|
csr: CsrBuffer;
|
||||||
|
csrPrivateKey: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
|
ttl?: string;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
|
isRenewal?: boolean;
|
||||||
|
originalCertificateId?: string;
|
||||||
|
}) => {
|
||||||
|
return orderCertificate(
|
||||||
|
{
|
||||||
|
caId,
|
||||||
|
profileId,
|
||||||
|
subscriberId: undefined,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
csr,
|
||||||
|
csrPrivateKey,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appConnectionDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL,
|
||||||
|
certificateProfileDAL
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createCertificateAuthority,
|
createCertificateAuthority,
|
||||||
updateCertificateAuthority,
|
updateCertificateAuthority,
|
||||||
listCertificateAuthorities,
|
listCertificateAuthorities,
|
||||||
orderSubscriberCertificate
|
orderSubscriberCertificate,
|
||||||
|
orderCertificateFromProfile
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+441
-4
@@ -21,8 +21,10 @@ import {
|
|||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertKeyUsage,
|
CertKeyUsage,
|
||||||
CertStatus
|
CertStatus,
|
||||||
|
TAltNameType
|
||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||||
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
|
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
|
||||||
@@ -42,6 +44,60 @@ import {
|
|||||||
TUpdateAzureAdCsCertificateAuthorityDTO
|
TUpdateAzureAdCsCertificateAuthorityDTO
|
||||||
} from "./azure-ad-cs-certificate-authority-types";
|
} from "./azure-ad-cs-certificate-authority-types";
|
||||||
|
|
||||||
|
const parseTtlToDays = (ttl: string): number => {
|
||||||
|
const match = ttl.match(new RE2("^(\\d+)([dhm])$"));
|
||||||
|
if (!match) {
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, value, unit] = match;
|
||||||
|
const num = parseInt(value, 10);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case "d":
|
||||||
|
return num;
|
||||||
|
case "h":
|
||||||
|
return Math.ceil(num / 24);
|
||||||
|
case "m":
|
||||||
|
return Math.ceil(num / (24 * 60));
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL unit: ${unit}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRenewalThreshold = (
|
||||||
|
profileRenewBeforeDays: number | undefined,
|
||||||
|
certificateTtlInDays: number
|
||||||
|
): number | undefined => {
|
||||||
|
if (profileRenewBeforeDays === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileRenewBeforeDays >= certificateTtlInDays) {
|
||||||
|
return Math.max(1, certificateTtlInDays - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileRenewBeforeDays;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateFinalRenewBeforeDays = (
|
||||||
|
profile: { apiConfig?: { autoRenew?: boolean; renewBeforeDays?: number } } | undefined,
|
||||||
|
ttl: string
|
||||||
|
): number | undefined => {
|
||||||
|
const hasAutoRenewEnabled = profile?.apiConfig?.autoRenew === true;
|
||||||
|
if (!hasAutoRenewEnabled) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileRenewBeforeDays = profile?.apiConfig?.renewBeforeDays;
|
||||||
|
if (profileRenewBeforeDays !== undefined) {
|
||||||
|
const certificateTtlInDays = parseTtlToDays(ttl);
|
||||||
|
return calculateRenewalThreshold(profileRenewBeforeDays, certificateTtlInDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
type TAzureAdCsCertificateAuthorityFnsDeps = {
|
type TAzureAdCsCertificateAuthorityFnsDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||||
@@ -50,7 +106,7 @@ type TAzureAdCsCertificateAuthorityFnsDeps = {
|
|||||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
|
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" | "findById"
|
||||||
>;
|
>;
|
||||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
@@ -61,6 +117,7 @@ type TAzureAdCsCertificateAuthorityFnsDeps = {
|
|||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||||
|
certificateProfileDAL?: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AzureCertificateRequest = {
|
type AzureCertificateRequest = {
|
||||||
@@ -190,7 +247,7 @@ const buildSubjectDN = (commonName: string, properties?: TPkiSubscriberPropertie
|
|||||||
|
|
||||||
export const castDbEntryToAzureAdCsCertificateAuthority = (
|
export const castDbEntryToAzureAdCsCertificateAuthority = (
|
||||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||||
): TAzureAdCsCertificateAuthority & { credentials: unknown } => {
|
): TAzureAdCsCertificateAuthority & { credentials: Buffer | null | undefined } => {
|
||||||
if (!ca.externalCa?.id) {
|
if (!ca.externalCa?.id) {
|
||||||
throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" });
|
throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" });
|
||||||
}
|
}
|
||||||
@@ -591,7 +648,8 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
}: TAzureAdCsCertificateAuthorityFnsDeps) => {
|
}: TAzureAdCsCertificateAuthorityFnsDeps) => {
|
||||||
const createCertificateAuthority = async ({
|
const createCertificateAuthority = async ({
|
||||||
name,
|
name,
|
||||||
@@ -1024,6 +1082,384 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const orderCertificateFromProfile = async ({
|
||||||
|
caId,
|
||||||
|
profileId,
|
||||||
|
commonName,
|
||||||
|
altNames = [],
|
||||||
|
keyUsages = [],
|
||||||
|
extendedKeyUsages = [],
|
||||||
|
template,
|
||||||
|
validity,
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm = CertKeyAlgorithm.RSA_2048,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId
|
||||||
|
}: {
|
||||||
|
caId: string;
|
||||||
|
profileId: string;
|
||||||
|
commonName: string;
|
||||||
|
altNames?: string[];
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
|
template?: string;
|
||||||
|
validity: { ttl: string };
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: CertKeyAlgorithm;
|
||||||
|
isRenewal?: boolean;
|
||||||
|
originalCertificateId?: string;
|
||||||
|
}) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||||
|
if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) {
|
||||||
|
throw new BadRequestError({ message: "CA is not an Active Directory Certificate Service CA" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca);
|
||||||
|
if (azureCa.status !== CaStatus.ACTIVE) {
|
||||||
|
throw new BadRequestError({ message: "CA is disabled" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { username, password, adcsUrl, sslRejectUnauthorized, sslCertificate } =
|
||||||
|
await getAzureADCSConnectionCredentials(
|
||||||
|
azureCa.configuration.azureAdcsConnectionId,
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
);
|
||||||
|
|
||||||
|
const credentials: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
sslRejectUnauthorized?: boolean;
|
||||||
|
sslCertificate?: string;
|
||||||
|
} = {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
sslRejectUnauthorized,
|
||||||
|
sslCertificate
|
||||||
|
};
|
||||||
|
|
||||||
|
let alg;
|
||||||
|
if (signatureAlgorithm) {
|
||||||
|
switch (signatureAlgorithm.toUpperCase()) {
|
||||||
|
case "RSA-SHA256":
|
||||||
|
case "SHA256WITHRSA":
|
||||||
|
alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
break;
|
||||||
|
case "RSA-SHA384":
|
||||||
|
case "SHA384WITHRSA":
|
||||||
|
alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_3072);
|
||||||
|
break;
|
||||||
|
case "RSA-SHA512":
|
||||||
|
case "SHA512WITHRSA":
|
||||||
|
alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_4096);
|
||||||
|
break;
|
||||||
|
case "ECDSA-SHA256":
|
||||||
|
case "SHA256WITHECDSA":
|
||||||
|
alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.ECDSA_P256);
|
||||||
|
break;
|
||||||
|
case "ECDSA-SHA384":
|
||||||
|
case "SHA384WITHECDSA":
|
||||||
|
alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.ECDSA_P384);
|
||||||
|
break;
|
||||||
|
case "ECDSA-SHA512":
|
||||||
|
case "SHA512WITHECDSA":
|
||||||
|
alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.ECDSA_P521);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
|
||||||
|
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
const subjectDN = buildSubjectDN(commonName);
|
||||||
|
|
||||||
|
let sanExtension = "";
|
||||||
|
if (altNames && altNames.length > 0) {
|
||||||
|
sanExtension = altNames.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
|
name: subjectDN,
|
||||||
|
keys: leafKeys,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
...(sanExtension && {
|
||||||
|
extensions: [
|
||||||
|
new x509.SubjectAlternativeNameExtension(
|
||||||
|
altNames.map((name) => ({ type: "dns" as TAltNameType, value: name })),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const csrPem = csrObj.toString("pem");
|
||||||
|
|
||||||
|
let templateValue = template;
|
||||||
|
if (!templateValue) {
|
||||||
|
templateValue = "WebServer";
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateInput = templateValue.trim();
|
||||||
|
if (!templateInput || templateInput.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate template name cannot be empty"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let validityPeriod: string | undefined;
|
||||||
|
if (notBefore && notAfter) {
|
||||||
|
if (notAfter <= notBefore) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate notAfter date must be after notBefore date"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffMs = notAfter.getTime() - notBefore.getTime();
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
validityPeriod = `${diffDays}d`;
|
||||||
|
} else if (notAfter) {
|
||||||
|
const diffMs = notAfter.getTime() - Date.now();
|
||||||
|
if (diffMs <= 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Certificate notAfter date must be in the future"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
validityPeriod = `${diffDays}d`;
|
||||||
|
} else if (validity.ttl) {
|
||||||
|
validityPeriod = validity.ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateRequest: AzureCertificateRequest = {
|
||||||
|
csr: csrPem,
|
||||||
|
template: templateInput,
|
||||||
|
attributes: {
|
||||||
|
subject: subjectDN,
|
||||||
|
...(sanExtension && { subjectAlternativeName: sanExtension }),
|
||||||
|
...(validityPeriod && { validityPeriod })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let submissionResponse;
|
||||||
|
const maxOidRetries = 3;
|
||||||
|
let oidRetryCount = 0;
|
||||||
|
|
||||||
|
while (oidRetryCount <= maxOidRetries) {
|
||||||
|
try {
|
||||||
|
submissionResponse = await submitCertificateRequest(credentials, adcsUrl, certificateRequest);
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
const isOidError =
|
||||||
|
error instanceof BadRequestError &&
|
||||||
|
(error.message.includes("OID resolution error") || error.message.includes("Cannot get OID for name type"));
|
||||||
|
|
||||||
|
if (isOidError && oidRetryCount < maxOidRetries) {
|
||||||
|
oidRetryCount += 1;
|
||||||
|
|
||||||
|
const delay = 3000 * oidRetryCount;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!submissionResponse) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to submit certificate request after multiple attempts due to OID resolution issues"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submissionResponse.status === "denied") {
|
||||||
|
throw new BadRequestError({ message: "Certificate request was denied by ADCS" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificatePem = "";
|
||||||
|
|
||||||
|
if (submissionResponse.status === "issued" && submissionResponse.certificate) {
|
||||||
|
certificatePem = submissionResponse.certificate;
|
||||||
|
} else {
|
||||||
|
const maxRetries = 5;
|
||||||
|
const initialDelay = 2000;
|
||||||
|
let retryCount = 0;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
while (retryCount < maxRetries) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
certificatePem = await retrieveCertificate(credentials, adcsUrl, submissionResponse.certificateId);
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
retryCount++;
|
||||||
|
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
// Wait with exponential backoff: 2s, 4s, 8s, 16s, 32s
|
||||||
|
const delay = initialDelay * 2 ** (retryCount - 1);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount === maxRetries) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificate request submitted with ID ${submissionResponse.certificateId} but failed to retrieve after ${maxRetries} attempts. The certificate may still be pending approval or processing. Last error: ${lastError?.message || "Unknown error"}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certificatePem) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to obtain certificate from ADCS. The certificate may still be pending processing."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanedCertificatePem = certificatePem.trim();
|
||||||
|
|
||||||
|
if (!cleanedCertificatePem.includes("-----BEGIN CERTIFICATE-----")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid certificate format received from ADCS. Expected PEM format."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedCertificatePem = cleanedCertificatePem
|
||||||
|
.replace(new RE2("\\r\\n", "g"), "\n")
|
||||||
|
.replace(new RE2("\\r", "g"), "\n")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!cleanedCertificatePem.includes("-----END CERTIFICATE-----")) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid certificate format received from ADCS. Missing end marker."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let certObj: x509.X509Certificate;
|
||||||
|
try {
|
||||||
|
certObj = new x509.X509Certificate(cleanedCertificatePem);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to parse certificate from ADCS: ${error instanceof Error ? error.message : "Unknown error"}. Certificate data may be corrupted.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChainPem = submissionResponse.certificateChain || "";
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(certificateChainPem)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(skLeaf)
|
||||||
|
});
|
||||||
|
|
||||||
|
let certificateId: string;
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
profileId,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: commonName,
|
||||||
|
commonName,
|
||||||
|
altNames: altNames.join(","),
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
notBefore: certObj.notBefore,
|
||||||
|
notAfter: certObj.notAfter,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
keyAlgorithm,
|
||||||
|
signatureAlgorithm,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
renewedFromCertificateId: isRenewal && originalCertificateId ? originalCertificateId : null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
certificateId = cert.id;
|
||||||
|
|
||||||
|
if (isRenewal && originalCertificateId) {
|
||||||
|
await certificateDAL.updateById(originalCertificateId, { renewedByCertificateId: cert.id }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedPrivateKey
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profileId && validity?.ttl && certificateProfileDAL) {
|
||||||
|
const profile = await certificateProfileDAL.findById(profileId, tx);
|
||||||
|
if (profile) {
|
||||||
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(undefined, validity.ttl);
|
||||||
|
|
||||||
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
await certificateDAL.updateById(
|
||||||
|
cert.id,
|
||||||
|
{
|
||||||
|
renewBeforeDays: finalRenewBeforeDays
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: cleanedCertificatePem,
|
||||||
|
certificateChain: certificateChainPem,
|
||||||
|
privateKey: skLeaf,
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
certificateId: certificateId!,
|
||||||
|
ca: azureCa
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getTemplates = async ({ caId, projectId }: { caId: string; projectId: string }) => {
|
const getTemplates = async ({ caId, projectId }: { caId: string; projectId: string }) => {
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||||
if (!ca || ca.projectId !== projectId) {
|
if (!ca || ca.projectId !== projectId) {
|
||||||
@@ -1163,6 +1599,7 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
|||||||
updateCertificateAuthority,
|
updateCertificateAuthority,
|
||||||
listCertificateAuthorities,
|
listCertificateAuthorities,
|
||||||
orderSubscriberCertificate,
|
orderSubscriberCertificate,
|
||||||
|
orderCertificateFromProfile,
|
||||||
getTemplates
|
getTemplates
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+7
@@ -11,6 +11,13 @@ export const AzureAdCsCertificateAuthorityConfigurationSchema = z.object({
|
|||||||
azureAdcsConnectionId: z.string().uuid().trim().describe("Azure ADCS Connection ID")
|
azureAdcsConnectionId: z.string().uuid().trim().describe("Azure ADCS Connection ID")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AzureAdCsCertificateAuthorityCredentialsSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
sslRejectUnauthorized: z.boolean().optional(),
|
||||||
|
sslCertificate: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export const AzureAdCsCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
|
export const AzureAdCsCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
|
||||||
type: z.literal(CaType.AZURE_AD_CS),
|
type: z.literal(CaType.AZURE_AD_CS),
|
||||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
|
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TAppConnectionServiceFactory } from "../app-connection/app-connection-s
|
|||||||
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
|
import { TCertificateProfileDALFactory } from "../certificate-profile/certificate-profile-dal";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||||
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||||
@@ -63,7 +64,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
|
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
@@ -73,6 +74,7 @@ type TCertificateAuthorityServiceFactoryDep = {
|
|||||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
|
certificateProfileDAL?: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
||||||
@@ -91,7 +93,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsService,
|
kmsService,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
}: TCertificateAuthorityServiceFactoryDep) => {
|
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||||
const acmeFns = AcmeCertificateAuthorityFns({
|
const acmeFns = AcmeCertificateAuthorityFns({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
@@ -105,7 +108,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||||
@@ -120,7 +124,8 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const createCertificateAuthority = async (
|
const createCertificateAuthority = async (
|
||||||
@@ -677,6 +682,41 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCaById = async ({
|
||||||
|
caId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: {
|
||||||
|
caId: string;
|
||||||
|
actor: OrgServiceActor["type"];
|
||||||
|
actorId: string;
|
||||||
|
actorAuthMethod: OrgServiceActor["authMethod"];
|
||||||
|
actorOrgId?: string;
|
||||||
|
}) => {
|
||||||
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "CA not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
return ca;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createCertificateAuthority,
|
createCertificateAuthority,
|
||||||
findCertificateAuthorityById,
|
findCertificateAuthorityById,
|
||||||
@@ -685,6 +725,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
updateCertificateAuthority,
|
updateCertificateAuthority,
|
||||||
deleteCertificateAuthority,
|
deleteCertificateAuthority,
|
||||||
getAzureAdcsTemplates,
|
getAzureAdcsTemplates,
|
||||||
|
getCaById,
|
||||||
deprecatedUpdateCertificateAuthority,
|
deprecatedUpdateCertificateAuthority,
|
||||||
deprecatedDeleteCertificateAuthority
|
deprecatedDeleteCertificateAuthority
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
import acme from "acme-client";
|
||||||
|
|
||||||
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||||
|
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
|
||||||
|
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
|
import { CertKeyAlgorithm } from "../certificate-common/certificate-constants";
|
||||||
|
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
|
||||||
|
import { CertificateRequestStatus } from "../certificate-request/certificate-request-types";
|
||||||
|
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||||
|
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||||
|
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||||
|
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
|
||||||
|
import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
|
import { CaType } from "./certificate-authority-enums";
|
||||||
|
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||||
|
import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal";
|
||||||
|
|
||||||
|
export type TIssueCertificateFromProfileJobData = {
|
||||||
|
certificateId: string;
|
||||||
|
profileId: string;
|
||||||
|
caId: string;
|
||||||
|
commonName?: string;
|
||||||
|
altNames?: string[];
|
||||||
|
ttl: string;
|
||||||
|
signatureAlgorithm: string;
|
||||||
|
keyAlgorithm: string;
|
||||||
|
keyUsages?: string[];
|
||||||
|
extendedKeyUsages?: string[];
|
||||||
|
isRenewal?: boolean;
|
||||||
|
originalCertificateId?: string;
|
||||||
|
certificateRequestId?: string;
|
||||||
|
csr?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TCertificateIssuanceQueueFactoryDep = {
|
||||||
|
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||||
|
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||||
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey"
|
||||||
|
>;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById" | "updateById">;
|
||||||
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
|
certificateProfileDAL?: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
|
certificateRequestService?: Pick<
|
||||||
|
TCertificateRequestServiceFactory,
|
||||||
|
"attachCertificateToRequest" | "updateCertificateRequestStatus"
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateIssuanceQueueFactory = ReturnType<typeof certificateIssuanceQueueFactory>;
|
||||||
|
|
||||||
|
export const certificateIssuanceQueueFactory = ({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
queueService,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
pkiSubscriberDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL,
|
||||||
|
certificateRequestService
|
||||||
|
}: TCertificateIssuanceQueueFactoryDep) => {
|
||||||
|
const acmeFns = AcmeCertificateAuthorityFns({
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
pkiSubscriberDAL,
|
||||||
|
projectDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
pkiSubscriberDAL,
|
||||||
|
projectDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue,
|
||||||
|
certificateProfileDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a certificate issuance job using pgBoss
|
||||||
|
*/
|
||||||
|
const queueCertificateIssuance = async ({
|
||||||
|
certificateId,
|
||||||
|
profileId,
|
||||||
|
caId,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId,
|
||||||
|
certificateRequestId,
|
||||||
|
csr
|
||||||
|
}: TIssueCertificateFromProfileJobData) => {
|
||||||
|
const jobData: TIssueCertificateFromProfileJobData = {
|
||||||
|
certificateId,
|
||||||
|
profileId,
|
||||||
|
caId,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId,
|
||||||
|
certificateRequestId,
|
||||||
|
csr
|
||||||
|
};
|
||||||
|
|
||||||
|
await queueService.queuePg(QueueJobs.CaIssueCertificateFromProfile, jobData, {
|
||||||
|
retryLimit: 3,
|
||||||
|
retryDelay: 5,
|
||||||
|
retryBackoff: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process certificate issuance jobs
|
||||||
|
*/
|
||||||
|
const processCertificateIssuanceJobs = async (data: TIssueCertificateFromProfileJobData) => {
|
||||||
|
const {
|
||||||
|
certificateId,
|
||||||
|
profileId,
|
||||||
|
caId,
|
||||||
|
commonName,
|
||||||
|
altNames,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId,
|
||||||
|
certificateRequestId,
|
||||||
|
csr
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Processing certificate issuance job for [certificateId=${certificateId}] [caId=${caId}]`);
|
||||||
|
|
||||||
|
if (!caId) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Certificate authority ID is required for external CA certificate issuance`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||||
|
|
||||||
|
if (ca.externalCa?.type === CaType.ACME) {
|
||||||
|
let certificateCsr: string;
|
||||||
|
let skLeaf: string = "";
|
||||||
|
|
||||||
|
if (csr) {
|
||||||
|
certificateCsr = csr;
|
||||||
|
} else {
|
||||||
|
const keyAlg = keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyAlg, true, ["sign", "verify"]);
|
||||||
|
const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey);
|
||||||
|
skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
const [, generatedCsr] = await acme.crypto.createCsr(
|
||||||
|
{
|
||||||
|
altNames: altNames ? [...altNames] : [],
|
||||||
|
commonName: commonName || ""
|
||||||
|
},
|
||||||
|
skLeaf
|
||||||
|
);
|
||||||
|
certificateCsr = generatedCsr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const acmeResult = await acmeFns.orderCertificateFromProfile({
|
||||||
|
caId,
|
||||||
|
profileId,
|
||||||
|
commonName: commonName || "",
|
||||||
|
altNames: altNames || [],
|
||||||
|
csr: Buffer.from(certificateCsr),
|
||||||
|
csrPrivateKey: skLeaf,
|
||||||
|
keyUsages: keyUsages as CertKeyUsage[],
|
||||||
|
extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (certificateRequestId && certificateRequestService && acmeResult?.id) {
|
||||||
|
try {
|
||||||
|
await certificateRequestService.attachCertificateToRequest({
|
||||||
|
certificateRequestId,
|
||||||
|
certificateId: acmeResult.id
|
||||||
|
});
|
||||||
|
logger.info(`Certificate attached to request [certificateRequestId=${certificateRequestId}]`);
|
||||||
|
} catch (attachError) {
|
||||||
|
logger.error(
|
||||||
|
attachError,
|
||||||
|
`Failed to attach certificate to request [certificateRequestId=${certificateRequestId}]`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await certificateRequestService.updateCertificateRequestStatus({
|
||||||
|
certificateRequestId,
|
||||||
|
status: CertificateRequestStatus.FAILED,
|
||||||
|
errorMessage: `Failed to attach certificate: ${attachError instanceof Error ? attachError.message : String(attachError)}`
|
||||||
|
});
|
||||||
|
} catch (statusUpdateError) {
|
||||||
|
logger.error(
|
||||||
|
statusUpdateError,
|
||||||
|
`Failed to update certificate request status [certificateRequestId=${certificateRequestId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ca.externalCa?.type === CaType.AZURE_AD_CS) {
|
||||||
|
let template: string | undefined;
|
||||||
|
if (certificateProfileDAL) {
|
||||||
|
try {
|
||||||
|
const profile = await certificateProfileDAL.findById(profileId);
|
||||||
|
if (
|
||||||
|
profile?.externalConfigs &&
|
||||||
|
typeof profile.externalConfigs === "object" &&
|
||||||
|
profile.externalConfigs !== null
|
||||||
|
) {
|
||||||
|
const configs = profile.externalConfigs;
|
||||||
|
if (typeof configs.template === "string") {
|
||||||
|
template = configs.template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to fetch profile ${profileId} for template extraction: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const azureParams = {
|
||||||
|
caId,
|
||||||
|
profileId,
|
||||||
|
commonName: commonName || "",
|
||||||
|
altNames: altNames || [],
|
||||||
|
keyUsages: keyUsages as CertKeyUsage[],
|
||||||
|
extendedKeyUsages: extendedKeyUsages as CertExtendedKeyUsage[],
|
||||||
|
validity: { ttl },
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm: keyAlgorithm as CertKeyAlgorithm,
|
||||||
|
isRenewal,
|
||||||
|
originalCertificateId,
|
||||||
|
template,
|
||||||
|
...(csr && { csr })
|
||||||
|
};
|
||||||
|
|
||||||
|
const azureResult = await azureAdCsFns.orderCertificateFromProfile(azureParams);
|
||||||
|
|
||||||
|
if (certificateRequestId && certificateRequestService && azureResult?.certificateId) {
|
||||||
|
try {
|
||||||
|
await certificateRequestService.attachCertificateToRequest({
|
||||||
|
certificateRequestId,
|
||||||
|
certificateId: azureResult.certificateId
|
||||||
|
});
|
||||||
|
logger.info(`Certificate attached to request [certificateRequestId=${certificateRequestId}]`);
|
||||||
|
} catch (attachError) {
|
||||||
|
logger.error(
|
||||||
|
attachError,
|
||||||
|
`Failed to attach certificate to request [certificateRequestId=${certificateRequestId}]`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await certificateRequestService.updateCertificateRequestStatus({
|
||||||
|
certificateRequestId,
|
||||||
|
status: CertificateRequestStatus.FAILED,
|
||||||
|
errorMessage: `Failed to attach certificate: ${attachError instanceof Error ? attachError.message : String(attachError)}`
|
||||||
|
});
|
||||||
|
} catch (statusUpdateError) {
|
||||||
|
logger.error(
|
||||||
|
statusUpdateError,
|
||||||
|
`Failed to update certificate request status [certificateRequestId=${certificateRequestId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Successfully processed certificate issuance job with [certificateId=${certificateId}] [caId=${caId}]`
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, `Certificate issuance job failed for [certificateId=${certificateId}] [caId=${caId}]`);
|
||||||
|
|
||||||
|
if (certificateRequestId && certificateRequestService) {
|
||||||
|
try {
|
||||||
|
await certificateRequestService.updateCertificateRequestStatus({
|
||||||
|
certificateRequestId,
|
||||||
|
status: CertificateRequestStatus.FAILED,
|
||||||
|
errorMessage: `Certificate issuance failed: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
});
|
||||||
|
logger.info(`Updated certificate request ${certificateRequestId} status to failed due to issuance error`);
|
||||||
|
} catch (statusUpdateError) {
|
||||||
|
logger.error(
|
||||||
|
statusUpdateError,
|
||||||
|
`Failed to update certificate request status [certificateRequestId=${certificateRequestId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeCertificateIssuanceQueue = async () => {
|
||||||
|
await queueService.startPg(
|
||||||
|
QueueJobs.CaIssueCertificateFromProfile,
|
||||||
|
async ([job]) => {
|
||||||
|
const data = job.data as TIssueCertificateFromProfileJobData;
|
||||||
|
await processCertificateIssuanceJobs(data);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
workerCount: 2,
|
||||||
|
batchSize: 1,
|
||||||
|
pollingIntervalSeconds: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("Certificate issuance queue worker initialized successfully");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
queueCertificateIssuance,
|
||||||
|
initializeCertificateIssuanceQueue,
|
||||||
|
processCertificateIssuanceJobs
|
||||||
|
};
|
||||||
|
};
|
||||||
+30
-17
@@ -67,6 +67,7 @@ import {
|
|||||||
TGetCaDTO,
|
TGetCaDTO,
|
||||||
TImportCertToCaDTO,
|
TImportCertToCaDTO,
|
||||||
TIssueCertFromCaDTO,
|
TIssueCertFromCaDTO,
|
||||||
|
TIssueCertFromCaResponse,
|
||||||
TRenewCaCertDTO,
|
TRenewCaCertDTO,
|
||||||
TSignCertFromCaDTO,
|
TSignCertFromCaDTO,
|
||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
@@ -1198,7 +1199,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
isFromProfile,
|
isFromProfile,
|
||||||
internal = false,
|
internal = false,
|
||||||
tx
|
tx
|
||||||
}: TIssueCertFromCaDTO) => {
|
}: TIssueCertFromCaDTO): Promise<TIssueCertFromCaResponse> => {
|
||||||
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
|
let ca: TCertificateAuthorityWithAssociatedCa | undefined;
|
||||||
let certificateTemplate: TCertificateTemplates | undefined;
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
let collectionId = pkiCollectionId;
|
let collectionId = pkiCollectionId;
|
||||||
@@ -1532,10 +1533,11 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
return cert;
|
return cert;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cert;
|
||||||
if (tx) {
|
if (tx) {
|
||||||
await executeIssueCertOperations(tx);
|
cert = await executeIssueCertOperations(tx);
|
||||||
} else {
|
} else {
|
||||||
await certificateDAL.transaction(executeIssueCertOperations);
|
cert = await certificateDAL.transaction(executeIssueCertOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1544,6 +1546,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
privateKey: skLeaf,
|
privateKey: skLeaf,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
|
certificateId: cert.id,
|
||||||
|
commonName,
|
||||||
ca: expandInternalCa(ca)
|
ca: expandInternalCa(ca)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1571,15 +1575,16 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
keyUsages,
|
keyUsages,
|
||||||
extendedKeyUsages,
|
extendedKeyUsages,
|
||||||
signatureAlgorithm,
|
signatureAlgorithm,
|
||||||
keyAlgorithm
|
keyAlgorithm,
|
||||||
|
tx
|
||||||
} = dto;
|
} = dto;
|
||||||
|
|
||||||
let collectionId = pkiCollectionId;
|
let collectionId = pkiCollectionId;
|
||||||
|
|
||||||
if (caId) {
|
if (caId) {
|
||||||
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
|
||||||
} else if (certificateTemplateId) {
|
} else if (certificateTemplateId) {
|
||||||
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
certificateTemplate = await certificateTemplateDAL.getById(certificateTemplateId, tx);
|
||||||
if (!certificateTemplate) {
|
if (!certificateTemplate) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `Certificate template with ID '${certificateTemplateId}' not found`
|
message: `Certificate template with ID '${certificateTemplateId}' not found`
|
||||||
@@ -1587,7 +1592,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
collectionId = certificateTemplate.pkiCollectionId as string;
|
collectionId = certificateTemplate.pkiCollectionId as string;
|
||||||
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId);
|
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
@@ -1636,7 +1641,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
|
|
||||||
// check PKI collection
|
// check PKI collection
|
||||||
if (pkiCollectionId) {
|
if (pkiCollectionId) {
|
||||||
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId);
|
const pkiCollection = await pkiCollectionDAL.findById(pkiCollectionId, tx);
|
||||||
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${pkiCollectionId}' not found` });
|
if (!pkiCollection) throw new NotFoundError({ message: `PKI collection with ID '${pkiCollectionId}' not found` });
|
||||||
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
|
if (pkiCollection.projectId !== ca.projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
|
||||||
}
|
}
|
||||||
@@ -1905,8 +1910,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
plainText: Buffer.from(certificateChainPem)
|
plainText: Buffer.from(certificateChainPem)
|
||||||
});
|
});
|
||||||
|
|
||||||
await certificateDAL.transaction(async (tx) => {
|
const createSignedCert = async (transaction: Knex) => {
|
||||||
const cert = await certificateDAL.create(
|
const newCert = await certificateDAL.create(
|
||||||
{
|
{
|
||||||
caId: (ca as TCertificateAuthorities).id,
|
caId: (ca as TCertificateAuthorities).id,
|
||||||
caCertId: caCert.id,
|
caCertId: caCert.id,
|
||||||
@@ -1924,36 +1929,44 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
keyAlgorithm: keyAlgorithm || ca!.internalCa!.keyAlgorithm,
|
keyAlgorithm: keyAlgorithm || ca!.internalCa!.keyAlgorithm,
|
||||||
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
|
signatureAlgorithm: signatureAlgorithm || ca!.internalCa!.keyAlgorithm
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
await certificateBodyDAL.create(
|
await certificateBodyDAL.create(
|
||||||
{
|
{
|
||||||
certId: cert.id,
|
certId: newCert.id,
|
||||||
encryptedCertificate,
|
encryptedCertificate,
|
||||||
encryptedCertificateChain
|
encryptedCertificateChain
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (collectionId) {
|
if (collectionId) {
|
||||||
await pkiCollectionItemDAL.create(
|
await pkiCollectionItemDAL.create(
|
||||||
{
|
{
|
||||||
pkiCollectionId: collectionId,
|
pkiCollectionId: collectionId,
|
||||||
certId: cert.id
|
certId: newCert.id
|
||||||
},
|
},
|
||||||
tx
|
transaction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cert;
|
return newCert;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
let cert;
|
||||||
|
if (tx) {
|
||||||
|
cert = await createSignedCert(tx);
|
||||||
|
} else {
|
||||||
|
cert = await certificateDAL.transaction(createSignedCert);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: leafCert,
|
certificate: leafCert,
|
||||||
certificateChain: certificateChainPem,
|
certificateChain: certificateChainPem,
|
||||||
issuingCaCertificate,
|
issuingCaCertificate,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
|
certificateId: cert.id,
|
||||||
ca: expandInternalCa(ca),
|
ca: expandInternalCa(ca),
|
||||||
commonName: cn
|
commonName: cn
|
||||||
};
|
};
|
||||||
|
|||||||
+19
@@ -160,6 +160,7 @@ export type TSignCertFromCaDTO =
|
|||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
isFromProfile?: boolean;
|
isFromProfile?: boolean;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
tx?: Knex;
|
||||||
}
|
}
|
||||||
| ({
|
| ({
|
||||||
isInternal: false;
|
isInternal: false;
|
||||||
@@ -179,6 +180,7 @@ export type TSignCertFromCaDTO =
|
|||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
isFromProfile?: boolean;
|
isFromProfile?: boolean;
|
||||||
profileId?: string;
|
profileId?: string;
|
||||||
|
tx?: Knex;
|
||||||
} & Omit<TProjectPermission, "projectId">);
|
} & Omit<TProjectPermission, "projectId">);
|
||||||
|
|
||||||
export type TGetCaCertificateTemplatesDTO = {
|
export type TGetCaCertificateTemplatesDTO = {
|
||||||
@@ -248,3 +250,20 @@ export type TIssueCertWithTemplateDTO = {
|
|||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsage[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TCaReference = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
dn: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueCertFromCaResponse = {
|
||||||
|
certificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
issuingCaCertificate: string;
|
||||||
|
privateKey: string;
|
||||||
|
serialNumber: string;
|
||||||
|
certificateId: string;
|
||||||
|
ca: TCaReference;
|
||||||
|
commonName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,10 +22,19 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise<TCertificateProfile> => {
|
const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise<TCertificateProfile> => {
|
||||||
try {
|
try {
|
||||||
const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile).insert(data).returning("*")) as [
|
const dataToInsert = {
|
||||||
TCertificateProfile
|
...data,
|
||||||
];
|
externalConfigs: data.externalConfigs ? JSON.stringify(data.externalConfigs) : null
|
||||||
return certificateProfile;
|
};
|
||||||
|
|
||||||
|
const [insertedProfile] = await (tx || db)(TableName.PkiCertificateProfile).insert(dataToInsert).returning("*");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...insertedProfile,
|
||||||
|
externalConfigs: insertedProfile.externalConfigs
|
||||||
|
? (JSON.parse(insertedProfile.externalConfigs) as Record<string, unknown>)
|
||||||
|
: null
|
||||||
|
} as TCertificateProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Create certificate profile" });
|
throw new DatabaseError({ error, name: "Create certificate profile" });
|
||||||
}
|
}
|
||||||
@@ -33,11 +42,25 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex): Promise<TCertificateProfile> => {
|
const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex): Promise<TCertificateProfile> => {
|
||||||
try {
|
try {
|
||||||
const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile)
|
const dataToUpdate: Partial<Record<string, unknown>> = {
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.externalConfigs !== undefined) {
|
||||||
|
dataToUpdate.externalConfigs = data.externalConfigs ? JSON.stringify(data.externalConfigs) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedProfile] = await (tx || db)(TableName.PkiCertificateProfile)
|
||||||
.where({ id })
|
.where({ id })
|
||||||
.update(data)
|
.update(dataToUpdate)
|
||||||
.returning("*")) as [TCertificateProfile];
|
.returning("*");
|
||||||
return certificateProfile;
|
|
||||||
|
return {
|
||||||
|
...updatedProfile,
|
||||||
|
externalConfigs: updatedProfile.externalConfigs
|
||||||
|
? (JSON.parse(updatedProfile.externalConfigs) as Record<string, unknown>)
|
||||||
|
: null
|
||||||
|
} as TCertificateProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Update certificate profile" });
|
throw new DatabaseError({ error, name: "Update certificate profile" });
|
||||||
}
|
}
|
||||||
@@ -57,10 +80,16 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findById = async (id: string, tx?: Knex): Promise<TCertificateProfile | undefined> => {
|
const findById = async (id: string, tx?: Knex): Promise<TCertificateProfile | undefined> => {
|
||||||
try {
|
try {
|
||||||
const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first()) as
|
const certificateProfile = await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first();
|
||||||
| TCertificateProfile
|
|
||||||
| undefined;
|
if (!certificateProfile) return undefined;
|
||||||
return certificateProfile;
|
|
||||||
|
return {
|
||||||
|
...certificateProfile,
|
||||||
|
externalConfigs: certificateProfile.externalConfigs
|
||||||
|
? (JSON.parse(certificateProfile.externalConfigs) as Record<string, unknown>)
|
||||||
|
: null
|
||||||
|
} as TCertificateProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find certificate profile by id" });
|
throw new DatabaseError({ error, name: "Find certificate profile by id" });
|
||||||
}
|
}
|
||||||
@@ -203,6 +232,9 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
estConfigId: result.estConfigId,
|
estConfigId: result.estConfigId,
|
||||||
apiConfigId: result.apiConfigId,
|
apiConfigId: result.apiConfigId,
|
||||||
acmeConfigId: result.acmeConfigId,
|
acmeConfigId: result.acmeConfigId,
|
||||||
|
externalConfigs: result.externalConfigs
|
||||||
|
? (JSON.parse(result.externalConfigs) as Record<string, unknown>)
|
||||||
|
: null,
|
||||||
createdAt: result.createdAt,
|
createdAt: result.createdAt,
|
||||||
updatedAt: result.updatedAt,
|
updatedAt: result.updatedAt,
|
||||||
estConfig,
|
estConfig,
|
||||||
@@ -277,6 +309,16 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = baseQuery
|
const query = baseQuery
|
||||||
|
.leftJoin(
|
||||||
|
TableName.CertificateAuthority,
|
||||||
|
`${TableName.PkiCertificateProfile}.caId`,
|
||||||
|
`${TableName.CertificateAuthority}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ExternalCertificateAuthority,
|
||||||
|
`${TableName.CertificateAuthority}.id`,
|
||||||
|
`${TableName.ExternalCertificateAuthority}.caId`
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.PkiEstEnrollmentConfig,
|
TableName.PkiEstEnrollmentConfig,
|
||||||
`${TableName.PkiCertificateProfile}.estConfigId`,
|
`${TableName.PkiCertificateProfile}.estConfigId`,
|
||||||
@@ -294,6 +336,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.PkiCertificateProfile))
|
.select(selectAllTableCols(TableName.PkiCertificateProfile))
|
||||||
.select(
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"),
|
||||||
|
db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
|
||||||
|
db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"),
|
||||||
|
db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"),
|
||||||
|
db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalCaType"),
|
||||||
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
|
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
|
||||||
db
|
db
|
||||||
.ref("disableBootstrapCaValidation")
|
.ref("disableBootstrapCaValidation")
|
||||||
@@ -337,6 +384,16 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const certificateAuthority = result.caId
|
||||||
|
? {
|
||||||
|
id: result.caId as string,
|
||||||
|
name: result.caName as string,
|
||||||
|
status: result.caStatus as string,
|
||||||
|
isExternal: !!result.externalCaId,
|
||||||
|
externalType: result.externalCaType as string | undefined
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const baseProfile = {
|
const baseProfile = {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
projectId: result.projectId,
|
projectId: result.projectId,
|
||||||
@@ -349,11 +406,15 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
estConfigId: result.estConfigId,
|
estConfigId: result.estConfigId,
|
||||||
apiConfigId: result.apiConfigId,
|
apiConfigId: result.apiConfigId,
|
||||||
acmeConfigId: result.acmeConfigId,
|
acmeConfigId: result.acmeConfigId,
|
||||||
|
externalConfigs: result.externalConfigs
|
||||||
|
? (JSON.parse(result.externalConfigs as string) as Record<string, unknown>)
|
||||||
|
: null,
|
||||||
createdAt: result.createdAt,
|
createdAt: result.createdAt,
|
||||||
updatedAt: result.updatedAt,
|
updatedAt: result.updatedAt,
|
||||||
estConfig,
|
estConfig,
|
||||||
apiConfig,
|
apiConfig,
|
||||||
acmeConfig
|
acmeConfig,
|
||||||
|
certificateAuthority
|
||||||
};
|
};
|
||||||
|
|
||||||
return baseProfile as TCertificateProfileWithConfigs;
|
return baseProfile as TCertificateProfileWithConfigs;
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External configuration schema for Azure AD CS Certificate Authority
|
||||||
|
*/
|
||||||
|
export const AzureAdCsExternalConfigSchema = z.object({
|
||||||
|
template: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Template name is required for Azure AD CS")
|
||||||
|
.describe("Certificate template name for Azure AD CS")
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External configuration schema for ACME Certificate Authority
|
||||||
|
*/
|
||||||
|
export const AcmeExternalConfigSchema = z.object({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of CA types to their corresponding external configuration schemas
|
||||||
|
*/
|
||||||
|
export const ExternalConfigSchemaMap = {
|
||||||
|
[CaType.AZURE_AD_CS]: AzureAdCsExternalConfigSchema,
|
||||||
|
[CaType.ACME]: AcmeExternalConfigSchema,
|
||||||
|
[CaType.INTERNAL]: z.object({}).optional() // Internal CAs don't use external configs
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const createExternalConfigSchema = (caType?: CaType | null) => {
|
||||||
|
if (!caType || caType === CaType.INTERNAL) {
|
||||||
|
return z.object({}).nullable().optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = ExternalConfigSchemaMap[caType];
|
||||||
|
if (!schema) {
|
||||||
|
return z.object({}).nullable().optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.nullable().optional();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union type of all possible external configuration schemas
|
||||||
|
*/
|
||||||
|
export const ExternalConfigUnionSchema = z
|
||||||
|
.union([AzureAdCsExternalConfigSchema, AcmeExternalConfigSchema, z.object({})])
|
||||||
|
.nullable()
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
export type TExternalConfig = z.infer<typeof ExternalConfigUnionSchema>;
|
||||||
@@ -12,8 +12,8 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
|||||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||||
import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||||
import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
import type { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
|
||||||
import type { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
import type { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||||
|
import type { TExternalCertificateAuthorityDALFactory } from "../certificate-authority/external-certificate-authority-dal";
|
||||||
import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||||
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
|
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
|
||||||
import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||||
@@ -100,6 +100,7 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
apiConfigId: "api-config-123",
|
apiConfigId: "api-config-123",
|
||||||
estConfigId: null,
|
estConfigId: null,
|
||||||
|
externalConfigs: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
@@ -229,17 +230,10 @@ describe("CertificateProfileService", () => {
|
|||||||
delete: vi.fn()
|
delete: vi.fn()
|
||||||
} as unknown as TCertificateAuthorityDALFactory;
|
} as unknown as TCertificateAuthorityDALFactory;
|
||||||
|
|
||||||
const mockCertificateAuthorityCertDAL = {
|
const mockExternalCertificateAuthorityDAL = {
|
||||||
create: vi.fn(),
|
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
updateById: vi.fn(),
|
findOne: vi.fn()
|
||||||
deleteById: vi.fn(),
|
} as unknown as Pick<TExternalCertificateAuthorityDALFactory, "findById" | "findOne">;
|
||||||
transaction: vi.fn(),
|
|
||||||
find: vi.fn(),
|
|
||||||
findOne: vi.fn(),
|
|
||||||
update: vi.fn(),
|
|
||||||
delete: vi.fn()
|
|
||||||
} as unknown as TCertificateAuthorityCertDALFactory;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
||||||
@@ -261,7 +255,7 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateBodyDAL: mockCertificateBodyDAL,
|
certificateBodyDAL: mockCertificateBodyDAL,
|
||||||
certificateSecretDAL: mockCertificateSecretDAL,
|
certificateSecretDAL: mockCertificateSecretDAL,
|
||||||
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
|
externalCertificateAuthorityDAL: mockExternalCertificateAuthorityDAL,
|
||||||
permissionService: mockPermissionService,
|
permissionService: mockPermissionService,
|
||||||
licenseService: mockLicenseService,
|
licenseService: mockLicenseService,
|
||||||
kmsService: mockKmsService,
|
kmsService: mockKmsService,
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
|||||||
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||||
import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns";
|
import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns";
|
||||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
|
||||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||||
|
import { CaType } from "../certificate-authority/certificate-authority-enums";
|
||||||
|
import { TExternalCertificateAuthorityDALFactory } from "../certificate-authority/external-certificate-authority-dal";
|
||||||
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||||
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
|
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
|
||||||
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||||
@@ -68,6 +69,55 @@ const validateIssuerTypeConstraints = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateTemplateByExternalCaType = (
|
||||||
|
externalCaType: CaType | undefined,
|
||||||
|
externalConfigs: Record<string, unknown> | null | undefined
|
||||||
|
) => {
|
||||||
|
if (!externalCaType) return;
|
||||||
|
|
||||||
|
switch (externalCaType) {
|
||||||
|
case CaType.AZURE_AD_CS:
|
||||||
|
if (!externalConfigs?.template || typeof externalConfigs.template !== "string") {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Azure ADCS Certificate Authority requires a template to be specified in external configs"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateExternalConfigs = async (
|
||||||
|
externalConfigs: Record<string, unknown> | null | undefined,
|
||||||
|
caId: string | null,
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">,
|
||||||
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "findOne">
|
||||||
|
) => {
|
||||||
|
if (!externalConfigs) return;
|
||||||
|
|
||||||
|
if (!caId) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "External configs can only be specified when a Certificate Authority is selected"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new NotFoundError({ message: "Certificate Authority not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const externalCa = await externalCertificateAuthorityDAL.findOne({ caId });
|
||||||
|
|
||||||
|
if (!externalCa) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "External configs can only be specified for external Certificate Authorities"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validateTemplateByExternalCaType(externalCa.type as CaType, externalConfigs);
|
||||||
|
};
|
||||||
|
|
||||||
const generateAndEncryptAcmeEabSecret = async (
|
const generateAndEncryptAcmeEabSecret = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
||||||
@@ -180,7 +230,7 @@ type TCertificateProfileServiceFactoryDep = {
|
|||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "findById" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
@@ -190,10 +240,22 @@ type TCertificateProfileServiceFactoryDep = {
|
|||||||
export type TCertificateProfileServiceFactory = ReturnType<typeof certificateProfileServiceFactory>;
|
export type TCertificateProfileServiceFactory = ReturnType<typeof certificateProfileServiceFactory>;
|
||||||
|
|
||||||
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
|
||||||
|
let parsedExternalConfigs: Record<string, unknown> | null = null;
|
||||||
|
if (dalResult.externalConfigs && typeof dalResult.externalConfigs === "string") {
|
||||||
|
try {
|
||||||
|
parsedExternalConfigs = JSON.parse(dalResult.externalConfigs) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
parsedExternalConfigs = null;
|
||||||
|
}
|
||||||
|
} else if (dalResult.externalConfigs && typeof dalResult.externalConfigs === "object") {
|
||||||
|
parsedExternalConfigs = dalResult.externalConfigs as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...dalResult,
|
...dalResult,
|
||||||
enrollmentType: dalResult.enrollmentType as EnrollmentType,
|
enrollmentType: dalResult.enrollmentType as EnrollmentType,
|
||||||
issuerType: dalResult.issuerType as IssuerType
|
issuerType: dalResult.issuerType as IssuerType,
|
||||||
|
externalConfigs: parsedExternalConfigs
|
||||||
} as TCertificateProfile;
|
} as TCertificateProfile;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,6 +267,8 @@ export const certificateProfileServiceFactory = ({
|
|||||||
acmeEnrollmentConfigDAL,
|
acmeEnrollmentConfigDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
@@ -272,6 +336,14 @@ export const certificateProfileServiceFactory = ({
|
|||||||
|
|
||||||
validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null);
|
validateIssuerTypeConstraints(data.issuerType, data.enrollmentType, data.caId ?? null);
|
||||||
|
|
||||||
|
// Validate external configs
|
||||||
|
await validateExternalConfigs(
|
||||||
|
data.externalConfigs,
|
||||||
|
data.caId ?? null,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL
|
||||||
|
);
|
||||||
|
|
||||||
// Validate enrollment configuration requirements
|
// Validate enrollment configuration requirements
|
||||||
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
@@ -340,7 +412,8 @@ export const certificateProfileServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
estConfigId,
|
estConfigId,
|
||||||
apiConfigId,
|
apiConfigId,
|
||||||
acmeConfigId
|
acmeConfigId,
|
||||||
|
externalConfigs: data.externalConfigs
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -414,6 +487,16 @@ export const certificateProfileServiceFactory = ({
|
|||||||
|
|
||||||
validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId);
|
validateIssuerTypeConstraints(finalIssuerType, finalEnrollmentType, finalCaId ?? null, existingProfile.caId);
|
||||||
|
|
||||||
|
// Validate external configs only if they are provided in the update
|
||||||
|
if (data.externalConfigs !== undefined) {
|
||||||
|
await validateExternalConfigs(
|
||||||
|
data.externalConfigs,
|
||||||
|
finalCaId ?? null,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedData =
|
const updatedData =
|
||||||
finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data;
|
finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data;
|
||||||
|
|
||||||
@@ -558,9 +641,24 @@ export const certificateProfileServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse externalConfigs from JSON string to object if it exists
|
||||||
|
let parsedExternalConfigs: Record<string, unknown> | null = null;
|
||||||
|
if (profile.externalConfigs && typeof profile.externalConfigs === "string") {
|
||||||
|
try {
|
||||||
|
parsedExternalConfigs = JSON.parse(profile.externalConfigs) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, leave as null
|
||||||
|
parsedExternalConfigs = null;
|
||||||
|
}
|
||||||
|
} else if (profile.externalConfigs && typeof profile.externalConfigs === "object") {
|
||||||
|
// Already an object, use as-is
|
||||||
|
parsedExternalConfigs = profile.externalConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...profile,
|
...profile,
|
||||||
enrollmentType: profile.enrollmentType as EnrollmentType
|
enrollmentType: profile.enrollmentType as EnrollmentType,
|
||||||
|
externalConfigs: parsedExternalConfigs
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,28 @@ export enum IssuerType {
|
|||||||
SELF_SIGNED = "self-signed"
|
SELF_SIGNED = "self-signed"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType" | "issuerType"> & {
|
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType" | "issuerType" | "externalConfigs"> & {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
issuerType: IssuerType;
|
issuerType: IssuerType;
|
||||||
|
externalConfigs?: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType" | "issuerType"> & {
|
export type TCertificateProfileInsert = Omit<
|
||||||
|
TPkiCertificateProfilesInsert,
|
||||||
|
"enrollmentType" | "issuerType" | "externalConfigs"
|
||||||
|
> & {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
issuerType: IssuerType;
|
issuerType: IssuerType;
|
||||||
|
externalConfigs?: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType" | "issuerType"> & {
|
export type TCertificateProfileUpdate = Omit<
|
||||||
|
TPkiCertificateProfilesUpdate,
|
||||||
|
"enrollmentType" | "issuerType" | "externalConfigs"
|
||||||
|
> & {
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
issuerType?: IssuerType;
|
issuerType?: IssuerType;
|
||||||
|
externalConfigs?: Record<string, unknown> | null;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
@@ -50,6 +59,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
status: string;
|
status: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
isExternal?: boolean;
|
||||||
|
externalType?: string;
|
||||||
};
|
};
|
||||||
certificateTemplate?: {
|
certificateTemplate?: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TCertificateRequests, TCertificates } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
type TCertificateRequestWithCertificate = TCertificateRequests & {
|
||||||
|
certificate: TCertificates | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateRequestDALFactory = ReturnType<typeof certificateRequestDALFactory>;
|
||||||
|
|
||||||
|
export const certificateRequestDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateRequestOrm = ormify(db, TableName.CertificateRequests);
|
||||||
|
|
||||||
|
const findByIdWithCertificate = async (id: string): Promise<TCertificateRequestWithCertificate | null> => {
|
||||||
|
try {
|
||||||
|
const certificateRequest = await certificateRequestOrm.findById(id);
|
||||||
|
if (!certificateRequest) return null;
|
||||||
|
|
||||||
|
if (!certificateRequest.certificateId) {
|
||||||
|
return {
|
||||||
|
...certificateRequest,
|
||||||
|
certificate: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificate = await db(TableName.Certificate)
|
||||||
|
.where("id", certificateRequest.certificateId)
|
||||||
|
.select(selectAllTableCols(TableName.Certificate))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...certificateRequest,
|
||||||
|
certificate: certificate || null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find certificate request by ID with certificate" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPendingByProjectId = async (projectId: string): Promise<TCertificateRequests[]> => {
|
||||||
|
try {
|
||||||
|
return (await db(TableName.CertificateRequests)
|
||||||
|
.where({ projectId, status: "pending" })
|
||||||
|
.orderBy("createdAt", "desc")) as TCertificateRequests[];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find pending certificate requests by project ID" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStatus = async (
|
||||||
|
id: string,
|
||||||
|
status: string,
|
||||||
|
errorMessage?: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TCertificateRequests> => {
|
||||||
|
try {
|
||||||
|
const updateData: Partial<TCertificateRequests> = { status };
|
||||||
|
if (errorMessage !== undefined) {
|
||||||
|
updateData.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
return await certificateRequestOrm.updateById(id, updateData, tx);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update certificate request status" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachCertificate = async (id: string, certificateId: string, tx?: Knex): Promise<TCertificateRequests> => {
|
||||||
|
try {
|
||||||
|
return await certificateRequestOrm.updateById(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
certificateId,
|
||||||
|
status: "issued"
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Attach certificate to request" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...certificateRequestOrm,
|
||||||
|
findByIdWithCertificate,
|
||||||
|
findPendingByProjectId,
|
||||||
|
updateStatus,
|
||||||
|
attachCertificate
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { createMongoAbility, ForbiddenError } from "@casl/ability";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSet,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
import { ActorType, AuthMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
|
|
||||||
|
import { TCertificateRequestDALFactory } from "./certificate-request-dal";
|
||||||
|
import { certificateRequestServiceFactory, TCertificateRequestServiceFactory } from "./certificate-request-service";
|
||||||
|
import { CertificateRequestStatus } from "./certificate-request-types";
|
||||||
|
|
||||||
|
describe("CertificateRequestService", () => {
|
||||||
|
let service: TCertificateRequestServiceFactory;
|
||||||
|
|
||||||
|
const mockCertificateRequestDAL: Pick<
|
||||||
|
TCertificateRequestDALFactory,
|
||||||
|
"create" | "findById" | "findByIdWithCertificate" | "updateStatus" | "attachCertificate"
|
||||||
|
> = {
|
||||||
|
create: vi.fn() as any,
|
||||||
|
findById: vi.fn() as any,
|
||||||
|
findByIdWithCertificate: vi.fn() as any,
|
||||||
|
updateStatus: vi.fn() as any,
|
||||||
|
attachCertificate: vi.fn() as any
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateDAL: Pick<TCertificateDALFactory, "findById"> = {
|
||||||
|
findById: vi.fn() as any
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateService: Pick<TCertificateServiceFactory, "getCertBody" | "getCertPrivateKey"> = {
|
||||||
|
getCertBody: vi.fn() as any,
|
||||||
|
getCertPrivateKey: vi.fn() as any
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPermissionService: Pick<TPermissionServiceFactory, "getProjectPermission"> = {
|
||||||
|
getProjectPermission: vi.fn() as any
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
service = certificateRequestServiceFactory({
|
||||||
|
certificateRequestDAL: mockCertificateRequestDAL as TCertificateRequestDALFactory,
|
||||||
|
certificateDAL: mockCertificateDAL,
|
||||||
|
certificateService: mockCertificateService,
|
||||||
|
permissionService: mockPermissionService
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCertificateRequest", () => {
|
||||||
|
const mockCreateData = {
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
profileId: "550e8400-e29b-41d4-a716-446655440004",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should create certificate request successfully", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Create,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockCreatedRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440005",
|
||||||
|
status: CertificateRequestStatus.PENDING,
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
profileId: "550e8400-e29b-41d4-a716-446655440004",
|
||||||
|
commonName: "test.example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.create as any).mockResolvedValue(mockCreatedRequest);
|
||||||
|
|
||||||
|
const result = await service.createCertificateRequest(mockCreateData);
|
||||||
|
|
||||||
|
expect(mockPermissionService.getProjectPermission).toHaveBeenCalledWith({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
expect(mockCertificateRequestDAL.create).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
status: CertificateRequestStatus.PENDING,
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
profileId: "550e8400-e29b-41d4-a716-446655440004",
|
||||||
|
commonName: "test.example.com"
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockCreatedRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw ForbiddenError when user lacks permission", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: ForbiddenError.from(createMongoAbility([]))
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
|
||||||
|
await expect(service.createCertificateRequest(mockCreateData)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getCertificateRequest", () => {
|
||||||
|
const mockGetData = {
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440005"
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should get certificate request successfully", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440005",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||||
|
|
||||||
|
const result = await service.getCertificateRequest(mockGetData);
|
||||||
|
|
||||||
|
expect(mockPermissionService.getProjectPermission).toHaveBeenCalledWith({
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
expect(mockCertificateRequestDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440005");
|
||||||
|
expect(result).toEqual(mockRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when certificate request does not exist", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw BadRequestError when certificate request belongs to different project", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440005",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440099",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||||
|
|
||||||
|
await expect(service.getCertificateRequest(mockGetData)).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getCertificateFromRequest", () => {
|
||||||
|
const mockGetData = {
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440005"
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should get certificate from request successfully when certificate is attached", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockCertificate = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440006",
|
||||||
|
serialNumber: "123456",
|
||||||
|
commonName: "test.example.com"
|
||||||
|
};
|
||||||
|
const mockRequestWithCert = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440005",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificate: mockCertificate,
|
||||||
|
errorMessage: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
const mockCertBody = {
|
||||||
|
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----"
|
||||||
|
};
|
||||||
|
const mockPrivateKey = {
|
||||||
|
certPrivateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----"
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert);
|
||||||
|
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
|
||||||
|
(mockCertificateService.getCertPrivateKey as any).mockResolvedValue(mockPrivateKey);
|
||||||
|
|
||||||
|
const result = await service.getCertificateFromRequest(mockGetData);
|
||||||
|
|
||||||
|
expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith(
|
||||||
|
"550e8400-e29b-41d4-a716-446655440005"
|
||||||
|
);
|
||||||
|
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440006",
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||||
|
});
|
||||||
|
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440006",
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----",
|
||||||
|
privateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----",
|
||||||
|
serialNumber: "123456",
|
||||||
|
errorMessage: null,
|
||||||
|
createdAt: mockRequestWithCert.createdAt,
|
||||||
|
updatedAt: mockRequestWithCert.updatedAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get certificate from request successfully when no certificate is attached", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockRequestWithoutCert = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440007",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
status: CertificateRequestStatus.PENDING,
|
||||||
|
certificate: null,
|
||||||
|
errorMessage: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithoutCert);
|
||||||
|
|
||||||
|
const result = await service.getCertificateFromRequest(mockGetData);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: CertificateRequestStatus.PENDING,
|
||||||
|
certificate: null,
|
||||||
|
privateKey: null,
|
||||||
|
serialNumber: null,
|
||||||
|
errorMessage: null,
|
||||||
|
createdAt: mockRequestWithoutCert.createdAt,
|
||||||
|
updatedAt: mockRequestWithoutCert.updatedAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get certificate from request successfully when private key access is denied", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockCertificate = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440008",
|
||||||
|
serialNumber: "123456",
|
||||||
|
commonName: "test.example.com"
|
||||||
|
};
|
||||||
|
const mockRequestWithCert = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440005",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificate: mockCertificate,
|
||||||
|
errorMessage: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
const mockCertBody = {
|
||||||
|
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----"
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert);
|
||||||
|
(mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody);
|
||||||
|
(mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key access denied"));
|
||||||
|
|
||||||
|
const result = await service.getCertificateFromRequest(mockGetData);
|
||||||
|
|
||||||
|
expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith(
|
||||||
|
"550e8400-e29b-41d4-a716-446655440005"
|
||||||
|
);
|
||||||
|
expect(mockCertificateService.getCertBody).toHaveBeenCalledWith({
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440008",
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||||
|
});
|
||||||
|
expect(mockCertificateService.getCertPrivateKey).toHaveBeenCalledWith({
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440008",
|
||||||
|
actor: ActorType.USER,
|
||||||
|
actorId: "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
actorAuthMethod: AuthMethod.EMAIL,
|
||||||
|
actorOrgId: "550e8400-e29b-41d4-a716-446655440002"
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----",
|
||||||
|
privateKey: null,
|
||||||
|
serialNumber: "123456",
|
||||||
|
errorMessage: null,
|
||||||
|
createdAt: mockRequestWithCert.createdAt,
|
||||||
|
updatedAt: mockRequestWithCert.updatedAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get certificate from request with error message when failed", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const mockFailedRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440010",
|
||||||
|
projectId: "550e8400-e29b-41d4-a716-446655440003",
|
||||||
|
status: CertificateRequestStatus.FAILED,
|
||||||
|
certificate: null,
|
||||||
|
errorMessage: "Certificate issuance failed",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockFailedRequest);
|
||||||
|
|
||||||
|
const result = await service.getCertificateFromRequest(mockGetData);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: CertificateRequestStatus.FAILED,
|
||||||
|
certificate: null,
|
||||||
|
privateKey: null,
|
||||||
|
serialNumber: null,
|
||||||
|
errorMessage: "Certificate issuance failed",
|
||||||
|
createdAt: mockFailedRequest.createdAt,
|
||||||
|
updatedAt: mockFailedRequest.updatedAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when certificate request does not exist", async () => {
|
||||||
|
const mockPermission = {
|
||||||
|
permission: createMongoAbility<ProjectPermissionSet>([
|
||||||
|
{
|
||||||
|
action: ProjectPermissionCertificateActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Certificates
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission);
|
||||||
|
(mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getCertificateFromRequest(mockGetData)).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateCertificateRequestStatus", () => {
|
||||||
|
it("should update certificate request status successfully", async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440011",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
const mockUpdatedRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440011",
|
||||||
|
status: CertificateRequestStatus.ISSUED
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||||
|
(mockCertificateRequestDAL.updateStatus as any).mockResolvedValue(mockUpdatedRequest);
|
||||||
|
|
||||||
|
const result = await service.updateCertificateRequestStatus({
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440011",
|
||||||
|
status: CertificateRequestStatus.ISSUED
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateRequestDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440011");
|
||||||
|
expect(mockCertificateRequestDAL.updateStatus).toHaveBeenCalledWith(
|
||||||
|
"550e8400-e29b-41d4-a716-446655440011",
|
||||||
|
CertificateRequestStatus.ISSUED,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockUpdatedRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update certificate request status with error message", async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440012",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
const mockUpdatedRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440012",
|
||||||
|
status: CertificateRequestStatus.FAILED
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||||
|
(mockCertificateRequestDAL.updateStatus as any).mockResolvedValue(mockUpdatedRequest);
|
||||||
|
|
||||||
|
const result = await service.updateCertificateRequestStatus({
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440012",
|
||||||
|
status: CertificateRequestStatus.FAILED,
|
||||||
|
errorMessage: "Certificate issuance failed"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateRequestDAL.updateStatus).toHaveBeenCalledWith(
|
||||||
|
"550e8400-e29b-41d4-a716-446655440012",
|
||||||
|
CertificateRequestStatus.FAILED,
|
||||||
|
"Certificate issuance failed"
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockUpdatedRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when certificate request does not exist", async () => {
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateCertificateRequestStatus({
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440013",
|
||||||
|
status: CertificateRequestStatus.ISSUED
|
||||||
|
})
|
||||||
|
).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("attachCertificateToRequest", () => {
|
||||||
|
it("should attach certificate to request successfully", async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440014",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
const mockCertificate = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440015"
|
||||||
|
};
|
||||||
|
const mockUpdatedRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440014",
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificateId: "550e8400-e29b-41d4-a716-446655440015"
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||||
|
(mockCertificateDAL.findById as any).mockResolvedValue(mockCertificate);
|
||||||
|
(mockCertificateRequestDAL.attachCertificate as any).mockResolvedValue(mockUpdatedRequest);
|
||||||
|
|
||||||
|
const result = await service.attachCertificateToRequest({
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440014",
|
||||||
|
certificateId: "550e8400-e29b-41d4-a716-446655440015"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCertificateRequestDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440014");
|
||||||
|
expect(mockCertificateDAL.findById).toHaveBeenCalledWith("550e8400-e29b-41d4-a716-446655440015");
|
||||||
|
expect(mockCertificateRequestDAL.attachCertificate).toHaveBeenCalledWith(
|
||||||
|
"550e8400-e29b-41d4-a716-446655440014",
|
||||||
|
"550e8400-e29b-41d4-a716-446655440015"
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockUpdatedRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when certificate request does not exist", async () => {
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.attachCertificateToRequest({
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440016",
|
||||||
|
certificateId: "550e8400-e29b-41d4-a716-446655440017"
|
||||||
|
})
|
||||||
|
).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when certificate does not exist", async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
id: "550e8400-e29b-41d4-a716-446655440018",
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
};
|
||||||
|
|
||||||
|
(mockCertificateRequestDAL.findById as any).mockResolvedValue(mockRequest);
|
||||||
|
(mockCertificateDAL.findById as any).mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.attachCertificateToRequest({
|
||||||
|
certificateRequestId: "550e8400-e29b-41d4-a716-446655440018",
|
||||||
|
certificateId: "550e8400-e29b-41d4-a716-446655440019"
|
||||||
|
})
|
||||||
|
).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TCertificateRequestDALFactory } from "./certificate-request-dal";
|
||||||
|
import {
|
||||||
|
CertificateRequestStatus,
|
||||||
|
TAttachCertificateToRequestDTO,
|
||||||
|
TCreateCertificateRequestDTO,
|
||||||
|
TGetCertificateFromRequestDTO,
|
||||||
|
TGetCertificateRequestDTO,
|
||||||
|
TUpdateCertificateRequestStatusDTO
|
||||||
|
} from "./certificate-request-types";
|
||||||
|
|
||||||
|
type TCertificateRequestServiceFactoryDep = {
|
||||||
|
certificateRequestDAL: TCertificateRequestDALFactory;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||||
|
certificateService: Pick<TCertificateServiceFactory, "getCertBody" | "getCertPrivateKey">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateRequestServiceFactory = ReturnType<typeof certificateRequestServiceFactory>;
|
||||||
|
|
||||||
|
const certificateRequestDataSchema = z
|
||||||
|
.object({
|
||||||
|
profileId: z.string().uuid().optional(),
|
||||||
|
caId: z.string().uuid().optional(),
|
||||||
|
csr: z.string().min(1).optional(),
|
||||||
|
commonName: z.string().max(255).optional(),
|
||||||
|
altNames: z.string().max(1000).optional(),
|
||||||
|
keyUsages: z.array(z.string()).max(20).optional(),
|
||||||
|
extendedKeyUsages: z.array(z.string()).max(20).optional(),
|
||||||
|
notBefore: z.date().optional(),
|
||||||
|
notAfter: z.date().optional(),
|
||||||
|
keyAlgorithm: z.string().max(100).optional(),
|
||||||
|
signatureAlgorithm: z.string().max(100).optional(),
|
||||||
|
metadata: z.string().max(2000).optional(),
|
||||||
|
certificateId: z.string().optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
// Must have either profileId or caId
|
||||||
|
return data.profileId || data.caId;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Either profileId or caId must be provided"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
// If notAfter is provided, it must be after notBefore
|
||||||
|
if (data.notBefore && data.notAfter) {
|
||||||
|
return data.notAfter > data.notBefore;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "notAfter must be after notBefore"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateCertificateRequestData = (data: unknown) => {
|
||||||
|
try {
|
||||||
|
return certificateRequestDataSchema.parse(data);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid certificate request data: ${error.errors.map((e) => e.message).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const certificateRequestServiceFactory = ({
|
||||||
|
certificateRequestDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateService,
|
||||||
|
permissionService
|
||||||
|
}: TCertificateRequestServiceFactoryDep) => {
|
||||||
|
const createCertificateRequest = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId,
|
||||||
|
tx,
|
||||||
|
status,
|
||||||
|
...requestData
|
||||||
|
}: TCreateCertificateRequestDTO & { tx?: Knex }) => {
|
||||||
|
if (actor !== ActorType.ACME_ACCOUNT) {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Create,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input data before creating the request
|
||||||
|
const validatedData = validateCertificateRequestData(requestData);
|
||||||
|
|
||||||
|
const certificateRequest = await certificateRequestDAL.create(
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
projectId,
|
||||||
|
...validatedData
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCertificateRequest = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId,
|
||||||
|
certificateRequestId
|
||||||
|
}: TGetCertificateRequestDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certificateRequest = await certificateRequestDAL.findById(certificateRequestId);
|
||||||
|
if (!certificateRequest) {
|
||||||
|
throw new NotFoundError({ message: "Certificate request not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificateRequest.projectId !== projectId) {
|
||||||
|
throw new NotFoundError({ message: "Certificate request not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCertificateFromRequest = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId,
|
||||||
|
certificateRequestId
|
||||||
|
}: TGetCertificateFromRequestDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionCertificateActions.Read,
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
|
const certificateRequest = await certificateRequestDAL.findByIdWithCertificate(certificateRequestId);
|
||||||
|
if (!certificateRequest) {
|
||||||
|
throw new NotFoundError({ message: "Certificate request not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificateRequest.projectId !== projectId) {
|
||||||
|
throw new NotFoundError({ message: "Certificate request not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no certificate is attached, return basic info
|
||||||
|
if (!certificateRequest.certificate) {
|
||||||
|
return {
|
||||||
|
status: certificateRequest.status as CertificateRequestStatus,
|
||||||
|
certificate: null,
|
||||||
|
privateKey: null,
|
||||||
|
serialNumber: null,
|
||||||
|
errorMessage: certificateRequest.errorMessage || null,
|
||||||
|
createdAt: certificateRequest.createdAt,
|
||||||
|
updatedAt: certificateRequest.updatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get certificate body (PEM data)
|
||||||
|
const certBody = await certificateService.getCertBody({
|
||||||
|
id: certificateRequest.certificate.id,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to get private key (may fail if user doesn't have permission)
|
||||||
|
let privateKey: string | null = null;
|
||||||
|
try {
|
||||||
|
const certPrivateKey = await certificateService.getCertPrivateKey({
|
||||||
|
id: certificateRequest.certificate.id,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
});
|
||||||
|
privateKey = certPrivateKey.certPrivateKey;
|
||||||
|
} catch (error) {
|
||||||
|
// Private key access denied - continue without it
|
||||||
|
privateKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: certificateRequest.status as CertificateRequestStatus,
|
||||||
|
certificate: certBody.certificate,
|
||||||
|
privateKey,
|
||||||
|
serialNumber: certificateRequest.certificate.serialNumber,
|
||||||
|
errorMessage: certificateRequest.errorMessage || null,
|
||||||
|
createdAt: certificateRequest.createdAt,
|
||||||
|
updatedAt: certificateRequest.updatedAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCertificateRequestStatus = async ({
|
||||||
|
certificateRequestId,
|
||||||
|
status,
|
||||||
|
errorMessage
|
||||||
|
}: TUpdateCertificateRequestStatusDTO) => {
|
||||||
|
const certificateRequest = await certificateRequestDAL.findById(certificateRequestId);
|
||||||
|
if (!certificateRequest) {
|
||||||
|
throw new NotFoundError({ message: "Certificate request not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequestDAL.updateStatus(certificateRequestId, status, errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachCertificateToRequest = async ({
|
||||||
|
certificateRequestId,
|
||||||
|
certificateId
|
||||||
|
}: TAttachCertificateToRequestDTO) => {
|
||||||
|
const certificateRequest = await certificateRequestDAL.findById(certificateRequestId);
|
||||||
|
if (!certificateRequest) {
|
||||||
|
throw new NotFoundError({ message: "Certificate request not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (!certificate) {
|
||||||
|
throw new NotFoundError({ message: "Certificate not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateRequestDAL.attachCertificate(certificateRequestId, certificateId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createCertificateRequest,
|
||||||
|
getCertificateRequest,
|
||||||
|
getCertificateFromRequest,
|
||||||
|
updateCertificateRequestStatus,
|
||||||
|
attachCertificateToRequest
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum CertificateRequestStatus {
|
||||||
|
PENDING = "pending",
|
||||||
|
ISSUED = "issued",
|
||||||
|
FAILED = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateCertificateRequestDTO = TProjectPermission & {
|
||||||
|
profileId?: string;
|
||||||
|
caId?: string;
|
||||||
|
csr?: string;
|
||||||
|
commonName?: string;
|
||||||
|
altNames?: string;
|
||||||
|
keyUsages?: string[];
|
||||||
|
extendedKeyUsages?: string[];
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
|
keyAlgorithm?: string;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
metadata?: string;
|
||||||
|
status: CertificateRequestStatus;
|
||||||
|
certificateId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCertificateRequestDTO = TProjectPermission & {
|
||||||
|
certificateRequestId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCertificateFromRequestDTO = TProjectPermission & {
|
||||||
|
certificateRequestId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateCertificateRequestStatusDTO = {
|
||||||
|
certificateRequestId: string;
|
||||||
|
status: CertificateRequestStatus;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAttachCertificateToRequestDTO = {
|
||||||
|
certificateRequestId: string;
|
||||||
|
certificateId: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
export const parseTtlToDays = (ttl: string): number => {
|
||||||
|
const match = ttl.match(new RE2("^(\\d+)([dhm])$"));
|
||||||
|
if (!match) {
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL format: ${ttl}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, value, unit] = match;
|
||||||
|
const num = parseInt(value, 10);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case "d":
|
||||||
|
return num;
|
||||||
|
case "h":
|
||||||
|
return Math.ceil(num / 24);
|
||||||
|
case "m":
|
||||||
|
return Math.ceil(num / (24 * 60));
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ message: `Invalid TTL unit: ${unit}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateRenewalThreshold = (
|
||||||
|
profileRenewBeforeDays: number | undefined,
|
||||||
|
certificateTtlInDays: number
|
||||||
|
): number | undefined => {
|
||||||
|
if (profileRenewBeforeDays === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileRenewBeforeDays >= certificateTtlInDays) {
|
||||||
|
// If renewBeforeDays >= TTL, renew 1 day before expiry
|
||||||
|
return Math.max(1, certificateTtlInDays - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileRenewBeforeDays;
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ import { TPkiAcmeAccountDALFactory } from "@app/ee/services/pki-acme/pki-acme-ac
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import { ACMESANType, CertificateOrderStatus, CertStatus } from "@app/services/certificate/certificate-types";
|
import { CertStatus } from "@app/services/certificate/certificate-types";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
CertIncludeType,
|
CertIncludeType,
|
||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
CertSubjectAttributeType
|
CertSubjectAttributeType,
|
||||||
|
CertSubjectAlternativeNameType
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
@@ -42,7 +43,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
const mockCertificateDAL: Pick<
|
const mockCertificateDAL: Pick<
|
||||||
TCertificateDALFactory,
|
TCertificateDALFactory,
|
||||||
"findOne" | "findById" | "updateById" | "transaction" | "create"
|
"findOne" | "findById" | "updateById" | "transaction" | "create" | "find"
|
||||||
> = {
|
> = {
|
||||||
findOne: vi.fn(),
|
findOne: vi.fn(),
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
@@ -57,7 +58,8 @@ describe("CertificateV3Service", () => {
|
|||||||
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
const mockTx = {};
|
const mockTx = {};
|
||||||
return callback(mockTx);
|
return callback(mockTx);
|
||||||
})
|
}),
|
||||||
|
find: vi.fn().mockResolvedValue([])
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create"> = {
|
const mockCertificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create"> = {
|
||||||
@@ -65,12 +67,24 @@ describe("CertificateV3Service", () => {
|
|||||||
create: vi.fn()
|
create: vi.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa"> = {
|
const mockCertificateAuthorityDAL: Pick<
|
||||||
findByIdWithAssociatedCa: vi.fn()
|
TCertificateAuthorityDALFactory,
|
||||||
|
"findByIdWithAssociatedCa" | "create" | "updateById" | "findById" | "transaction" | "findWithAssociatedCa"
|
||||||
|
> = {
|
||||||
|
findByIdWithAssociatedCa: vi.fn(),
|
||||||
|
create: vi.fn().mockResolvedValue({ id: "ca-123" }),
|
||||||
|
updateById: vi.fn().mockResolvedValue({ id: "ca-123" }),
|
||||||
|
findById: vi.fn().mockResolvedValue({ id: "ca-123" }),
|
||||||
|
findWithAssociatedCa: vi.fn().mockResolvedValue([]),
|
||||||
|
transaction: vi.fn().mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
const mockTx = {};
|
||||||
|
return callback(mockTx);
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs"> = {
|
const mockCertificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs" | "findById"> = {
|
||||||
findByIdWithConfigs: vi.fn()
|
findByIdWithConfigs: vi.fn(),
|
||||||
|
findById: vi.fn()
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateTemplateV2Service: Pick<
|
const mockCertificateTemplateV2Service: Pick<
|
||||||
@@ -168,7 +182,11 @@ describe("CertificateV3Service", () => {
|
|||||||
kmsService: {
|
kmsService: {
|
||||||
generateKmsKey: vi.fn().mockResolvedValue("kms-key-123"),
|
generateKmsKey: vi.fn().mockResolvedValue("kms-key-123"),
|
||||||
encryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("encrypted"))),
|
encryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("encrypted"))),
|
||||||
decryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("decrypted")))
|
decryptWithKmsKey: vi.fn().mockResolvedValue(vi.fn().mockResolvedValue(Buffer.from("decrypted"))),
|
||||||
|
createCipherPairWithDataKey: vi.fn().mockResolvedValue({
|
||||||
|
cipherTextBlob: Buffer.from("encrypted"),
|
||||||
|
plainTextKey: Buffer.from("plainkey")
|
||||||
|
})
|
||||||
},
|
},
|
||||||
projectDAL: {
|
projectDAL: {
|
||||||
findOne: vi.fn().mockResolvedValue({ id: "project-123" }),
|
findOne: vi.fn().mockResolvedValue({ id: "project-123" }),
|
||||||
@@ -178,7 +196,13 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockTx = {};
|
const mockTx = {};
|
||||||
return callback(mockTx);
|
return callback(mockTx);
|
||||||
})
|
})
|
||||||
} as any
|
} as any,
|
||||||
|
certificateIssuanceQueue: {
|
||||||
|
queueCertificateIssuance: vi.fn().mockResolvedValue(undefined)
|
||||||
|
},
|
||||||
|
certificateRequestService: {
|
||||||
|
createCertificateRequest: vi.fn().mockResolvedValue({ id: "cert-req-123" })
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,6 +291,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "issuing-ca",
|
issuingCaCertificate: "issuing-ca",
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
serialNumber: "123456",
|
serialNumber: "123456",
|
||||||
|
certificateId: "cert-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: {
|
ca: {
|
||||||
id: "ca-123",
|
id: "ca-123",
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
@@ -326,8 +352,13 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
|
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
const result = await service.issueCertificateFromProfile({
|
const result = await service.issueCertificateFromProfile({
|
||||||
profileId,
|
profileId,
|
||||||
certificateRequest: mockCertificateRequest,
|
certificateRequest: mockCertificateRequest,
|
||||||
@@ -461,6 +492,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "issuing-ca",
|
issuingCaCertificate: "issuing-ca",
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
serialNumber: "123456",
|
serialNumber: "123456",
|
||||||
|
certificateId: "cert-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: {
|
ca: {
|
||||||
id: "ca-123",
|
id: "ca-123",
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
@@ -503,8 +536,34 @@ describe("CertificateV3Service", () => {
|
|||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
||||||
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any);
|
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResultWithCa as any);
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||||
|
id: "cert-1",
|
||||||
|
serialNumber: "123456",
|
||||||
|
status: "ACTIVE",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
projectId: "project-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
friendlyName: "Test Algorithm Cert",
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(),
|
||||||
|
caId: "ca-1",
|
||||||
|
certificateTemplateId: "template-1",
|
||||||
|
revokedAt: null,
|
||||||
|
altNames: null,
|
||||||
|
caCertId: null,
|
||||||
|
keyUsages: null,
|
||||||
|
extendedKeyUsages: null,
|
||||||
|
revocationReason: null,
|
||||||
|
pkiSubscriberId: null,
|
||||||
|
profileId: null
|
||||||
|
});
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
await service.issueCertificateFromProfile({
|
await service.issueCertificateFromProfile({
|
||||||
profileId,
|
profileId,
|
||||||
certificateRequest: camelCaseRequest,
|
certificateRequest: camelCaseRequest,
|
||||||
@@ -729,8 +788,13 @@ describe("CertificateV3Service", () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
vi.mocked(mockInternalCaService.signCertFromCa).mockResolvedValue(mockSignResult as any);
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockCertRecord);
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
||||||
|
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
const result = await service.signCertificateFromProfile({
|
const result = await service.signCertificateFromProfile({
|
||||||
profileId,
|
profileId,
|
||||||
csr: mockCSR,
|
csr: mockCSR,
|
||||||
@@ -790,7 +854,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
describe("orderCertificateFromProfile", () => {
|
describe("orderCertificateFromProfile", () => {
|
||||||
const mockCertificateOrder = {
|
const mockCertificateOrder = {
|
||||||
altNames: [{ type: ACMESANType.DNS, value: "example.com" }],
|
altNames: [{ type: CertSubjectAlternativeNameType.DNS_NAME, value: "example.com" }],
|
||||||
validity: { ttl: "30d" },
|
validity: { ttl: "30d" },
|
||||||
commonName: "example.com",
|
commonName: "example.com",
|
||||||
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||||
@@ -799,168 +863,6 @@ describe("CertificateV3Service", () => {
|
|||||||
keyAlgorithm: "RSA_2048"
|
keyAlgorithm: "RSA_2048"
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should create order successfully for API enrollment profile", async () => {
|
|
||||||
const profileId = "profile-123";
|
|
||||||
const mockProfile = {
|
|
||||||
id: profileId,
|
|
||||||
projectId: "project-123",
|
|
||||||
enrollmentType: EnrollmentType.API,
|
|
||||||
issuerType: IssuerType.CA,
|
|
||||||
caId: "ca-123",
|
|
||||||
certificateTemplateId: "template-123",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
slug: "test-profile-order",
|
|
||||||
description: "Test order profile",
|
|
||||||
estConfigId: null,
|
|
||||||
apiConfigId: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCA = {
|
|
||||||
id: "ca-123",
|
|
||||||
projectId: "project-123",
|
|
||||||
externalCa: undefined,
|
|
||||||
internalCa: {
|
|
||||||
id: "internal-ca-123",
|
|
||||||
parentCaId: null,
|
|
||||||
type: "ROOT",
|
|
||||||
friendlyName: "Test CA",
|
|
||||||
organization: "Test Org",
|
|
||||||
ou: "Test OU",
|
|
||||||
country: "US",
|
|
||||||
province: "CA",
|
|
||||||
locality: "SF",
|
|
||||||
commonName: "Test CA",
|
|
||||||
dn: "CN=Test CA",
|
|
||||||
serialNumber: "123",
|
|
||||||
maxPathLength: null,
|
|
||||||
keyAlgorithm: "RSA_2048",
|
|
||||||
notBefore: undefined,
|
|
||||||
notAfter: undefined,
|
|
||||||
activeCaCertId: "cert-123",
|
|
||||||
caId: "ca-123"
|
|
||||||
},
|
|
||||||
name: "Test CA",
|
|
||||||
status: "ACTIVE",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
enableDirectIssuance: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockTemplate = {
|
|
||||||
id: "template-123",
|
|
||||||
name: "Test Order Template",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
projectId: "project-123",
|
|
||||||
description: "Test template for ordering certificates",
|
|
||||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
|
||||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
type: CertSubjectAttributeType.COMMON_NAME,
|
|
||||||
include: CertIncludeType.OPTIONAL,
|
|
||||||
value: ["example.com"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
subject: undefined,
|
|
||||||
sans: undefined,
|
|
||||||
keyUsages: undefined,
|
|
||||||
extendedKeyUsages: undefined,
|
|
||||||
algorithms: undefined,
|
|
||||||
validity: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCertificateResult = {
|
|
||||||
certificate: "cert",
|
|
||||||
certificateChain: "chain",
|
|
||||||
issuingCaCertificate: "issuing-ca",
|
|
||||||
privateKey: "key",
|
|
||||||
serialNumber: "123456",
|
|
||||||
ca: {
|
|
||||||
id: "ca-123",
|
|
||||||
projectId: "project-123",
|
|
||||||
name: "Test CA",
|
|
||||||
status: "ACTIVE",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
enableDirectIssuance: true,
|
|
||||||
externalCa: undefined,
|
|
||||||
internalCa: {
|
|
||||||
id: "internal-ca-123",
|
|
||||||
parentCaId: null,
|
|
||||||
type: "ROOT",
|
|
||||||
friendlyName: "Test CA",
|
|
||||||
organization: "Test Org",
|
|
||||||
ou: "Test OU",
|
|
||||||
country: "US",
|
|
||||||
province: "CA",
|
|
||||||
locality: "SF",
|
|
||||||
commonName: "Test CA",
|
|
||||||
dn: "CN=Test CA",
|
|
||||||
serialNumber: "123",
|
|
||||||
maxPathLength: null,
|
|
||||||
keyAlgorithm: "RSA_2048",
|
|
||||||
notBefore: null,
|
|
||||||
notAfter: null,
|
|
||||||
activeCaCertId: "cert-123",
|
|
||||||
caId: "ca-123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCertRecord = {
|
|
||||||
id: "cert-123",
|
|
||||||
serialNumber: "123456",
|
|
||||||
status: "ACTIVE",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
projectId: "project-123",
|
|
||||||
commonName: "example.com",
|
|
||||||
friendlyName: "Test Order Cert",
|
|
||||||
notBefore: new Date(),
|
|
||||||
notAfter: new Date(),
|
|
||||||
caId: "ca-123",
|
|
||||||
certificateTemplateId: "template-123",
|
|
||||||
revokedAt: null,
|
|
||||||
altNames: JSON.stringify([{ type: "DNS", value: "example.com" }]),
|
|
||||||
caCertId: null,
|
|
||||||
keyUsages: ["DIGITAL_SIGNATURE"],
|
|
||||||
extendedKeyUsages: ["SERVER_AUTH"],
|
|
||||||
revocationReason: null,
|
|
||||||
pkiSubscriberId: null,
|
|
||||||
profileId: null
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
|
||||||
vi.mocked(mockCertificateTemplateV2Service.validateCertificateRequest).mockResolvedValue({
|
|
||||||
isValid: true,
|
|
||||||
errors: [],
|
|
||||||
warnings: []
|
|
||||||
});
|
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
|
||||||
vi.mocked(mockCertificateTemplateV2Service.getTemplateV2ById).mockResolvedValue(mockTemplate);
|
|
||||||
vi.mocked(mockInternalCaService.issueCertFromCa).mockResolvedValue(mockCertificateResult as any);
|
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue(mockCertRecord);
|
|
||||||
vi.mocked(mockCertificateDAL.updateById).mockResolvedValue(mockCertRecord);
|
|
||||||
|
|
||||||
const result = await service.orderCertificateFromProfile({
|
|
||||||
profileId,
|
|
||||||
certificateOrder: mockCertificateOrder,
|
|
||||||
...mockActor
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toHaveProperty("orderId");
|
|
||||||
expect(result).toHaveProperty("status", "valid");
|
|
||||||
expect(result).toHaveProperty("certificate");
|
|
||||||
expect(result.subjectAlternativeNames).toHaveLength(1);
|
|
||||||
expect(result.subjectAlternativeNames[0]).toEqual({
|
|
||||||
type: ACMESANType.DNS,
|
|
||||||
value: "example.com",
|
|
||||||
status: CertificateOrderStatus.VALID
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => {
|
it("should throw ForbiddenRequestError when profile is not configured for API enrollment", async () => {
|
||||||
const profileId = "profile-123";
|
const profileId = "profile-123";
|
||||||
const mockProfile = {
|
const mockProfile = {
|
||||||
@@ -1097,6 +999,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "ca-cert",
|
issuingCaCertificate: "ca-cert",
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
serialNumber: "123456",
|
serialNumber: "123456",
|
||||||
|
certificateId: "cert-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: rsaCa as any
|
ca: rsaCa as any
|
||||||
});
|
});
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||||
@@ -1143,6 +1047,31 @@ describe("CertificateV3Service", () => {
|
|||||||
pkiSubscriberId: null,
|
pkiSubscriberId: null,
|
||||||
profileId: null
|
profileId: null
|
||||||
});
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||||
|
id: "cert-1",
|
||||||
|
serialNumber: "123456",
|
||||||
|
status: "ACTIVE",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
projectId: "project-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
friendlyName: "Test Algorithm Cert",
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(),
|
||||||
|
caId: "ca-1",
|
||||||
|
certificateTemplateId: "template-1",
|
||||||
|
revokedAt: null,
|
||||||
|
altNames: null,
|
||||||
|
caCertId: null,
|
||||||
|
keyUsages: null,
|
||||||
|
extendedKeyUsages: null,
|
||||||
|
revocationReason: null,
|
||||||
|
pkiSubscriberId: null,
|
||||||
|
profileId: null
|
||||||
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
// Should not throw - RSA CA is compatible with RSA signature algorithms
|
// Should not throw - RSA CA is compatible with RSA signature algorithms
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1229,6 +1158,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "ca-cert",
|
issuingCaCertificate: "ca-cert",
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
serialNumber: "123456",
|
serialNumber: "123456",
|
||||||
|
certificateId: "cert-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: ecCa as any
|
ca: ecCa as any
|
||||||
});
|
});
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||||
@@ -1275,6 +1206,31 @@ describe("CertificateV3Service", () => {
|
|||||||
pkiSubscriberId: null,
|
pkiSubscriberId: null,
|
||||||
profileId: null
|
profileId: null
|
||||||
});
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||||
|
id: "cert-1",
|
||||||
|
serialNumber: "123456",
|
||||||
|
status: "ACTIVE",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
projectId: "project-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
friendlyName: "Test Algorithm Cert",
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(),
|
||||||
|
caId: "ca-1",
|
||||||
|
certificateTemplateId: "template-1",
|
||||||
|
revokedAt: null,
|
||||||
|
altNames: null,
|
||||||
|
caCertId: null,
|
||||||
|
keyUsages: null,
|
||||||
|
extendedKeyUsages: null,
|
||||||
|
revocationReason: null,
|
||||||
|
pkiSubscriberId: null,
|
||||||
|
profileId: null
|
||||||
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
// Should not throw - EC CA is compatible with ECDSA signature algorithms
|
// Should not throw - EC CA is compatible with ECDSA signature algorithms
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1361,6 +1317,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "ca-cert",
|
issuingCaCertificate: "ca-cert",
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
serialNumber: "123456",
|
serialNumber: "123456",
|
||||||
|
certificateId: "cert-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: rsa8192Ca as any
|
ca: rsa8192Ca as any
|
||||||
});
|
});
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||||
@@ -1407,6 +1365,31 @@ describe("CertificateV3Service", () => {
|
|||||||
pkiSubscriberId: null,
|
pkiSubscriberId: null,
|
||||||
profileId: null
|
profileId: null
|
||||||
});
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||||
|
id: "cert-1",
|
||||||
|
serialNumber: "123456",
|
||||||
|
status: "ACTIVE",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
projectId: "project-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
friendlyName: "Test Algorithm Cert",
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(),
|
||||||
|
caId: "ca-1",
|
||||||
|
certificateTemplateId: "template-1",
|
||||||
|
revokedAt: null,
|
||||||
|
altNames: null,
|
||||||
|
caCertId: null,
|
||||||
|
keyUsages: null,
|
||||||
|
extendedKeyUsages: null,
|
||||||
|
revocationReason: null,
|
||||||
|
pkiSubscriberId: null,
|
||||||
|
profileId: null
|
||||||
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
// Should not throw - dynamic check supports new RSA key sizes
|
// Should not throw - dynamic check supports new RSA key sizes
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1493,6 +1476,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "ca-cert",
|
issuingCaCertificate: "ca-cert",
|
||||||
privateKey: "key",
|
privateKey: "key",
|
||||||
serialNumber: "123456",
|
serialNumber: "123456",
|
||||||
|
certificateId: "cert-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: newEcCa as any
|
ca: newEcCa as any
|
||||||
});
|
});
|
||||||
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
vi.mocked(mockCertificateDAL.findOne).mockResolvedValue({
|
||||||
@@ -1539,6 +1524,31 @@ describe("CertificateV3Service", () => {
|
|||||||
pkiSubscriberId: null,
|
pkiSubscriberId: null,
|
||||||
profileId: null
|
profileId: null
|
||||||
});
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.findById).mockResolvedValue({
|
||||||
|
id: "cert-1",
|
||||||
|
serialNumber: "123456",
|
||||||
|
status: "ACTIVE",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
projectId: "project-1",
|
||||||
|
commonName: "test.example.com",
|
||||||
|
friendlyName: "Test Algorithm Cert",
|
||||||
|
notBefore: new Date(),
|
||||||
|
notAfter: new Date(),
|
||||||
|
caId: "ca-1",
|
||||||
|
certificateTemplateId: "template-1",
|
||||||
|
revokedAt: null,
|
||||||
|
altNames: null,
|
||||||
|
caCertId: null,
|
||||||
|
keyUsages: null,
|
||||||
|
extendedKeyUsages: null,
|
||||||
|
revocationReason: null,
|
||||||
|
pkiSubscriberId: null,
|
||||||
|
profileId: null
|
||||||
|
});
|
||||||
|
vi.mocked(mockCertificateDAL.transaction).mockImplementation(async (callback: (tx: any) => Promise<unknown>) => {
|
||||||
|
return callback(undefined as any);
|
||||||
|
});
|
||||||
|
|
||||||
// Should not throw - dynamic check supports new EC curves
|
// Should not throw - dynamic check supports new EC curves
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1672,8 +1682,9 @@ describe("CertificateV3Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should successfully renew eligible certificate", async () => {
|
it("should successfully renew eligible certificate", async () => {
|
||||||
// Mock the initial findById call
|
vi.mocked(mockCertificateDAL.findById)
|
||||||
vi.mocked(mockCertificateDAL.findById).mockResolvedValue(mockOriginalCert);
|
.mockResolvedValueOnce(mockOriginalCert)
|
||||||
|
.mockResolvedValueOnce({ ...mockOriginalCert, id: "cert-456", serialNumber: "789012" });
|
||||||
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
vi.mocked(mockCertificateSecretDAL.findOne).mockResolvedValue({ id: "secret-123", certId: "cert-123" } as any);
|
||||||
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
vi.mocked(mockCertificateProfileDAL.findByIdWithConfigs).mockResolvedValue(mockProfile);
|
||||||
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
vi.mocked(mockCertificateAuthorityDAL.findByIdWithAssociatedCa).mockResolvedValue(mockCA);
|
||||||
@@ -1689,6 +1700,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "issuing-ca",
|
issuingCaCertificate: "issuing-ca",
|
||||||
privateKey: "private-key",
|
privateKey: "private-key",
|
||||||
serialNumber: "789012",
|
serialNumber: "789012",
|
||||||
|
certificateId: "cert-456",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: mockCA
|
ca: mockCA
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2006,6 +2019,8 @@ describe("CertificateV3Service", () => {
|
|||||||
issuingCaCertificate: "issuing-ca",
|
issuingCaCertificate: "issuing-ca",
|
||||||
privateKey: "private-key",
|
privateKey: "private-key",
|
||||||
serialNumber: "789012",
|
serialNumber: "789012",
|
||||||
|
certificateId: "cert-456",
|
||||||
|
commonName: "test.example.com",
|
||||||
ca: mockCA
|
ca: mockCA
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,8 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat
|
|||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsage,
|
|
||||||
CertificateOrderStatus,
|
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertKeyType,
|
CertKeyType,
|
||||||
CertKeyUsage,
|
|
||||||
CertSignatureAlgorithm,
|
CertSignatureAlgorithm,
|
||||||
CertStatus
|
CertStatus
|
||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
@@ -59,15 +56,16 @@ import {
|
|||||||
bufferToString,
|
bufferToString,
|
||||||
buildCertificateSubjectFromTemplate,
|
buildCertificateSubjectFromTemplate,
|
||||||
buildSubjectAlternativeNamesFromTemplate,
|
buildSubjectAlternativeNamesFromTemplate,
|
||||||
convertExtendedKeyUsageArrayFromLegacy,
|
|
||||||
convertExtendedKeyUsageArrayToLegacy,
|
convertExtendedKeyUsageArrayToLegacy,
|
||||||
convertKeyUsageArrayFromLegacy,
|
|
||||||
convertKeyUsageArrayToLegacy,
|
convertKeyUsageArrayToLegacy,
|
||||||
mapEnumsForValidation,
|
mapEnumsForValidation,
|
||||||
normalizeDateForApi,
|
normalizeDateForApi,
|
||||||
removeRootCaFromChain
|
removeRootCaFromChain
|
||||||
} from "../certificate-common/certificate-utils";
|
} from "../certificate-common/certificate-utils";
|
||||||
|
import { TCertificateRequestServiceFactory } from "../certificate-request/certificate-request-service";
|
||||||
|
import { CertificateRequestStatus } from "../certificate-request/certificate-request-types";
|
||||||
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { TCertificateRequest } from "../certificate-template-v2/certificate-template-v2-types";
|
||||||
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||||
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||||
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
|
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
|
||||||
@@ -85,11 +83,17 @@ import {
|
|||||||
} from "./certificate-v3-types";
|
} from "./certificate-v3-types";
|
||||||
|
|
||||||
type TCertificateV3ServiceFactoryDep = {
|
type TCertificateV3ServiceFactoryDep = {
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "findById" | "updateById" | "transaction" | "create">;
|
certificateDAL: Pick<
|
||||||
|
TCertificateDALFactory,
|
||||||
|
"findOne" | "findById" | "updateById" | "transaction" | "create" | "find"
|
||||||
|
>;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
|
TCertificateAuthorityDALFactory,
|
||||||
|
"findByIdWithAssociatedCa" | "create" | "transaction" | "updateById" | "findWithAssociatedCa" | "findById"
|
||||||
|
>;
|
||||||
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs" | "findById">;
|
||||||
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
|
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById">;
|
||||||
certificateTemplateV2Service: Pick<
|
certificateTemplateV2Service: Pick<
|
||||||
TCertificateTemplateV2ServiceFactory,
|
TCertificateTemplateV2ServiceFactory,
|
||||||
@@ -103,8 +107,16 @@ type TCertificateV3ServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
kmsService: Pick<
|
||||||
|
TKmsServiceFactory,
|
||||||
|
"generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey" | "createCipherPairWithDataKey"
|
||||||
|
>;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
certificateIssuanceQueue: Pick<
|
||||||
|
import("../certificate-authority/certificate-issuance-queue").TCertificateIssuanceQueueFactory,
|
||||||
|
"queueCertificateIssuance"
|
||||||
|
>;
|
||||||
|
certificateRequestService: Pick<TCertificateRequestServiceFactory, "createCertificateRequest">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||||
@@ -292,16 +304,62 @@ const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | s
|
|||||||
return bufferToString(certData as unknown as Buffer);
|
return bufferToString(certData as unknown as Buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseKeyUsages = (keyUsages: unknown): CertKeyUsage[] => {
|
const parseKeyUsages = (keyUsages: unknown): CertKeyUsageType[] => {
|
||||||
if (!keyUsages) return [];
|
if (!keyUsages) return [];
|
||||||
if (Array.isArray(keyUsages)) return keyUsages as CertKeyUsage[];
|
|
||||||
return (keyUsages as string).split(",").map((usage) => usage.trim() as CertKeyUsage);
|
const validKeyUsages = Object.values(CertKeyUsageType);
|
||||||
|
|
||||||
|
if (Array.isArray(keyUsages)) {
|
||||||
|
return keyUsages.filter(
|
||||||
|
(usage): usage is CertKeyUsageType =>
|
||||||
|
typeof usage === "string" && validKeyUsages.includes(usage as CertKeyUsageType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof keyUsages === "string") {
|
||||||
|
return keyUsages
|
||||||
|
.split(",")
|
||||||
|
.map((usage) => usage.trim())
|
||||||
|
.filter((usage): usage is CertKeyUsageType => validKeyUsages.includes(usage as CertKeyUsageType));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsage[] => {
|
const parseExtendedKeyUsages = (extendedKeyUsages: unknown): CertExtendedKeyUsageType[] => {
|
||||||
if (!extendedKeyUsages) return [];
|
if (!extendedKeyUsages) return [];
|
||||||
if (Array.isArray(extendedKeyUsages)) return extendedKeyUsages as CertExtendedKeyUsage[];
|
|
||||||
return (extendedKeyUsages as string).split(",").map((usage) => usage.trim() as CertExtendedKeyUsage);
|
const validExtendedKeyUsages = Object.values(CertExtendedKeyUsageType);
|
||||||
|
|
||||||
|
if (Array.isArray(extendedKeyUsages)) {
|
||||||
|
return extendedKeyUsages.filter(
|
||||||
|
(usage): usage is CertExtendedKeyUsageType =>
|
||||||
|
typeof usage === "string" && validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof extendedKeyUsages === "string") {
|
||||||
|
return extendedKeyUsages
|
||||||
|
.split(",")
|
||||||
|
.map((usage) => usage.trim())
|
||||||
|
.filter((usage): usage is CertExtendedKeyUsageType =>
|
||||||
|
validExtendedKeyUsages.includes(usage as CertExtendedKeyUsageType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertEnumsToStringArray = <T extends string>(enumArray: T[]): string[] => {
|
||||||
|
return enumArray.map((item) => item as string);
|
||||||
|
};
|
||||||
|
|
||||||
|
const combineKeyUsageFlags = (keyUsages: string[]): number => {
|
||||||
|
return keyUsages.reduce((acc: number, usage) => {
|
||||||
|
const flag = x509.KeyUsageFlags[usage as keyof typeof x509.KeyUsageFlags];
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
return typeof flag === "number" ? acc | flag : acc;
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValidRenewalTiming = (renewBeforeDays: number, certificateExpiryDate: Date): boolean => {
|
const isValidRenewalTiming = (renewBeforeDays: number, certificateExpiryDate: Date): boolean => {
|
||||||
@@ -441,11 +499,7 @@ const generateSelfSignedCertificate = async ({
|
|||||||
...(certificateRequest.keyUsages?.length
|
...(certificateRequest.keyUsages?.length
|
||||||
? [
|
? [
|
||||||
new x509.KeyUsagesExtension(
|
new x509.KeyUsagesExtension(
|
||||||
(convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || []).reduce(
|
combineKeyUsageFlags(convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || []),
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
(acc: number, usage) => acc | x509.KeyUsageFlags[usage],
|
|
||||||
0
|
|
||||||
),
|
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -759,6 +813,37 @@ const processSelfSignedCertificate = async ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const detectSanType = (value: string): { type: CertSubjectAlternativeNameType; value: string } => {
|
||||||
|
const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(value);
|
||||||
|
const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(value);
|
||||||
|
|
||||||
|
if (isIpv4 || isIpv6) {
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(value)) {
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.EMAIL,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(value)) {
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.URI,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: CertSubjectAlternativeNameType.DNS_NAME,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const certificateV3ServiceFactory = ({
|
export const certificateV3ServiceFactory = ({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
@@ -773,7 +858,9 @@ export const certificateV3ServiceFactory = ({
|
|||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue,
|
pkiSyncQueue,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
certificateIssuanceQueue,
|
||||||
|
certificateRequestService
|
||||||
}: TCertificateV3ServiceFactoryDep) => {
|
}: TCertificateV3ServiceFactoryDep) => {
|
||||||
const issueCertificateFromProfile = async ({
|
const issueCertificateFromProfile = async ({
|
||||||
profileId,
|
profileId,
|
||||||
@@ -857,7 +944,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
const result = await certificateDAL.transaction(async (tx) => {
|
const result = await certificateDAL.transaction(async (tx) => {
|
||||||
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
const effectiveAlgorithms = getEffectiveAlgorithms(effectiveSignatureAlgorithm, effectiveKeyAlgorithm);
|
||||||
|
|
||||||
return processSelfSignedCertificate({
|
const selfSignedResult = await processSelfSignedCertificate({
|
||||||
certificateRequest,
|
certificateRequest,
|
||||||
template,
|
template,
|
||||||
profile,
|
profile,
|
||||||
@@ -869,9 +956,31 @@ export const certificateV3ServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
tx,
|
||||||
|
profileId: profile.id,
|
||||||
|
commonName: certificateRequest.commonName,
|
||||||
|
altNames: certificateRequest.altNames?.map((san) => san.value).join(","),
|
||||||
|
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages),
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages),
|
||||||
|
notBefore: certificateRequest.notBefore,
|
||||||
|
notAfter: certificateRequest.notAfter,
|
||||||
|
keyAlgorithm: effectiveKeyAlgorithm,
|
||||||
|
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificateId: selfSignedResult.certificateData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...selfSignedResult, certificateRequestId: certRequestResult.id };
|
||||||
});
|
});
|
||||||
|
|
||||||
const { selfSignedResult, certificateData } = result;
|
const { selfSignedResult, certificateData, certificateRequestId } = result;
|
||||||
|
|
||||||
const subjectCommonName =
|
const subjectCommonName =
|
||||||
(selfSignedResult.certificateSubject.common_name as string) ||
|
(selfSignedResult.certificateSubject.common_name as string) ||
|
||||||
@@ -897,6 +1006,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
privateKey: selfSignedResult.privateKey.toString("utf8"),
|
privateKey: selfSignedResult.privateKey.toString("utf8"),
|
||||||
serialNumber: selfSignedResult.serialNumber,
|
serialNumber: selfSignedResult.serialNumber,
|
||||||
certificateId: certificateData.id,
|
certificateId: certificateData.id,
|
||||||
|
certificateRequestId,
|
||||||
projectId: profile.projectId,
|
projectId: profile.projectId,
|
||||||
profileName: profile.slug,
|
profileName: profile.slug,
|
||||||
commonName: subjectCommonName
|
commonName: subjectCommonName
|
||||||
@@ -915,8 +1025,16 @@ export const certificateV3ServiceFactory = ({
|
|||||||
validateCaSupport(ca, "direct certificate issuance");
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
validateAlgorithmCompatibility(ca, template);
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
const {
|
||||||
await internalCaService.issueCertFromCa({
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey,
|
||||||
|
serialNumber,
|
||||||
|
cert,
|
||||||
|
certificateRequestId
|
||||||
|
} = await certificateDAL.transaction(async (tx) => {
|
||||||
|
const certResult = await internalCaService.issueCertFromCa({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
friendlyName: certificateSubject.common_name || "Certificate",
|
friendlyName: certificateSubject.common_name || "Certificate",
|
||||||
commonName: certificateSubject.common_name || "",
|
commonName: certificateSubject.common_name || "",
|
||||||
@@ -932,25 +1050,50 @@ export const certificateV3ServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
isFromProfile: true
|
isFromProfile: true,
|
||||||
|
tx
|
||||||
});
|
});
|
||||||
|
|
||||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
const certificateRecord = await certificateDAL.findById(certResult.certificateId, tx);
|
||||||
if (!cert) {
|
if (!certificateRecord) {
|
||||||
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
profile,
|
profile,
|
||||||
certificateRequest.validity.ttl,
|
certificateRequest.validity.ttl,
|
||||||
new Date(cert.notAfter)
|
new Date(certificateRecord.notAfter)
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||||
if (finalRenewBeforeDays !== undefined) {
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
updateData.renewBeforeDays = finalRenewBeforeDays;
|
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||||
}
|
}
|
||||||
await certificateDAL.updateById(cert.id, updateData);
|
await certificateDAL.updateById(certificateRecord.id, updateData, tx);
|
||||||
|
|
||||||
|
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
tx,
|
||||||
|
caId: ca.id,
|
||||||
|
profileId: profile.id,
|
||||||
|
commonName: certificateRequest.commonName,
|
||||||
|
altNames: certificateRequest.altNames?.map((san) => san.value).join(","),
|
||||||
|
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages),
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages),
|
||||||
|
notBefore: certificateRequest.notBefore,
|
||||||
|
notAfter: certificateRequest.notAfter,
|
||||||
|
keyAlgorithm: effectiveKeyAlgorithm,
|
||||||
|
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificateId: certResult.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...certResult, cert: certificateRecord, certificateRequestId: certRequestResult.id };
|
||||||
|
});
|
||||||
|
|
||||||
let finalCertificateChain = bufferToString(certificateChain);
|
let finalCertificateChain = bufferToString(certificateChain);
|
||||||
if (removeRootsFromChain) {
|
if (removeRootsFromChain) {
|
||||||
@@ -964,6 +1107,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
privateKey: bufferToString(privateKey),
|
privateKey: bufferToString(privateKey),
|
||||||
serialNumber,
|
serialNumber,
|
||||||
certificateId: cert.id,
|
certificateId: cert.id,
|
||||||
|
certificateRequestId,
|
||||||
projectId: profile.projectId,
|
projectId: profile.projectId,
|
||||||
profileName: profile.slug,
|
profileName: profile.slug,
|
||||||
commonName: cert.commonName || ""
|
commonName: cert.commonName || ""
|
||||||
@@ -1047,33 +1191,64 @@ export const certificateV3ServiceFactory = ({
|
|||||||
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
|
const effectiveSignatureAlgorithm = extractedSignatureAlgorithm;
|
||||||
const effectiveKeyAlgorithm = extractedKeyAlgorithm;
|
const effectiveKeyAlgorithm = extractedKeyAlgorithm;
|
||||||
|
|
||||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber, cert, certificateRequestId } =
|
||||||
await internalCaService.signCertFromCa({
|
await certificateDAL.transaction(async (tx) => {
|
||||||
isInternal: true,
|
const certResult = await internalCaService.signCertFromCa({
|
||||||
caId: ca.id,
|
isInternal: true,
|
||||||
csr,
|
caId: ca.id,
|
||||||
ttl: validity.ttl,
|
csr,
|
||||||
altNames: undefined,
|
ttl: validity.ttl,
|
||||||
notBefore: normalizeDateForApi(notBefore),
|
altNames: undefined,
|
||||||
notAfter: normalizeDateForApi(notAfter),
|
notBefore: normalizeDateForApi(notBefore),
|
||||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
notAfter: normalizeDateForApi(notAfter),
|
||||||
keyAlgorithm: effectiveKeyAlgorithm,
|
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||||
isFromProfile: true
|
keyAlgorithm: effectiveKeyAlgorithm,
|
||||||
|
isFromProfile: true,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
const signedCertRecord = await certificateDAL.findById(certResult.certificateId, tx);
|
||||||
|
if (!signedCertRecord) {
|
||||||
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(
|
||||||
|
profile,
|
||||||
|
validity.ttl,
|
||||||
|
new Date(signedCertRecord.notAfter)
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateData: { profileId: string; renewBeforeDays?: number } = { profileId };
|
||||||
|
if (finalRenewBeforeDays !== undefined) {
|
||||||
|
updateData.renewBeforeDays = finalRenewBeforeDays;
|
||||||
|
}
|
||||||
|
await certificateDAL.updateById(signedCertRecord.id, updateData, tx);
|
||||||
|
|
||||||
|
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
tx,
|
||||||
|
caId: ca.id,
|
||||||
|
profileId: profile.id,
|
||||||
|
csr,
|
||||||
|
commonName: mappedCertificateRequest.commonName,
|
||||||
|
altNames: mappedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","),
|
||||||
|
keyUsages: convertKeyUsageArrayToLegacy(mappedCertificateRequest.keyUsages),
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(mappedCertificateRequest.extendedKeyUsages),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
keyAlgorithm: effectiveKeyAlgorithm,
|
||||||
|
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificateId: certResult.certificateId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...certResult, cert: signedCertRecord, certificateRequestId: certRequestResult.id };
|
||||||
});
|
});
|
||||||
|
|
||||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
|
||||||
if (!cert) {
|
|
||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalRenewBeforeDays = calculateFinalRenewBeforeDays(profile, validity.ttl, new Date(cert.notAfter));
|
|
||||||
|
|
||||||
const updateData2: { profileId: string; renewBeforeDays?: number } = { profileId };
|
|
||||||
if (finalRenewBeforeDays !== undefined) {
|
|
||||||
updateData2.renewBeforeDays = finalRenewBeforeDays;
|
|
||||||
}
|
|
||||||
await certificateDAL.updateById(cert.id, updateData2);
|
|
||||||
|
|
||||||
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
|
||||||
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
let certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
|
||||||
if (removeRootsFromChain) {
|
if (removeRootsFromChain) {
|
||||||
@@ -1086,6 +1261,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
certificateChain: certificateChainString,
|
certificateChain: certificateChainString,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
certificateId: cert.id,
|
certificateId: cert.id,
|
||||||
|
certificateRequestId,
|
||||||
projectId: profile.projectId,
|
projectId: profile.projectId,
|
||||||
profileName: profile.slug,
|
profileName: profile.slug,
|
||||||
commonName: cert.commonName || ""
|
commonName: cert.commonName || ""
|
||||||
@@ -1098,8 +1274,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId
|
||||||
removeRootsFromChain
|
|
||||||
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
|
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
|
||||||
const profile = await validateProfileAndPermissions(
|
const profile = await validateProfileAndPermissions(
|
||||||
profileId,
|
profileId,
|
||||||
@@ -1113,37 +1288,41 @@ export const certificateV3ServiceFactory = ({
|
|||||||
EnrollmentType.API
|
EnrollmentType.API
|
||||||
);
|
);
|
||||||
|
|
||||||
const certificateRequest = {
|
let certificateRequest: TCertificateRequest;
|
||||||
commonName: certificateOrder.commonName,
|
let extractedKeyAlgorithm: string | undefined;
|
||||||
keyUsages: certificateOrder.keyUsages,
|
let extractedSignatureAlgorithm: string | undefined;
|
||||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
|
||||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => {
|
if (certificateOrder.csr) {
|
||||||
let certType: CertSubjectAlternativeNameType;
|
certificateRequest = extractCertificateRequestFromCSR(certificateOrder.csr);
|
||||||
switch (san.type) {
|
const algorithms = extractAlgorithmsFromCSR(certificateOrder.csr);
|
||||||
case "dns":
|
extractedKeyAlgorithm = algorithms.keyAlgorithm;
|
||||||
certType = CertSubjectAlternativeNameType.DNS_NAME;
|
extractedSignatureAlgorithm = algorithms.signatureAlgorithm;
|
||||||
break;
|
certificateRequest.validity = certificateOrder.validity;
|
||||||
case "ip":
|
if (certificateOrder.notBefore && certificateOrder.notAfter) {
|
||||||
certType = CertSubjectAlternativeNameType.IP_ADDRESS;
|
certificateRequest.notBefore = certificateOrder.notBefore;
|
||||||
break;
|
certificateRequest.notAfter = certificateOrder.notAfter;
|
||||||
default:
|
}
|
||||||
throw new BadRequestError({
|
} else {
|
||||||
message: `Unsupported Subject Alternative Name type: ${san.type as string}`
|
certificateRequest = {
|
||||||
});
|
commonName: certificateOrder.commonName,
|
||||||
}
|
keyUsages: certificateOrder.keyUsages,
|
||||||
return {
|
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||||
type: certType,
|
subjectAlternativeNames: certificateOrder.altNames,
|
||||||
value: san.value
|
validity: certificateOrder.validity,
|
||||||
};
|
notBefore: certificateOrder.notBefore,
|
||||||
}),
|
notAfter: certificateOrder.notAfter,
|
||||||
validity: certificateOrder.validity,
|
signatureAlgorithm: certificateOrder.signatureAlgorithm,
|
||||||
notBefore: certificateOrder.notBefore,
|
keyAlgorithm: certificateOrder.keyAlgorithm
|
||||||
notAfter: certificateOrder.notAfter,
|
};
|
||||||
signatureAlgorithm: certificateOrder.signatureAlgorithm,
|
}
|
||||||
keyAlgorithm: certificateOrder.keyAlgorithm
|
|
||||||
};
|
|
||||||
|
|
||||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||||
|
|
||||||
|
if (certificateOrder.csr) {
|
||||||
|
mappedCertificateRequest.keyAlgorithm = extractedKeyAlgorithm;
|
||||||
|
mappedCertificateRequest.signatureAlgorithm = extractedSignatureAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
profile.certificateTemplateId,
|
profile.certificateTemplateId,
|
||||||
mappedCertificateRequest
|
mappedCertificateRequest
|
||||||
@@ -1169,42 +1348,61 @@ export const certificateV3ServiceFactory = ({
|
|||||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||||
|
|
||||||
if (caType === CaType.INTERNAL) {
|
if (caType === CaType.INTERNAL) {
|
||||||
const certificateResult = await issueCertificateFromProfile({
|
throw new BadRequestError({
|
||||||
profileId,
|
message: "Certificate ordering is not supported for the specified CA type"
|
||||||
certificateRequest,
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) {
|
||||||
|
const orderId = randomUUID();
|
||||||
|
|
||||||
|
const certRequest = await certificateRequestService.createCertificateRequest({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
removeRootsFromChain
|
projectId: profile.projectId,
|
||||||
|
caId: ca.id,
|
||||||
|
profileId: profile.id,
|
||||||
|
commonName: certificateOrder.commonName || "",
|
||||||
|
keyUsages: certificateOrder.keyUsages ? convertEnumsToStringArray(certificateOrder.keyUsages) : [],
|
||||||
|
extendedKeyUsages: certificateOrder.extendedKeyUsages
|
||||||
|
? convertEnumsToStringArray(certificateOrder.extendedKeyUsages)
|
||||||
|
: [],
|
||||||
|
keyAlgorithm: certificateOrder.keyAlgorithm || "",
|
||||||
|
signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
|
||||||
|
altNames: certificateOrder.altNames?.map((san) => san.value).join(",") || "",
|
||||||
|
notBefore: certificateOrder.notBefore,
|
||||||
|
notAfter: certificateOrder.notAfter,
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
});
|
});
|
||||||
|
|
||||||
const orderId = randomUUID();
|
await certificateIssuanceQueue.queueCertificateIssuance({
|
||||||
|
certificateId: orderId,
|
||||||
|
profileId: profile.id,
|
||||||
|
caId: profile.caId || "",
|
||||||
|
ttl: certificateOrder.validity?.ttl || "1y",
|
||||||
|
signatureAlgorithm: certificateOrder.signatureAlgorithm || "",
|
||||||
|
keyAlgorithm: certificateRequest.keyAlgorithm || "",
|
||||||
|
commonName: certificateRequest.commonName || "",
|
||||||
|
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value) || [],
|
||||||
|
keyUsages: certificateRequest.keyUsages ? convertEnumsToStringArray(certificateRequest.keyUsages) : [],
|
||||||
|
extendedKeyUsages: certificateRequest.extendedKeyUsages
|
||||||
|
? convertEnumsToStringArray(certificateRequest.extendedKeyUsages)
|
||||||
|
: [],
|
||||||
|
certificateRequestId: certRequest.id,
|
||||||
|
csr: certificateOrder.csr
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orderId,
|
certificateRequestId: certRequest.id,
|
||||||
status: CertificateOrderStatus.VALID,
|
projectId: certRequest.projectId,
|
||||||
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
profileName: profile.slug
|
||||||
type: san.type,
|
|
||||||
value: san.value,
|
|
||||||
status: CertificateOrderStatus.VALID
|
|
||||||
})),
|
|
||||||
authorizations: [],
|
|
||||||
finalize: `/api/v1/cert-manager/certificates/orders/${orderId}/completed`,
|
|
||||||
certificate: certificateResult.certificate,
|
|
||||||
projectId: certificateResult.projectId,
|
|
||||||
profileName: certificateResult.profileName
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caType === CaType.ACME) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "ACME certificate ordering via profiles is not yet implemented."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Certificate ordering is not supported for CA type: ${caType}`
|
message: "Certificate ordering is not supported for the specified CA type"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1216,7 +1414,9 @@ export const certificateV3ServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
internal = false,
|
internal = false,
|
||||||
removeRootsFromChain
|
removeRootsFromChain
|
||||||
}: TRenewCertificateDTO & { internal?: boolean }): Promise<TCertificateFromProfileResponse> => {
|
}: Omit<TRenewCertificateDTO, "certificateRequestId"> & {
|
||||||
|
internal?: boolean;
|
||||||
|
}): Promise<TCertificateFromProfileResponse> => {
|
||||||
const renewalResult = await certificateDAL.transaction(async (tx) => {
|
const renewalResult = await certificateDAL.transaction(async (tx) => {
|
||||||
const originalCert = await certificateDAL.findById(certificateId, tx);
|
const originalCert = await certificateDAL.findById(certificateId, tx);
|
||||||
if (!originalCert) {
|
if (!originalCert) {
|
||||||
@@ -1229,14 +1429,30 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalSignatureAlgorithm = originalCert.signatureAlgorithm as CertSignatureAlgorithm;
|
// Validate and cast algorithms with fallbacks
|
||||||
const originalKeyAlgorithm = originalCert.keyAlgorithm as CertKeyAlgorithm;
|
let originalSignatureAlgorithm = Object.values(CertSignatureAlgorithm).includes(
|
||||||
|
originalCert.signatureAlgorithm as CertSignatureAlgorithm
|
||||||
|
)
|
||||||
|
? (originalCert.signatureAlgorithm as CertSignatureAlgorithm)
|
||||||
|
: CertSignatureAlgorithm.RSA_SHA256;
|
||||||
|
let originalKeyAlgorithm = Object.values(CertKeyAlgorithm).includes(originalCert.keyAlgorithm as CertKeyAlgorithm)
|
||||||
|
? (originalCert.keyAlgorithm as CertKeyAlgorithm)
|
||||||
|
: CertKeyAlgorithm.RSA_2048;
|
||||||
|
|
||||||
|
// For external CA certificates without stored algorithm info, extract from certificate
|
||||||
if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
|
if (!originalSignatureAlgorithm || !originalKeyAlgorithm) {
|
||||||
throw new BadRequestError({
|
const isExternalCA = originalCert.caId && !originalCert.caId.startsWith("internal");
|
||||||
message:
|
|
||||||
"Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented."
|
if (isExternalCA) {
|
||||||
});
|
// For external CA certificates, we can extract algorithm info from the cert or use defaults
|
||||||
|
originalSignatureAlgorithm = originalSignatureAlgorithm || CertSignatureAlgorithm.RSA_SHA256;
|
||||||
|
originalKeyAlgorithm = originalKeyAlgorithm || CertKeyAlgorithm.RSA_2048;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Original certificate does not have algorithm information stored. Cannot renew certificate issued before algorithm tracking was implemented."
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile = null;
|
let profile = null;
|
||||||
@@ -1303,7 +1519,10 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validateCaSupport(ca, "direct certificate issuance");
|
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||||
|
if (caType === CaType.INTERNAL) {
|
||||||
|
validateCaSupport(ca, "direct certificate issuance");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId;
|
const templateId = profile?.certificateTemplateId || originalCert.certificateTemplateId;
|
||||||
@@ -1329,42 +1548,10 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
const certificateRequest = {
|
const certificateRequest = {
|
||||||
commonName: originalCert.commonName || undefined,
|
commonName: originalCert.commonName || undefined,
|
||||||
keyUsages: convertKeyUsageArrayFromLegacy(parseKeyUsages(originalCert.keyUsages)),
|
keyUsages: parseKeyUsages(originalCert.keyUsages),
|
||||||
extendedKeyUsages: convertExtendedKeyUsageArrayFromLegacy(
|
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages),
|
||||||
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
|
|
||||||
),
|
|
||||||
subjectAlternativeNames: originalCert.altNames
|
subjectAlternativeNames: originalCert.altNames
|
||||||
? originalCert.altNames.split(",").map((san) => {
|
? originalCert.altNames.split(",").map((san) => detectSanType(san.trim()))
|
||||||
const trimmed = san.trim();
|
|
||||||
|
|
||||||
const isIpv4 = new RE2("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$").test(trimmed);
|
|
||||||
const isIpv6 = new RE2("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$").test(trimmed);
|
|
||||||
if (isIpv4 || isIpv6) {
|
|
||||||
return {
|
|
||||||
type: CertSubjectAlternativeNameType.IP_ADDRESS,
|
|
||||||
value: trimmed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new RE2("^[^@]+@[^@]+\\.[^@]+$").test(trimmed)) {
|
|
||||||
return {
|
|
||||||
type: CertSubjectAlternativeNameType.EMAIL,
|
|
||||||
value: trimmed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new RE2("^[a-zA-Z][a-zA-Z0-9+.-]*:").test(trimmed)) {
|
|
||||||
return {
|
|
||||||
type: CertSubjectAlternativeNameType.URI,
|
|
||||||
value: trimmed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: CertSubjectAlternativeNameType.DNS_NAME,
|
|
||||||
value: trimmed
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [],
|
: [],
|
||||||
validity: {
|
validity: {
|
||||||
ttl
|
ttl
|
||||||
@@ -1408,41 +1595,66 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate Authority not found for CA-signed certificate renewal" });
|
throw new NotFoundError({ message: "Certificate Authority not found for CA-signed certificate renewal" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAlgorithmCompatibility(ca, {
|
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||||
algorithms: template?.algorithms
|
|
||||||
} as { algorithms?: { signature?: string[] } });
|
|
||||||
|
|
||||||
const caResult = await internalCaService.issueCertFromCa({
|
// Only validate algorithm compatibility for internal CAs
|
||||||
caId: ca.id,
|
if (caType === CaType.INTERNAL) {
|
||||||
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
validateAlgorithmCompatibility(ca, {
|
||||||
commonName: originalCert.commonName || "",
|
algorithms: template?.algorithms
|
||||||
altNames: originalCert.altNames || "",
|
} as { algorithms?: { signature?: string[] } });
|
||||||
ttl,
|
}
|
||||||
notBefore: normalizeDateForApi(notBefore),
|
|
||||||
notAfter: normalizeDateForApi(notAfter),
|
|
||||||
keyUsages: parseKeyUsages(originalCert.keyUsages),
|
|
||||||
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages),
|
|
||||||
signatureAlgorithm: originalSignatureAlgorithm,
|
|
||||||
keyAlgorithm: originalKeyAlgorithm,
|
|
||||||
isFromProfile: true,
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
internal: true,
|
|
||||||
tx
|
|
||||||
});
|
|
||||||
|
|
||||||
certificate = caResult.certificate;
|
if (caType === CaType.INTERNAL) {
|
||||||
certificateChain = caResult.certificateChain;
|
// Internal CA renewal - existing logic
|
||||||
issuingCaCertificate = caResult.issuingCaCertificate;
|
const caResult = await internalCaService.issueCertFromCa({
|
||||||
serialNumber = caResult.serialNumber;
|
caId: ca.id,
|
||||||
|
friendlyName: originalCert.friendlyName || originalCert.commonName || "Renewed Certificate",
|
||||||
|
commonName: originalCert.commonName || "",
|
||||||
|
altNames: originalCert.altNames || "",
|
||||||
|
ttl,
|
||||||
|
notBefore: normalizeDateForApi(notBefore),
|
||||||
|
notAfter: normalizeDateForApi(notAfter),
|
||||||
|
keyUsages: convertKeyUsageArrayToLegacy(parseKeyUsages(originalCert.keyUsages)),
|
||||||
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(
|
||||||
|
parseExtendedKeyUsages(originalCert.extendedKeyUsages)
|
||||||
|
),
|
||||||
|
signatureAlgorithm: originalSignatureAlgorithm,
|
||||||
|
keyAlgorithm: originalKeyAlgorithm,
|
||||||
|
isFromProfile: true,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
internal: true,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
const foundCert = await certificateDAL.findOne({ serialNumber, caId: ca.id }, tx);
|
certificate = caResult.certificate;
|
||||||
if (!foundCert) {
|
certificateChain = caResult.certificateChain;
|
||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
issuingCaCertificate = caResult.issuingCaCertificate;
|
||||||
|
serialNumber = caResult.serialNumber;
|
||||||
|
|
||||||
|
const foundCert = await certificateDAL.findById(caResult.certificateId, tx);
|
||||||
|
if (!foundCert) {
|
||||||
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
|
}
|
||||||
|
newCert = foundCert;
|
||||||
|
} else if (caType === CaType.ACME || caType === CaType.AZURE_AD_CS) {
|
||||||
|
// External CA renewal - mark for async processing outside transaction
|
||||||
|
return {
|
||||||
|
isExternalCA: true,
|
||||||
|
ca,
|
||||||
|
profile,
|
||||||
|
originalCert,
|
||||||
|
originalSignatureAlgorithm,
|
||||||
|
originalKeyAlgorithm,
|
||||||
|
ttl
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `CA type ${String(caType)} does not support certificate renewal`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
newCert = foundCert;
|
|
||||||
} else {
|
} else {
|
||||||
// Self-signed certificate renewal
|
// Self-signed certificate renewal
|
||||||
const effectiveAlgorithms = getEffectiveAlgorithms(
|
const effectiveAlgorithms = getEffectiveAlgorithms(
|
||||||
@@ -1511,6 +1723,28 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
|
await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
|
||||||
|
|
||||||
|
const certRequestResult = await certificateRequestService.createCertificateRequest({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId: originalCert.projectId,
|
||||||
|
tx,
|
||||||
|
caId: ca?.id || originalCert.caId || undefined,
|
||||||
|
profileId: originalCert.profileId || undefined,
|
||||||
|
commonName: originalCert.commonName || undefined,
|
||||||
|
altNames: originalCert.altNames || undefined,
|
||||||
|
keyUsages: parseKeyUsages(originalCert.keyUsages),
|
||||||
|
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages),
|
||||||
|
notBefore: new Date(newCert.notBefore),
|
||||||
|
notAfter: new Date(newCert.notAfter),
|
||||||
|
keyAlgorithm: originalKeyAlgorithm,
|
||||||
|
signatureAlgorithm: originalSignatureAlgorithm,
|
||||||
|
metadata: `Renewed from certificate ID: ${originalCert.id}`,
|
||||||
|
status: CertificateRequestStatus.ISSUED,
|
||||||
|
certificateId: newCert.id
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
@@ -1518,10 +1752,76 @@ export const certificateV3ServiceFactory = ({
|
|||||||
serialNumber,
|
serialNumber,
|
||||||
newCert,
|
newCert,
|
||||||
originalCert,
|
originalCert,
|
||||||
profile
|
profile,
|
||||||
|
certRequestResult
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let certificateRequestId: string = renewalResult.certRequestResult?.id || "";
|
||||||
|
|
||||||
|
// Handle external CA renewals separately
|
||||||
|
if ("isExternalCA" in renewalResult && renewalResult.isExternalCA) {
|
||||||
|
const { ca, profile, originalCert, originalSignatureAlgorithm, originalKeyAlgorithm, ttl } = renewalResult;
|
||||||
|
|
||||||
|
const renewalOrderId = randomUUID();
|
||||||
|
const altNamesArray = originalCert.altNames
|
||||||
|
? originalCert.altNames.split(",").map((san: string) => san.trim())
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const certificateRequest = await certificateRequestService.createCertificateRequest({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId: originalCert.projectId,
|
||||||
|
profileId: profile?.id,
|
||||||
|
caId: ca.id,
|
||||||
|
commonName: originalCert.commonName || undefined,
|
||||||
|
altNames: originalCert.altNames || undefined,
|
||||||
|
keyUsages: parseKeyUsages(originalCert.keyUsages),
|
||||||
|
extendedKeyUsages: parseExtendedKeyUsages(originalCert.extendedKeyUsages),
|
||||||
|
keyAlgorithm: originalKeyAlgorithm,
|
||||||
|
signatureAlgorithm: originalSignatureAlgorithm,
|
||||||
|
metadata: `Renewed from certificate ID: ${originalCert.id}`,
|
||||||
|
status: CertificateRequestStatus.PENDING
|
||||||
|
});
|
||||||
|
|
||||||
|
certificateRequestId = certificateRequest.id;
|
||||||
|
|
||||||
|
await certificateIssuanceQueue.queueCertificateIssuance({
|
||||||
|
certificateId: renewalOrderId,
|
||||||
|
profileId: profile?.id || "",
|
||||||
|
caId: ca.id,
|
||||||
|
commonName: originalCert.commonName || "",
|
||||||
|
altNames: altNamesArray,
|
||||||
|
ttl,
|
||||||
|
signatureAlgorithm: originalSignatureAlgorithm,
|
||||||
|
keyAlgorithm: originalKeyAlgorithm,
|
||||||
|
keyUsages: convertEnumsToStringArray(parseKeyUsages(originalCert.keyUsages)),
|
||||||
|
extendedKeyUsages: convertEnumsToStringArray(parseExtendedKeyUsages(originalCert.extendedKeyUsages)),
|
||||||
|
isRenewal: true,
|
||||||
|
originalCertificateId: certificateId,
|
||||||
|
certificateRequestId: certificateRequest.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: "", // External CA renewal is async
|
||||||
|
certificateChain: "",
|
||||||
|
issuingCaCertificate: "",
|
||||||
|
serialNumber: "",
|
||||||
|
certificateId: renewalOrderId,
|
||||||
|
certificateRequestId: certificateRequest.id,
|
||||||
|
projectId: originalCert.projectId,
|
||||||
|
profileName: profile?.slug || "External CA Profile",
|
||||||
|
commonName: originalCert.commonName || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type check to ensure we have internal CA renewal result
|
||||||
|
if ("isExternalCA" in renewalResult) {
|
||||||
|
throw new BadRequestError({ message: "External CA renewals should be handled asynchronously" });
|
||||||
|
}
|
||||||
|
|
||||||
await triggerAutoSyncForCertificate(renewalResult.newCert.id, {
|
await triggerAutoSyncForCertificate(renewalResult.newCert.id, {
|
||||||
certificateSyncDAL,
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
@@ -1538,6 +1838,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
certificateChain: finalCertificateChain,
|
certificateChain: finalCertificateChain,
|
||||||
serialNumber: renewalResult.serialNumber,
|
serialNumber: renewalResult.serialNumber,
|
||||||
certificateId: renewalResult.newCert.id,
|
certificateId: renewalResult.newCert.id,
|
||||||
|
certificateRequestId,
|
||||||
projectId: renewalResult.originalCert.projectId,
|
projectId: renewalResult.originalCert.projectId,
|
||||||
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
|
profileName: renewalResult.profile?.slug || "Self-signed Certificate",
|
||||||
commonName: renewalResult.originalCert.commonName || ""
|
commonName: renewalResult.originalCert.commonName || ""
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
|
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsageType,
|
CertExtendedKeyUsageType,
|
||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
@@ -45,7 +44,7 @@ export type TOrderCertificateFromProfileDTO = {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
certificateOrder: {
|
certificateOrder: {
|
||||||
altNames: Array<{
|
altNames: Array<{
|
||||||
type: ACMESANType;
|
type: CertSubjectAlternativeNameType;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
validity: {
|
validity: {
|
||||||
@@ -58,6 +57,8 @@ export type TOrderCertificateFromProfileDTO = {
|
|||||||
notAfter?: Date;
|
notAfter?: Date;
|
||||||
signatureAlgorithm?: string;
|
signatureAlgorithm?: string;
|
||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
|
template?: string;
|
||||||
|
csr?: string;
|
||||||
};
|
};
|
||||||
removeRootsFromChain?: boolean;
|
removeRootsFromChain?: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -69,34 +70,14 @@ export type TCertificateFromProfileResponse = {
|
|||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
|
certificateRequestId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
profileName: string;
|
profileName: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateOrderResponse = {
|
export type TCertificateOrderResponse = {
|
||||||
orderId: string;
|
certificateRequestId: string;
|
||||||
status: CertificateOrderStatus;
|
|
||||||
subjectAlternativeNames: Array<{
|
|
||||||
type: ACMESANType;
|
|
||||||
value: string;
|
|
||||||
status: CertificateOrderStatus;
|
|
||||||
}>;
|
|
||||||
authorizations: Array<{
|
|
||||||
identifier: {
|
|
||||||
type: ACMESANType;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
status: CertificateOrderStatus;
|
|
||||||
expires?: string;
|
|
||||||
challenges: Array<{
|
|
||||||
type: string;
|
|
||||||
status: CertificateOrderStatus;
|
|
||||||
url: string;
|
|
||||||
token: string;
|
|
||||||
}>;
|
|
||||||
}>;
|
|
||||||
finalize: string;
|
|
||||||
certificate?: string;
|
certificate?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
profileName: string;
|
profileName: string;
|
||||||
@@ -105,6 +86,7 @@ export type TCertificateOrderResponse = {
|
|||||||
export type TRenewCertificateDTO = {
|
export type TRenewCertificateDTO = {
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
removeRootsFromChain?: boolean;
|
removeRootsFromChain?: boolean;
|
||||||
|
certificateRequestId?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateRenewalConfigDTO = {
|
export type TUpdateRenewalConfigDTO = {
|
||||||
|
|||||||
@@ -189,10 +189,12 @@ export const useGetCaCertTemplates = (caId: string) => {
|
|||||||
|
|
||||||
export const useGetAzureAdcsTemplates = ({
|
export const useGetAzureAdcsTemplates = ({
|
||||||
caId,
|
caId,
|
||||||
projectId
|
projectId,
|
||||||
|
isAzureAdcsCa
|
||||||
}: {
|
}: {
|
||||||
caId: string;
|
caId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
isAzureAdcsCa: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: caKeys.getAzureAdcsTemplates(caId, projectId),
|
queryKey: caKeys.getAzureAdcsTemplates(caId, projectId),
|
||||||
@@ -202,6 +204,6 @@ export const useGetAzureAdcsTemplates = ({
|
|||||||
}>(`/api/v1/cert-manager/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`);
|
}>(`/api/v1/cert-manager/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: Boolean(caId && projectId)
|
enabled: Boolean(caId && projectId && isAzureAdcsCa)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,14 +22,25 @@ export type TCertificateProfile = {
|
|||||||
apiConfigId?: string;
|
apiConfigId?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
externalConfigs?: Record<string, unknown> | null;
|
||||||
|
certificateAuthority?: {
|
||||||
|
id: string;
|
||||||
|
projectId?: string;
|
||||||
|
status: string;
|
||||||
|
name: string;
|
||||||
|
isExternal?: boolean;
|
||||||
|
externalType?: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||||
certificateAuthority?: {
|
certificateAuthority?: {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId?: string;
|
||||||
status: string;
|
status: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
isExternal?: boolean;
|
||||||
|
externalType?: string | null;
|
||||||
};
|
};
|
||||||
certificateTemplate?: {
|
certificateTemplate?: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -72,6 +83,7 @@ export type TCreateCertificateProfileDTO = {
|
|||||||
renewBeforeDays?: number;
|
renewBeforeDays?: number;
|
||||||
};
|
};
|
||||||
acmeConfig?: unknown;
|
acmeConfig?: unknown;
|
||||||
|
externalConfigs?: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateCertificateProfileDTO = {
|
export type TUpdateCertificateProfileDTO = {
|
||||||
@@ -90,6 +102,7 @@ export type TUpdateCertificateProfileDTO = {
|
|||||||
renewBeforeDays?: number;
|
renewBeforeDays?: number;
|
||||||
};
|
};
|
||||||
acmeConfig?: unknown;
|
acmeConfig?: unknown;
|
||||||
|
externalConfigs?: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteCertificateProfileDTO = {
|
export type TDeleteCertificateProfileDTO = {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
TRenewCertificateDTO,
|
TRenewCertificateDTO,
|
||||||
TRenewCertificateResponse,
|
TRenewCertificateResponse,
|
||||||
TRevokeCertDTO,
|
TRevokeCertDTO,
|
||||||
|
TUnifiedCertificateIssuanceDTO,
|
||||||
|
TUnifiedCertificateIssuanceResponse,
|
||||||
TUpdateRenewalConfigDTO
|
TUpdateRenewalConfigDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -185,3 +187,31 @@ export const useDownloadCertPkcs12 = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUnifiedCertificateIssuance = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TUnifiedCertificateIssuanceResponse, object, TUnifiedCertificateIssuanceDTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const { projectSlug, ...requestData } = body;
|
||||||
|
const { data } = await apiRequest.post<TUnifiedCertificateIssuanceResponse>(
|
||||||
|
"/api/v1/cert-manager/certificates",
|
||||||
|
requestData
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["certificate-profiles", "list"]
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectKeys.allProjectCertificates()
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { TCertificate } from "./types";
|
|||||||
export const certKeys = {
|
export const certKeys = {
|
||||||
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
|
getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
|
||||||
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
|
getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"],
|
||||||
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"]
|
getCertBundle: (serialNumber: string) => [{ serialNumber }, "certBundle"],
|
||||||
|
getCertificateRequest: (requestId: string, projectSlug: string) => [
|
||||||
|
{ requestId, projectSlug },
|
||||||
|
"certificateRequest"
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetCert = (serialNumber: string) => {
|
export const useGetCert = (serialNumber: string) => {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export type TRenewCertificateResponse = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
certificateRequestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateRenewalConfigDTO = {
|
export type TUpdateRenewalConfigDTO = {
|
||||||
@@ -79,3 +80,59 @@ export type TDownloadPkcs12DTO = {
|
|||||||
password: string;
|
password: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TUnifiedCertificateIssuanceDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
profileId: string;
|
||||||
|
projectId: string;
|
||||||
|
csr?: string;
|
||||||
|
attributes?: {
|
||||||
|
commonName?: string;
|
||||||
|
keyUsages?: string[];
|
||||||
|
extendedKeyUsages?: string[];
|
||||||
|
altNames?: Array<{
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
signatureAlgorithm: string;
|
||||||
|
keyAlgorithm: string;
|
||||||
|
subjectAlternativeNames?: Array<{
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
ttl: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
};
|
||||||
|
removeRootsFromChain?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUnifiedCertificateResponse = {
|
||||||
|
certificate: {
|
||||||
|
certificate: string;
|
||||||
|
issuingCaCertificate: string;
|
||||||
|
certificateChain: string;
|
||||||
|
privateKey?: string;
|
||||||
|
serialNumber: string;
|
||||||
|
certificateId: string;
|
||||||
|
};
|
||||||
|
certificateRequestId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateRequestResponse = {
|
||||||
|
certificateRequestId: string;
|
||||||
|
status: "pending" | "issued" | "failed";
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUnifiedCertificateIssuanceResponse =
|
||||||
|
| TUnifiedCertificateResponse
|
||||||
|
| TCertificateRequestResponse;
|
||||||
|
|
||||||
|
export type TCertificateRequestDetails = {
|
||||||
|
status: "pending" | "issued" | "failed";
|
||||||
|
certificate: TCertificate | null;
|
||||||
|
errorMessage: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|||||||
+75
-49
@@ -20,9 +20,9 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { useGetCert } from "@app/hooks/api";
|
import { useGetCert } from "@app/hooks/api";
|
||||||
import { useCreateCertificateV3 } from "@app/hooks/api/ca";
|
|
||||||
import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
import { EnrollmentType, useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
|
import { useUnifiedCertificateIssuance } from "@app/hooks/api/certificates/mutations";
|
||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
import { CertSubjectAlternativeNameType } from "@app/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants";
|
||||||
@@ -103,10 +103,11 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type TCertificateDetails = {
|
type TCertificateDetails = {
|
||||||
serialNumber: string;
|
serialNumber?: string;
|
||||||
certificate: string;
|
certificate?: string;
|
||||||
certificateChain: string;
|
certificateChain?: string;
|
||||||
privateKey: string;
|
privateKey?: string;
|
||||||
|
issuingCaCertificate?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => {
|
||||||
@@ -122,12 +123,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
|
|
||||||
const { data: profilesData } = useListCertificateProfiles({
|
const { data: profilesData } = useListCertificateProfiles({
|
||||||
projectId: currentProject?.id || "",
|
projectId: currentProject?.id || "",
|
||||||
enrollmentType: EnrollmentType.API
|
enrollmentType: EnrollmentType.API,
|
||||||
|
includeConfigs: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
const { mutateAsync: issueCertificate } = useUnifiedCertificateIssuance();
|
||||||
projectId: currentProject?.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const formResolver = useMemo(() => {
|
const formResolver = useMemo(() => {
|
||||||
return zodResolver(createSchema(shouldShowSubjectSection));
|
return zodResolver(createSchema(shouldShowSubjectSection));
|
||||||
@@ -243,7 +243,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
keyUsages,
|
keyUsages,
|
||||||
extendedKeyUsages
|
extendedKeyUsages
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
if (!currentProject?.slug) {
|
if (!currentProject?.slug || !currentProject?.id) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Project not found. Please refresh and try again.",
|
text: "Project not found. Please refresh and try again.",
|
||||||
type: "error"
|
type: "error"
|
||||||
@@ -275,44 +275,70 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateRequest: any = {
|
try {
|
||||||
profileId: formProfileId,
|
// Prepare unified request
|
||||||
projectSlug: currentProject.slug,
|
const request: any = {
|
||||||
ttl,
|
profileId: formProfileId,
|
||||||
signatureAlgorithm,
|
projectSlug: currentProject.slug,
|
||||||
keyAlgorithm,
|
projectId: currentProject.id,
|
||||||
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
attributes: {
|
||||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
ttl,
|
||||||
};
|
signatureAlgorithm: signatureAlgorithm || "",
|
||||||
|
keyAlgorithm: keyAlgorithm || "",
|
||||||
|
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
||||||
|
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (constraints.shouldShowSubjectSection && commonName) {
|
if (constraints.shouldShowSubjectSection && commonName) {
|
||||||
certificateRequest.commonName = commonName;
|
request.attributes.commonName = commonName;
|
||||||
}
|
|
||||||
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
|
||||||
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
|
||||||
if (formattedSans && formattedSans.length > 0) {
|
|
||||||
certificateRequest.altNames = formattedSans;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (constraints.shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||||
|
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
||||||
|
if (formattedSans && formattedSans.length > 0) {
|
||||||
|
request.attributes.altNames = formattedSans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await issueCertificate(request);
|
||||||
|
|
||||||
|
// Handle certificate issuance response
|
||||||
|
|
||||||
|
if ("certificate" in response && response.certificate) {
|
||||||
|
const certData = response.certificate;
|
||||||
|
const certificateDetailsToSet = {
|
||||||
|
serialNumber: certData.serialNumber || "",
|
||||||
|
certificate: certData.certificate || "",
|
||||||
|
certificateChain: certData.certificateChain || "",
|
||||||
|
privateKey: certData.privateKey || "",
|
||||||
|
issuingCaCertificate: certData.issuingCaCertificate || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
setCertificateDetails(certificateDetailsToSet);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created certificate",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Certificate request - async processing
|
||||||
|
createNotification({
|
||||||
|
text: `Certificate request submitted successfully. This may take a few minutes to process. Certificate Request ID: ${response.certificateRequestId}`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
handlePopUpToggle("issueCertificate", false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to request certificate: ${(error as Error)?.message || "Unknown error"}`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { serialNumber, certificate, certificateChain, privateKey } =
|
|
||||||
await createCertificate(certificateRequest);
|
|
||||||
|
|
||||||
setCertificateDetails({
|
|
||||||
serialNumber,
|
|
||||||
certificate,
|
|
||||||
certificateChain,
|
|
||||||
privateKey
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully created certificate",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
currentProject?.slug,
|
currentProject?.slug,
|
||||||
createCertificate,
|
issueCertificate,
|
||||||
constraints.shouldShowSubjectSection,
|
constraints.shouldShowSubjectSection,
|
||||||
constraints.shouldShowSanSection
|
constraints.shouldShowSanSection
|
||||||
]
|
]
|
||||||
@@ -321,13 +347,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
const getModalTitle = () => {
|
const getModalTitle = () => {
|
||||||
if (certificateDetails) return "Certificate Created Successfully";
|
if (certificateDetails) return "Certificate Created Successfully";
|
||||||
if (cert) return "Certificate Details";
|
if (cert) return "Certificate Details";
|
||||||
return "Issue New Certificate";
|
return "Request New Certificate";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getModalSubTitle = () => {
|
const getModalSubTitle = () => {
|
||||||
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
|
if (certificateDetails) return "Certificate has been successfully created and is ready for use";
|
||||||
if (cert) return "View certificate information";
|
if (cert) return "View certificate information";
|
||||||
return "Issue a new certificate using a certificate profile";
|
return "Request a new certificate using a certificate profile";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -343,10 +369,10 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
|
<ModalContent title={getModalTitle()} subTitle={getModalSubTitle()}>
|
||||||
{certificateDetails && (
|
{certificateDetails && (
|
||||||
<CertificateContent
|
<CertificateContent
|
||||||
serialNumber={certificateDetails.serialNumber}
|
serialNumber={certificateDetails.serialNumber!}
|
||||||
certificate={certificateDetails.certificate}
|
certificate={certificateDetails.certificate!}
|
||||||
certificateChain={certificateDetails.certificateChain}
|
certificateChain={certificateDetails.certificateChain!}
|
||||||
privateKey={certificateDetails.privateKey}
|
privateKey={certificateDetails.privateKey!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cert && (
|
{cert && (
|
||||||
@@ -498,7 +524,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
|
isDisabled={isSubmitting || (!actualSelectedProfile && !profileId)}
|
||||||
>
|
>
|
||||||
{cert ? "Update" : "Issue Certificate"}
|
{cert ? "Update" : "Request Certificate"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
|
|||||||
+6
-2
@@ -20,12 +20,16 @@ export const CertificateRenewalModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
const onRenewConfirm = async () => {
|
const onRenewConfirm = async () => {
|
||||||
const { certificateId } = popUp.renewCertificate.data as { certificateId: string };
|
const { certificateId } = popUp.renewCertificate.data as { certificateId: string };
|
||||||
|
|
||||||
await renewCertificate({
|
const result = await renewCertificate({
|
||||||
certificateId
|
certificateId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const notificationText = result.certificateRequestId
|
||||||
|
? `Certificate renewal initiated successfully. Certificate Request ID: ${result.certificateRequestId}`
|
||||||
|
: "Certificate renewed successfully";
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Certificate renewed successfully",
|
text: notificationText,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const CertificatesSection = () => {
|
|||||||
onClick={() => handlePopUpOpen("issueCertificate")}
|
onClick={() => handlePopUpOpen("issueCertificate")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Issue
|
Request
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+2
-1
@@ -213,7 +213,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
// Fetch Azure ADCS templates when Azure CA is selected
|
// Fetch Azure ADCS templates when Azure CA is selected
|
||||||
const { data: azureTemplates } = useGetAzureAdcsTemplates({
|
const { data: azureTemplates } = useGetAzureAdcsTemplates({
|
||||||
caId: selectedCa?.type === CaType.AZURE_AD_CS ? selectedCaId : "",
|
caId: selectedCa?.type === CaType.AZURE_AD_CS ? selectedCaId : "",
|
||||||
projectId
|
projectId,
|
||||||
|
isAzureAdcsCa: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize form with ALL subscriber data including template
|
// Initialize form with ALL subscriber data including template
|
||||||
|
|||||||
+176
-25
@@ -9,6 +9,7 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -19,7 +20,8 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject, useSubscription } from "@app/context";
|
import { useProject, useSubscription } from "@app/context";
|
||||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
import { CaType } from "@app/hooks/api/ca/enums";
|
||||||
|
import { useGetAzureAdcsTemplates, useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||||
import {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
IssuerType,
|
IssuerType,
|
||||||
@@ -77,7 +79,12 @@ const createSchema = z
|
|||||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
acmeConfig: z.object({}).optional()
|
acmeConfig: z.object({}).optional(),
|
||||||
|
externalConfigs: z
|
||||||
|
.object({
|
||||||
|
template: z.string().min(1, "Azure ADCS template is required")
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -212,7 +219,12 @@ const editSchema = z
|
|||||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
acmeConfig: z.object({}).optional()
|
acmeConfig: z.object({}).optional(),
|
||||||
|
externalConfigs: z
|
||||||
|
.object({
|
||||||
|
template: z.string().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -339,7 +351,7 @@ export const CreateProfileModal = ({
|
|||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
|
const { data: allCaData } = useListCasByProjectId(currentProject?.id || "");
|
||||||
const { data: templateData } = useListCertificateTemplatesV2({
|
const { data: templateData } = useListCertificateTemplatesV2({
|
||||||
projectId: currentProject?.id || "",
|
projectId: currentProject?.id || "",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
@@ -351,9 +363,23 @@ export const CreateProfileModal = ({
|
|||||||
|
|
||||||
const isEdit = mode === "edit" && profile;
|
const isEdit = mode === "edit" && profile;
|
||||||
|
|
||||||
const certificateAuthorities = caData || [];
|
const certificateAuthorities = (allCaData || []).map((ca) => ({
|
||||||
|
...ca,
|
||||||
|
groupType: ca.type === "internal" ? "internal" : "external"
|
||||||
|
}));
|
||||||
const certificateTemplates = templateData?.certificateTemplates || [];
|
const certificateTemplates = templateData?.certificateTemplates || [];
|
||||||
|
|
||||||
|
const getGroupHeaderLabel = (groupType: "internal" | "external") => {
|
||||||
|
switch (groupType) {
|
||||||
|
case "internal":
|
||||||
|
return "Internal CAs";
|
||||||
|
case "external":
|
||||||
|
return "External CAs";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||||
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
||||||
defaultValues: isEdit
|
defaultValues: isEdit
|
||||||
@@ -380,7 +406,17 @@ export const CreateProfileModal = ({
|
|||||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
|
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
|
||||||
|
externalConfigs: profile.externalConfigs
|
||||||
|
? {
|
||||||
|
template:
|
||||||
|
typeof profile.externalConfigs === "object" &&
|
||||||
|
profile.externalConfigs !== null &&
|
||||||
|
typeof profile.externalConfigs.template === "string"
|
||||||
|
? profile.externalConfigs.template
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
slug: "",
|
slug: "",
|
||||||
@@ -393,15 +429,28 @@ export const CreateProfileModal = ({
|
|||||||
autoRenew: false,
|
autoRenew: false,
|
||||||
renewBeforeDays: 30
|
renewBeforeDays: 30
|
||||||
},
|
},
|
||||||
acmeConfig: {}
|
acmeConfig: {},
|
||||||
|
externalConfigs: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchedEnrollmentType = watch("enrollmentType");
|
const watchedEnrollmentType = watch("enrollmentType");
|
||||||
const watchedIssuerType = watch("issuerType");
|
const watchedIssuerType = watch("issuerType");
|
||||||
|
const watchedCertificateAuthorityId = watch("certificateAuthorityId");
|
||||||
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
||||||
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
||||||
|
|
||||||
|
// Get the selected CA to check if it's Azure ADCS
|
||||||
|
const selectedCa = certificateAuthorities.find((ca) => ca.id === watchedCertificateAuthorityId);
|
||||||
|
const isAzureAdcsCa = selectedCa?.type === CaType.AZURE_AD_CS;
|
||||||
|
|
||||||
|
// Fetch Azure ADCS templates if needed
|
||||||
|
const { data: azureAdcsTemplatesData } = useGetAzureAdcsTemplates({
|
||||||
|
caId: watchedCertificateAuthorityId || "",
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
isAzureAdcsCa
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit && profile) {
|
if (isEdit && profile) {
|
||||||
reset({
|
reset({
|
||||||
@@ -427,10 +476,38 @@ export const CreateProfileModal = ({
|
|||||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined
|
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
|
||||||
|
externalConfigs: profile.externalConfigs
|
||||||
|
? {
|
||||||
|
template:
|
||||||
|
typeof profile.externalConfigs === "object" &&
|
||||||
|
profile.externalConfigs !== null &&
|
||||||
|
typeof profile.externalConfigs.template === "string"
|
||||||
|
? profile.externalConfigs.template
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isEdit, profile, reset]);
|
}, [isEdit, profile, reset, allCaData]);
|
||||||
|
|
||||||
|
// Additional effect to reset external configs when Azure ADCS templates are loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isEdit &&
|
||||||
|
profile &&
|
||||||
|
isAzureAdcsCa &&
|
||||||
|
azureAdcsTemplatesData?.templates &&
|
||||||
|
profile.externalConfigs &&
|
||||||
|
typeof profile.externalConfigs === "object" &&
|
||||||
|
profile.externalConfigs !== null &&
|
||||||
|
typeof profile.externalConfigs.template === "string"
|
||||||
|
) {
|
||||||
|
// Re-set the external configs to ensure the template value is properly set
|
||||||
|
// after the Azure ADCS templates have been loaded
|
||||||
|
setValue("externalConfigs.template", profile.externalConfigs.template);
|
||||||
|
}
|
||||||
|
}, [isEdit, profile, isAzureAdcsCa, azureAdcsTemplatesData, setValue]);
|
||||||
|
|
||||||
const onFormSubmit = async (data: FormData) => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
|
if (!isEdit && !subscription?.pkiAcme && data.enrollmentType === EnrollmentType.ACME) {
|
||||||
@@ -444,6 +521,18 @@ export const CreateProfileModal = ({
|
|||||||
|
|
||||||
if (!currentProject?.id && !isEdit) return;
|
if (!currentProject?.id && !isEdit) return;
|
||||||
|
|
||||||
|
// Validate Azure ADCS template requirement
|
||||||
|
if (
|
||||||
|
isAzureAdcsCa &&
|
||||||
|
(!data.externalConfigs?.template || data.externalConfigs.template.trim() === "")
|
||||||
|
) {
|
||||||
|
createNotification({
|
||||||
|
text: "Azure ADCS Certificate Authority requires a template to be specified",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
const updateData: TUpdateCertificateProfileDTO = {
|
const updateData: TUpdateCertificateProfileDTO = {
|
||||||
profileId: profile.id,
|
profileId: profile.id,
|
||||||
@@ -460,6 +549,11 @@ export const CreateProfileModal = ({
|
|||||||
updateData.acmeConfig = data.acmeConfig;
|
updateData.acmeConfig = data.acmeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add external configs if present
|
||||||
|
if (data.externalConfigs) {
|
||||||
|
updateData.externalConfigs = data.externalConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
await updateProfile.mutateAsync(updateData);
|
await updateProfile.mutateAsync(updateData);
|
||||||
} else {
|
} else {
|
||||||
if (!currentProject?.id) {
|
if (!currentProject?.id) {
|
||||||
@@ -491,6 +585,11 @@ export const CreateProfileModal = ({
|
|||||||
createData.acmeConfig = data.acmeConfig;
|
createData.acmeConfig = data.acmeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add external configs if present
|
||||||
|
if (data.externalConfigs) {
|
||||||
|
createData.externalConfigs = data.externalConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
await createProfile.mutateAsync(createData);
|
await createProfile.mutateAsync(createData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,30 +686,82 @@ export const CreateProfileModal = ({
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="certificateAuthorityId"
|
name="certificateAuthorityId"
|
||||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Issuing CA"
|
label="Issuing CA"
|
||||||
isRequired
|
isRequired
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<FilterableSelect
|
||||||
{...field}
|
value={certificateAuthorities.find((ca) => ca.id === value) || null}
|
||||||
value={value || undefined}
|
onChange={(selectedCaValue) => {
|
||||||
onValueChange={onChange}
|
if (Array.isArray(selectedCaValue)) {
|
||||||
|
onChange(selectedCaValue[0]?.id || "");
|
||||||
|
} else if (
|
||||||
|
selectedCaValue &&
|
||||||
|
typeof selectedCaValue === "object" &&
|
||||||
|
"id" in selectedCaValue
|
||||||
|
) {
|
||||||
|
onChange(selectedCaValue.id || "");
|
||||||
|
} else {
|
||||||
|
onChange("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
getOptionLabel={(ca) =>
|
||||||
|
ca.type === "internal" && ca.configuration.friendlyName
|
||||||
|
? ca.configuration.friendlyName
|
||||||
|
: ca.name
|
||||||
|
}
|
||||||
|
getOptionValue={(ca) => ca.id}
|
||||||
|
options={certificateAuthorities}
|
||||||
|
groupBy="groupType"
|
||||||
|
getGroupHeaderLabel={getGroupHeaderLabel}
|
||||||
placeholder="Select a certificate authority"
|
placeholder="Select a certificate authority"
|
||||||
className="w-full"
|
|
||||||
position="popper"
|
|
||||||
isDisabled={Boolean(isEdit)}
|
isDisabled={Boolean(isEdit)}
|
||||||
>
|
className="w-full"
|
||||||
{certificateAuthorities.map((ca) => (
|
/>
|
||||||
<SelectItem key={ca.id} value={ca.id}>
|
</FormControl>
|
||||||
{ca.type === "internal" && ca.configuration.friendlyName
|
)}
|
||||||
? ca.configuration.friendlyName
|
/>
|
||||||
: ca.name}
|
)}
|
||||||
</SelectItem>
|
|
||||||
))}
|
{/* Azure ADCS Template Selection */}
|
||||||
</Select>
|
{isAzureAdcsCa && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="externalConfigs.template"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Windows ADCS Template"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={
|
||||||
|
azureAdcsTemplatesData?.templates.find((template) => template.id === value) ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onChange={(selectedTemplate) => {
|
||||||
|
if (Array.isArray(selectedTemplate)) {
|
||||||
|
onChange(selectedTemplate[0]?.id || "");
|
||||||
|
} else if (
|
||||||
|
selectedTemplate &&
|
||||||
|
typeof selectedTemplate === "object" &&
|
||||||
|
"id" in selectedTemplate
|
||||||
|
) {
|
||||||
|
onChange(selectedTemplate.id || "");
|
||||||
|
} else {
|
||||||
|
onChange("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
getOptionLabel={(template) => template.name}
|
||||||
|
getOptionValue={(template) => template.id}
|
||||||
|
options={azureAdcsTemplatesData?.templates || []}
|
||||||
|
placeholder="Select an Azure ADCS certificate template"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-4
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
@@ -123,9 +124,11 @@ export const ProfileRow = ({
|
|||||||
<span className="text-sm text-mineshaft-300">
|
<span className="text-sm text-mineshaft-300">
|
||||||
{profile.issuerType === IssuerType.SELF_SIGNED
|
{profile.issuerType === IssuerType.SELF_SIGNED
|
||||||
? "Self-signed"
|
? "Self-signed"
|
||||||
: caData?.configuration.friendlyName ||
|
: profile.certificateAuthority?.isExternal
|
||||||
caData?.configuration.commonName ||
|
? profile.certificateAuthority.name
|
||||||
profile.caId}
|
: caData?.configuration.friendlyName ||
|
||||||
|
caData?.configuration.commonName ||
|
||||||
|
profile.caId}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -179,7 +182,7 @@ export const ProfileRow = ({
|
|||||||
}}
|
}}
|
||||||
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
|
icon={<FontAwesomeIcon icon={faPlus} className="w-3" />}
|
||||||
>
|
>
|
||||||
Issue Certificate
|
Request Certificate
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{canDeleteProfile && (
|
{canDeleteProfile && (
|
||||||
|
|||||||
Reference in New Issue
Block a user