mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 17:28:26 +00:00
Merge pull request #4777 from Infisical/feat/pki-sync-v2
PKI Syncs: Certificate Syncs and AWS ARN option
This commit is contained in:
Vendored
+8
@@ -62,6 +62,9 @@ import {
|
|||||||
TCertificateSecretsUpdate,
|
TCertificateSecretsUpdate,
|
||||||
TCertificatesInsert,
|
TCertificatesInsert,
|
||||||
TCertificatesUpdate,
|
TCertificatesUpdate,
|
||||||
|
TCertificateSyncs,
|
||||||
|
TCertificateSyncsInsert,
|
||||||
|
TCertificateSyncsUpdate,
|
||||||
TCertificateTemplateEstConfigs,
|
TCertificateTemplateEstConfigs,
|
||||||
TCertificateTemplateEstConfigsInsert,
|
TCertificateTemplateEstConfigsInsert,
|
||||||
TCertificateTemplateEstConfigsUpdate,
|
TCertificateTemplateEstConfigsUpdate,
|
||||||
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
|
|||||||
TPkiSubscribersUpdate
|
TPkiSubscribersUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
||||||
|
[TableName.CertificateSync]: KnexOriginal.CompositeTableType<
|
||||||
|
TCertificateSyncs,
|
||||||
|
TCertificateSyncsInsert,
|
||||||
|
TCertificateSyncsUpdate
|
||||||
|
>;
|
||||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateSync))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateSync, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("pkiSyncId").notNullable();
|
||||||
|
t.foreign("pkiSyncId").references("id").inTable(TableName.PkiSync).onDelete("CASCADE");
|
||||||
|
t.uuid("certificateId").notNullable();
|
||||||
|
t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.string("syncStatus").defaultTo(CertificateSyncStatus.Pending);
|
||||||
|
t.text("lastSyncMessage");
|
||||||
|
t.datetime("lastSyncedAt");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
// Ensure unique combination of pki sync and certificate
|
||||||
|
t.unique(["pkiSyncId", "certificateId"]);
|
||||||
|
|
||||||
|
t.index("pkiSyncId");
|
||||||
|
t.index("certificateId");
|
||||||
|
t.index("syncStatus");
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateSync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateSync);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateSync);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier"))) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||||
|
t.text("externalIdentifier").nullable();
|
||||||
|
t.index("externalIdentifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier")) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||||
|
t.dropIndex("externalIdentifier");
|
||||||
|
t.dropColumn("externalIdentifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 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 CertificateSyncsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
pkiSyncId: z.string().uuid(),
|
||||||
|
certificateId: z.string().uuid(),
|
||||||
|
syncStatus: z.string().default("pending").nullable().optional(),
|
||||||
|
lastSyncMessage: z.string().nullable().optional(),
|
||||||
|
lastSyncedAt: z.date().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
externalIdentifier: z.string().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateSyncs = z.infer<typeof CertificateSyncsSchema>;
|
||||||
|
export type TCertificateSyncsInsert = Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateSyncsUpdate = Partial<Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -17,6 +17,7 @@ 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-secrets";
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificate-syncs";
|
||||||
export * from "./certificate-template-est-configs";
|
export * from "./certificate-template-est-configs";
|
||||||
export * from "./certificate-templates";
|
export * from "./certificate-templates";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export enum TableName {
|
|||||||
AppConnection = "app_connections",
|
AppConnection = "app_connections",
|
||||||
SecretSync = "secret_syncs",
|
SecretSync = "secret_syncs",
|
||||||
PkiSync = "pki_syncs",
|
PkiSync = "pki_syncs",
|
||||||
|
CertificateSync = "certificate_syncs",
|
||||||
KmipClient = "kmip_clients",
|
KmipClient = "kmip_clients",
|
||||||
KmipOrgConfig = "kmip_org_configs",
|
KmipOrgConfig = "kmip_org_configs",
|
||||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ export enum EventType {
|
|||||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||||
GET_PKI_SYNCS = "get-pki-syncs",
|
GET_PKI_SYNCS = "get-pki-syncs",
|
||||||
GET_PKI_SYNC = "get-pki-sync",
|
GET_PKI_SYNC = "get-pki-sync",
|
||||||
|
GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates",
|
||||||
CREATE_PKI_SYNC = "create-pki-sync",
|
CREATE_PKI_SYNC = "create-pki-sync",
|
||||||
UPDATE_PKI_SYNC = "update-pki-sync",
|
UPDATE_PKI_SYNC = "update-pki-sync",
|
||||||
DELETE_PKI_SYNC = "delete-pki-sync",
|
DELETE_PKI_SYNC = "delete-pki-sync",
|
||||||
@@ -3161,6 +3162,16 @@ interface GetPkiSyncEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetPkiSyncCertificatesEvent {
|
||||||
|
type: EventType.GET_PKI_SYNC_CERTIFICATES;
|
||||||
|
metadata: {
|
||||||
|
syncId: string;
|
||||||
|
count: number;
|
||||||
|
certificateIds: string[];
|
||||||
|
destination: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePkiSyncEvent {
|
interface CreatePkiSyncEvent {
|
||||||
type: EventType.CREATE_PKI_SYNC;
|
type: EventType.CREATE_PKI_SYNC;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -4329,6 +4340,7 @@ export type Event =
|
|||||||
| SecretSyncRemoveSecretsEvent
|
| SecretSyncRemoveSecretsEvent
|
||||||
| GetPkiSyncsEvent
|
| GetPkiSyncsEvent
|
||||||
| GetPkiSyncEvent
|
| GetPkiSyncEvent
|
||||||
|
| GetPkiSyncCertificatesEvent
|
||||||
| CreatePkiSyncEvent
|
| CreatePkiSyncEvent
|
||||||
| UpdatePkiSyncEvent
|
| UpdatePkiSyncEvent
|
||||||
| DeletePkiSyncEvent
|
| DeletePkiSyncEvent
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ 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 { 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";
|
||||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
@@ -1064,6 +1065,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 certificateSyncDAL = certificateSyncDALFactory(db);
|
||||||
|
|
||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||||
@@ -2027,7 +2029,8 @@ export const registerRoutes = async (
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL
|
certificateAuthorityCertDAL,
|
||||||
|
certificateSyncDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||||
@@ -2138,6 +2141,7 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
});
|
});
|
||||||
@@ -2149,7 +2153,10 @@ export const registerRoutes = async (
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
internalCaService: internalCertificateAuthorityService,
|
internalCaService: internalCertificateAuthorityService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
@@ -2191,6 +2198,8 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const pkiSyncService = pkiSyncServiceFactory({
|
const pkiSyncService = pkiSyncServiceFactory({
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
|||||||
@@ -121,9 +121,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
caId: z.string().uuid().optional(),
|
caId: z.string().uuid().optional()
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
@@ -232,16 +226,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
renewBeforeDays: z.number().optional()
|
renewBeforeDays: z.number().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
|
||||||
metrics: z
|
|
||||||
.object({
|
|
||||||
profileId: z.string(),
|
|
||||||
totalCertificates: z.number(),
|
|
||||||
activeCertificates: z.number(),
|
|
||||||
expiredCertificates: z.number(),
|
|
||||||
expiringCertificates: z.number(),
|
|
||||||
revokedCertificates: z.number()
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
profileId: req.params.id
|
profileId: req.params.id
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = certificateProfile;
|
|
||||||
|
|
||||||
if (req.query.includeMetrics) {
|
|
||||||
const metrics = await server.services.certificateProfile.getProfileMetrics({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
profileId: req.params.id,
|
|
||||||
expiringDays: req.query.expiringDays
|
|
||||||
});
|
|
||||||
result = { ...certificateProfile, metrics };
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateProfile.projectId,
|
projectId: certificateProfile.projectId,
|
||||||
@@ -283,7 +253,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { certificateProfile: result };
|
return { certificateProfile };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
|
|||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
updateSchema: z.ZodType<{
|
updateSchema: z.ZodType<{
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
@@ -35,7 +35,7 @@ export const registerSyncPkiEndpoints = ({
|
|||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
responseSchema: z.ZodTypeAny;
|
responseSchema: z.ZodTypeAny;
|
||||||
syncOptions: {
|
syncOptions: {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ 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 { readLimit } from "@app/server/config/rateLimiter";
|
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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||||
|
|
||||||
const PkiSyncSchema = z.object({
|
const PkiSyncSchema = z.object({
|
||||||
@@ -60,7 +61,8 @@ const PkiSyncSchema = z.object({
|
|||||||
name: z.string()
|
name: z.string()
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional()
|
.optional(),
|
||||||
|
hasCertificate: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const PkiSyncOptionsSchema = z.object({
|
const PkiSyncOptionsSchema = z.object({
|
||||||
@@ -76,6 +78,27 @@ const PkiSyncOptionsSchema = z.object({
|
|||||||
minCertificateNameLength: z.number().optional()
|
minCertificateNameLength: z.number().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PkiSyncCertificateSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
pkiSyncId: z.string().uuid(),
|
||||||
|
certificateId: z.string().uuid(),
|
||||||
|
syncStatus: z.nativeEnum(CertificateSyncStatus),
|
||||||
|
lastSyncMessage: z.string().nullable().optional(),
|
||||||
|
lastSyncedAt: z.date().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
certificateSerialNumber: z.string().optional(),
|
||||||
|
certificateCommonName: z.string().optional(),
|
||||||
|
certificateAltNames: z.string().optional(),
|
||||||
|
certificateStatus: z.string().optional(),
|
||||||
|
certificateNotBefore: z.date().optional(),
|
||||||
|
certificateNotAfter: z.date().optional(),
|
||||||
|
certificateRenewBeforeDays: z.number().nullish(),
|
||||||
|
certificateRenewalError: z.string().nullish(),
|
||||||
|
pkiSyncName: z.string().optional(),
|
||||||
|
pkiSyncDestination: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -111,7 +134,8 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
tags: [ApiDocsTags.PkiSyncs],
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
description: "List all the PKI Syncs for the specified project.",
|
description: "List all the PKI Syncs for the specified project.",
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateId: z.string().uuid().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
||||||
@@ -120,11 +144,11 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
const {
|
||||||
query: { projectId },
|
query: { projectId, certificateId },
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
|
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId, certificateId }, permission);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -179,4 +203,163 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
return pkiSync;
|
return pkiSync;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:pkiSyncId/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
|
description: "List all certificates associated with a PKI Sync.",
|
||||||
|
params: z.object({
|
||||||
|
pkiSyncId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(20)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificates: PkiSyncCertificateSchema.array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { pkiSyncId } = req.params;
|
||||||
|
const { offset, limit } = req.query;
|
||||||
|
|
||||||
|
const { certificates, totalCount, pkiSyncInfo } = await server.services.pkiSync.listPkiSyncCertificates(
|
||||||
|
{ pkiSyncId, offset, limit },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: pkiSyncInfo.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PKI_SYNC_CERTIFICATES,
|
||||||
|
metadata: {
|
||||||
|
syncId: pkiSyncId,
|
||||||
|
destination: pkiSyncInfo.destination,
|
||||||
|
count: certificates.length,
|
||||||
|
certificateIds: certificates.map((c) => c.certificateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificates, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:pkiSyncId/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
|
description: "Add certificates to a PKI Sync.",
|
||||||
|
params: z.object({
|
||||||
|
pkiSyncId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
addedCertificates: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
pkiSyncId: z.string().uuid(),
|
||||||
|
certificateId: z.string().uuid(),
|
||||||
|
syncStatus: z.string().default("pending").optional().nullable(),
|
||||||
|
lastSyncMessage: z.string().optional().nullable(),
|
||||||
|
lastSyncedAt: z.date().optional().nullable(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { pkiSyncId } = req.params;
|
||||||
|
const { certificateIds } = req.body;
|
||||||
|
|
||||||
|
const { addedCertificates, pkiSyncInfo } = await server.services.pkiSync.addCertificatesToPkiSync(
|
||||||
|
{ pkiSyncId, certificateIds },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: pkiSyncInfo.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_PKI_SYNC,
|
||||||
|
metadata: {
|
||||||
|
pkiSyncId,
|
||||||
|
name: pkiSyncInfo.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { addedCertificates };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:pkiSyncId/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
|
description: "Remove certificates from a PKI Sync.",
|
||||||
|
params: z.object({
|
||||||
|
pkiSyncId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
removedCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { pkiSyncId } = req.params;
|
||||||
|
const { certificateIds } = req.body;
|
||||||
|
|
||||||
|
const { removedCount, pkiSyncInfo } = await server.services.pkiSync.removeCertificatesFromPkiSync(
|
||||||
|
{ pkiSyncId, certificateIds },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: pkiSyncInfo.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_PKI_SYNC,
|
||||||
|
metadata: {
|
||||||
|
pkiSyncId,
|
||||||
|
name: pkiSyncInfo.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { removedCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1195,8 +1195,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
offset: z.coerce.number().min(0).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
|
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit),
|
||||||
|
forPkiSync: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.optional()
|
||||||
|
.describe("Retrieve only certificates available for PKI sync")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
+2
-2
@@ -192,7 +192,7 @@ export const castDbEntryToAzureAdCsCertificateAuthority = (
|
|||||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||||
): TAzureAdCsCertificateAuthority & { credentials: unknown } => {
|
): TAzureAdCsCertificateAuthority & { credentials: unknown } => {
|
||||||
if (!ca.externalCa?.id) {
|
if (!ca.externalCa?.id) {
|
||||||
throw new BadRequestError({ message: "Malformed Azure AD Certificate Service certificate authority" });
|
throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ca.externalCa.dnsAppConnectionId) {
|
if (!ca.externalCa.dnsAppConnectionId) {
|
||||||
@@ -776,7 +776,7 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
|||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||||
if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) {
|
if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) {
|
||||||
throw new BadRequestError({ message: "CA is not an Azure AD Certificate Service CA" });
|
throw new BadRequestError({ message: "CA is not an Active Directory Certificate Service CA" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca);
|
const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { CaCapability, CaType } from "./certificate-authority-enums";
|
|||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
||||||
[CaType.INTERNAL]: "Internal",
|
[CaType.INTERNAL]: "Internal",
|
||||||
[CaType.ACME]: "ACME",
|
[CaType.ACME]: "ACME-compatible CA",
|
||||||
[CaType.AZURE_AD_CS]: "Azure AD Certificate Service"
|
[CaType.AZURE_AD_CS]: "Active Directory Certificate Service"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ import {
|
|||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
TCertificateProfileMetrics,
|
|
||||||
TCertificateProfileUpdate,
|
TCertificateProfileUpdate,
|
||||||
TCertificateProfileWithConfigs,
|
TCertificateProfileWithConfigs
|
||||||
TCertificateProfileWithRawMetrics
|
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
||||||
@@ -203,21 +201,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
includeMetrics?: boolean;
|
|
||||||
expiringDays?: number;
|
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
|
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
||||||
offset = 0,
|
|
||||||
limit = 20,
|
|
||||||
search,
|
|
||||||
enrollmentType,
|
|
||||||
caId,
|
|
||||||
includeMetrics = false,
|
|
||||||
expiringDays = 7
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||||
`${TableName.PkiCertificateProfile}.projectId`,
|
`${TableName.PkiCertificateProfile}.projectId`,
|
||||||
@@ -242,7 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = baseQuery
|
const query = baseQuery
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.PkiEstEnrollmentConfig,
|
TableName.PkiEstEnrollmentConfig,
|
||||||
`${TableName.PkiCertificateProfile}.estConfigId`,
|
`${TableName.PkiCertificateProfile}.estConfigId`,
|
||||||
@@ -267,52 +255,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
query = query.leftJoin(
|
|
||||||
TableName.Certificate,
|
|
||||||
`${TableName.PkiCertificateProfile}.id`,
|
|
||||||
`${TableName.Certificate}.profileId`
|
|
||||||
);
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const expiringDate = new Date();
|
|
||||||
expiringDate.setDate(now.getDate() + expiringDays);
|
|
||||||
|
|
||||||
query = query
|
|
||||||
.select(
|
|
||||||
selectAllTableCols(TableName.PkiCertificateProfile),
|
|
||||||
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
|
|
||||||
db
|
|
||||||
.ref("disableBootstrapCaValidation")
|
|
||||||
.withSchema(TableName.PkiEstEnrollmentConfig)
|
|
||||||
.as("estDisableBootstrapCaValidation"),
|
|
||||||
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
|
|
||||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
|
||||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
|
||||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
|
||||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
|
|
||||||
db.raw("COUNT(certificates.id) as total_certificates"),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
|
||||||
[expiringDate]
|
|
||||||
),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
|
|
||||||
[now]
|
|
||||||
),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
|
|
||||||
[now, expiringDate]
|
|
||||||
),
|
|
||||||
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
|
||||||
)
|
|
||||||
.groupBy(
|
|
||||||
`${TableName.PkiCertificateProfile}.id`,
|
|
||||||
`${TableName.PkiEstEnrollmentConfig}.id`,
|
|
||||||
`${TableName.PkiApiEnrollmentConfig}.id`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = (await query
|
const results = (await query
|
||||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
@@ -353,17 +295,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
apiConfig
|
apiConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
return {
|
|
||||||
...baseProfile,
|
|
||||||
total_certificates: result.total_certificates,
|
|
||||||
active_certificates: result.active_certificates,
|
|
||||||
expired_certificates: result.expired_certificates,
|
|
||||||
expiring_certificates: result.expiring_certificates,
|
|
||||||
revoked_certificates: result.revoked_certificates
|
|
||||||
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseProfile as TCertificateProfileWithConfigs;
|
return baseProfile as TCertificateProfileWithConfigs;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -485,45 +416,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfileMetrics = async (
|
|
||||||
profileId: string,
|
|
||||||
expiringDays: number = 7,
|
|
||||||
tx?: Knex
|
|
||||||
): Promise<TCertificateProfileMetrics> => {
|
|
||||||
try {
|
|
||||||
const now = new Date();
|
|
||||||
const expiringDate = new Date();
|
|
||||||
expiringDate.setDate(now.getDate() + expiringDays);
|
|
||||||
|
|
||||||
const metrics = await (tx || db)(TableName.Certificate)
|
|
||||||
.where("profileId", profileId)
|
|
||||||
.select(
|
|
||||||
db.raw("COUNT(*) as total_certificates"),
|
|
||||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
|
|
||||||
expiringDate
|
|
||||||
]),
|
|
||||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
|
|
||||||
[now, expiringDate]
|
|
||||||
),
|
|
||||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
|
||||||
)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return {
|
|
||||||
profileId,
|
|
||||||
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
|
|
||||||
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
|
|
||||||
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
|
|
||||||
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
|
|
||||||
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
||||||
@@ -546,7 +438,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
countByProjectId,
|
countByProjectId,
|
||||||
findByNameAndProjectId,
|
findByNameAndProjectId,
|
||||||
getCertificatesByProfile,
|
getCertificatesByProfile,
|
||||||
getProfileMetrics,
|
|
||||||
isProfileInUse
|
isProfileInUse
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -127,8 +127,3 @@ export const listCertificatesByProfileSchema = z.object({
|
|||||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
status: z.enum(["active", "expired", "revoked"]).optional(),
|
||||||
search: z.string().optional()
|
search: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCertificateProfileMetricsSchema = z.object({
|
|
||||||
profileId: z.string().uuid(),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).default(30)
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ describe("CertificateProfileService", () => {
|
|||||||
findByNameAndProjectId: vi.fn(),
|
findByNameAndProjectId: vi.fn(),
|
||||||
findByIdWithConfigs: vi.fn(),
|
findByIdWithConfigs: vi.fn(),
|
||||||
getCertificatesByProfile: vi.fn(),
|
getCertificatesByProfile: vi.fn(),
|
||||||
getProfileMetrics: vi.fn(),
|
|
||||||
isProfileInUse: vi.fn(),
|
isProfileInUse: vi.fn(),
|
||||||
transaction: vi.fn(),
|
transaction: vi.fn(),
|
||||||
find: vi.fn(),
|
find: vi.fn(),
|
||||||
@@ -493,9 +492,7 @@ describe("CertificateProfileService", () => {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
enrollmentType: undefined,
|
enrollmentType: undefined,
|
||||||
caId: undefined,
|
caId: undefined
|
||||||
includeMetrics: false,
|
|
||||||
expiringDays: 30
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -515,51 +512,7 @@ describe("CertificateProfileService", () => {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
search: "test",
|
search: "test",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123"
|
||||||
includeMetrics: false,
|
|
||||||
expiringDays: 30
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should list profiles with metrics when includeMetrics is true", async () => {
|
|
||||||
const mockProfilesWithMetrics = [
|
|
||||||
{
|
|
||||||
...sampleProfile,
|
|
||||||
total_certificates: 10,
|
|
||||||
active_certificates: 8,
|
|
||||||
expired_certificates: 1,
|
|
||||||
expiring_certificates: 1,
|
|
||||||
revoked_certificates: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
(mockCertificateProfileDAL.findByProjectId as any).mockResolvedValue(mockProfilesWithMetrics);
|
|
||||||
|
|
||||||
const result = await service.listProfiles({
|
|
||||||
...mockActor,
|
|
||||||
projectId: "project-123",
|
|
||||||
includeMetrics: true,
|
|
||||||
expiringDays: 15
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.profiles).toHaveLength(1);
|
|
||||||
expect(result.profiles[0]).toHaveProperty("metrics");
|
|
||||||
expect(result.profiles[0].metrics).toEqual({
|
|
||||||
profileId: sampleProfile.id,
|
|
||||||
totalCertificates: 10,
|
|
||||||
activeCertificates: 8,
|
|
||||||
expiredCertificates: 1,
|
|
||||||
expiringCertificates: 1,
|
|
||||||
revokedCertificates: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
|
||||||
offset: 0,
|
|
||||||
limit: 20,
|
|
||||||
search: undefined,
|
|
||||||
enrollmentType: undefined,
|
|
||||||
caId: undefined,
|
|
||||||
includeMetrics: true,
|
|
||||||
expiringDays: 15
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -659,54 +612,6 @@ describe("CertificateProfileService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getProfileMetrics", () => {
|
|
||||||
const mockMetrics = {
|
|
||||||
profileId: "profile-123",
|
|
||||||
totalCertificates: 10,
|
|
||||||
activeCertificates: 8,
|
|
||||||
expiredCertificates: 1,
|
|
||||||
expiringCertificates: 2,
|
|
||||||
revokedCertificates: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
|
||||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(mockMetrics);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get profile metrics successfully", async () => {
|
|
||||||
const result = await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual(mockMetrics);
|
|
||||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
|
||||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get profile metrics with custom expiring days", async () => {
|
|
||||||
await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123",
|
|
||||||
expiringDays: 60
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 60);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw NotFoundError when profile not found", async () => {
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123"
|
|
||||||
})
|
|
||||||
).rejects.toThrow(NotFoundError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("comprehensive certificate profile scenarios", () => {
|
describe("comprehensive certificate profile scenarios", () => {
|
||||||
describe("profile configuration validation", () => {
|
describe("profile configuration validation", () => {
|
||||||
it("should validate EST enrollment configuration", async () => {
|
it("should validate EST enrollment configuration", async () => {
|
||||||
@@ -929,53 +834,6 @@ describe("CertificateProfileService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("metrics and monitoring", () => {
|
|
||||||
it("should calculate profile metrics correctly", async () => {
|
|
||||||
const detailedMetrics = {
|
|
||||||
profileId: "profile-123",
|
|
||||||
totalCertificates: 50,
|
|
||||||
activeCertificates: 40,
|
|
||||||
expiredCertificates: 5,
|
|
||||||
expiringCertificates: 3,
|
|
||||||
revokedCertificates: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
|
||||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(detailedMetrics);
|
|
||||||
|
|
||||||
const result = await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123",
|
|
||||||
expiringDays: 14
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual(detailedMetrics);
|
|
||||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 14);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle zero certificate metrics", async () => {
|
|
||||||
const emptyMetrics = {
|
|
||||||
profileId: "profile-123",
|
|
||||||
totalCertificates: 0,
|
|
||||||
activeCertificates: 0,
|
|
||||||
expiredCertificates: 0,
|
|
||||||
expiringCertificates: 0,
|
|
||||||
revokedCertificates: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
|
||||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(emptyMetrics);
|
|
||||||
|
|
||||||
const result = await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.totalCertificates).toBe(0);
|
|
||||||
expect(result.activeCertificates).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("error scenarios", () => {
|
describe("error scenarios", () => {
|
||||||
it("should handle database connection errors gracefully", async () => {
|
it("should handle database connection errors gracefully", async () => {
|
||||||
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
|
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ import {
|
|||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
TCertificateProfileMetrics,
|
|
||||||
TCertificateProfileUpdate,
|
TCertificateProfileUpdate,
|
||||||
TCertificateProfileWithConfigs,
|
TCertificateProfileWithConfigs
|
||||||
TCertificateProfileWithRawMetrics
|
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
const validateAndEncryptPemCaChain = async (
|
const validateAndEncryptPemCaChain = async (
|
||||||
@@ -361,18 +359,14 @@ export const certificateProfileServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
profileId,
|
profileId
|
||||||
includeMetrics = false,
|
|
||||||
expiringDays = 30
|
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
profileId: string;
|
profileId: string;
|
||||||
includeMetrics?: boolean;
|
}): Promise<TCertificateProfile> => {
|
||||||
expiringDays?: number;
|
|
||||||
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
|
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
const profile = await certificateProfileDAL.findById(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
@@ -393,14 +387,6 @@ export const certificateProfileServiceFactory = ({
|
|||||||
|
|
||||||
const converted = convertDalToService(profile);
|
const converted = convertDalToService(profile);
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
|
||||||
return {
|
|
||||||
...converted,
|
|
||||||
metrics
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
return converted;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -506,9 +492,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit = 20,
|
limit = 20,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
caId,
|
caId
|
||||||
includeMetrics = false,
|
|
||||||
expiringDays = 30
|
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -520,10 +504,8 @@ export const certificateProfileServiceFactory = ({
|
|||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
includeMetrics?: boolean;
|
|
||||||
expiringDays?: number;
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
|
profiles: TCertificateProfileWithConfigs[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}> => {
|
}> => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
@@ -544,9 +526,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
caId,
|
caId
|
||||||
includeMetrics,
|
|
||||||
expiringDays
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||||
@@ -591,27 +571,12 @@ export const certificateProfileServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const converted = convertDalToService(profileWithConfigs);
|
const converted = convertDalToService(profileWithConfigs);
|
||||||
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
|
const result: TCertificateProfileWithConfigs = {
|
||||||
...converted,
|
...converted,
|
||||||
estConfig: decryptedEstConfig,
|
estConfig: decryptedEstConfig,
|
||||||
apiConfig: profileWithConfigs.apiConfig
|
apiConfig: profileWithConfigs.apiConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
|
|
||||||
result = {
|
|
||||||
...result,
|
|
||||||
metrics: {
|
|
||||||
profileId: converted.id,
|
|
||||||
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
|
|
||||||
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
|
|
||||||
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
|
|
||||||
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
|
|
||||||
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -709,43 +674,6 @@ export const certificateProfileServiceFactory = ({
|
|||||||
return certificates;
|
return certificates;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfileMetrics = async ({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
profileId,
|
|
||||||
expiringDays = 30
|
|
||||||
}: {
|
|
||||||
actor: ActorType;
|
|
||||||
actorId: string;
|
|
||||||
actorAuthMethod: ActorAuthMethod;
|
|
||||||
actorOrgId: string;
|
|
||||||
profileId: string;
|
|
||||||
expiringDays?: number;
|
|
||||||
}): Promise<TCertificateProfileMetrics> => {
|
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
|
||||||
if (!profile) {
|
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
projectId: profile.projectId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionCertificateProfileActions.Read,
|
|
||||||
ProjectPermissionSub.CertificateProfiles
|
|
||||||
);
|
|
||||||
|
|
||||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
|
||||||
return metrics;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEstConfigurationByProfile = async (
|
const getEstConfigurationByProfile = async (
|
||||||
params:
|
params:
|
||||||
| {
|
| {
|
||||||
@@ -818,7 +746,6 @@ export const certificateProfileServiceFactory = ({
|
|||||||
listProfiles,
|
listProfiles,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
getProfileCertificates,
|
getProfileCertificates,
|
||||||
getProfileMetrics,
|
|
||||||
getEstConfigurationByProfile
|
getEstConfigurationByProfile
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,18 +54,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
|||||||
autoRenew: boolean;
|
autoRenew: boolean;
|
||||||
renewBeforeDays?: number;
|
renewBeforeDays?: number;
|
||||||
};
|
};
|
||||||
metrics?: TCertificateProfileMetrics;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TCertificateProfileMetrics {
|
|
||||||
profileId: string;
|
|
||||||
totalCertificates: number;
|
|
||||||
activeCertificates: number;
|
|
||||||
expiredCertificates: number;
|
|
||||||
expiringCertificates: number;
|
|
||||||
revokedCertificates: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TCertificateProfileCertificate {
|
export interface TCertificateProfileCertificate {
|
||||||
id: string;
|
id: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
@@ -76,11 +66,3 @@ export interface TCertificateProfileCertificate {
|
|||||||
revokedAt: Date | null;
|
revokedAt: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
|
|
||||||
total_certificates?: string;
|
|
||||||
active_certificates?: string;
|
|
||||||
expired_certificates?: string;
|
|
||||||
expiring_certificates?: string;
|
|
||||||
revoked_certificates?: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TCertificateSyncs } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { CertificateSyncStatus } from "./certificate-sync-enums";
|
||||||
|
|
||||||
|
export type TCertificateSyncDALFactory = ReturnType<typeof certificateSyncDALFactory>;
|
||||||
|
|
||||||
|
type CertificateSyncFindFilter = Parameters<typeof buildFindFilter<TCertificateSyncs>>[0];
|
||||||
|
|
||||||
|
export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateSyncOrm = ormify(db, TableName.CertificateSync);
|
||||||
|
|
||||||
|
const findByPkiSyncId = async (pkiSyncId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId })
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync));
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByPkiSyncId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByCertificateId = async (certificateId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ certificateId })
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync));
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByCertificateId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByPkiSyncAndCertificate = async (pkiSyncId: string, certificateId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId, certificateId })
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync))
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByPkiSyncAndCertificate" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCertificateIdsByPkiSyncId = async (pkiSyncId: string, tx?: Knex): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId })
|
||||||
|
.select("certificateId")) as Array<{ certificateId: string }>;
|
||||||
|
return docs.map((doc) => doc.certificateId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindCertificateIdsByPkiSyncId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPkiSyncIdsByCertificateId = async (certificateId: string, tx?: Knex): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ certificateId })
|
||||||
|
.select("pkiSyncId")) as Array<{ pkiSyncId: string }>;
|
||||||
|
return docs.map((doc) => doc.pkiSyncId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindPkiSyncIdsByCertificateId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCertificates = async (
|
||||||
|
pkiSyncId: string,
|
||||||
|
certificateData: Array<{ certificateId: string; externalIdentifier?: string }>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TCertificateSyncs[]> => {
|
||||||
|
try {
|
||||||
|
const insertData = certificateData.map(({ certificateId, externalIdentifier }) => ({
|
||||||
|
pkiSyncId,
|
||||||
|
certificateId,
|
||||||
|
syncStatus: CertificateSyncStatus.Pending,
|
||||||
|
externalIdentifier
|
||||||
|
}));
|
||||||
|
|
||||||
|
const docs = await (tx || db)(TableName.CertificateSync).insert(insertData).returning("*");
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "AddCertificates" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCertificates = async (pkiSyncId: string, certificateIds: string[], tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const deletedCount = await (tx || db)(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId })
|
||||||
|
.whereIn("certificateId", certificateIds)
|
||||||
|
.del();
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "RemoveCertificates" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAllCertificatesFromSync = async (pkiSyncId: string, tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const deletedCount = await (tx || db)(TableName.CertificateSync).where({ pkiSyncId }).del();
|
||||||
|
return deletedCount;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "RemoveAllCertificatesFromSync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSyncStatus = async (
|
||||||
|
pkiSyncId: string,
|
||||||
|
certificateId: string,
|
||||||
|
status: string,
|
||||||
|
message?: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TCertificateSyncs | undefined> => {
|
||||||
|
try {
|
||||||
|
const updateData: Partial<TCertificateSyncs> = {
|
||||||
|
syncStatus: status,
|
||||||
|
lastSyncedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message !== undefined) {
|
||||||
|
updateData.lastSyncMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await (tx || db)(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId, certificateId })
|
||||||
|
.update(updateData)
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
return docs[0];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "UpdateSyncStatus" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkUpdateSyncStatus = async (
|
||||||
|
updates: Array<{
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (tx) {
|
||||||
|
for (const update of updates) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, tx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await certificateSyncOrm.transaction(async (trx) => {
|
||||||
|
for (const update of updates) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, trx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "BulkUpdateSyncStatus" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithDetails = async (
|
||||||
|
options: {
|
||||||
|
filter?: CertificateSyncFindFilter;
|
||||||
|
pkiSyncId?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<{
|
||||||
|
certificateDetails: (TCertificateSyncs & {
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number | null;
|
||||||
|
certificateRenewedByCertificateId?: string;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
|
})[];
|
||||||
|
totalCount: number;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const { filter, pkiSyncId, offset, limit } = options;
|
||||||
|
|
||||||
|
const baseQuery = (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.leftJoin(TableName.Certificate, `${TableName.CertificateSync}.certificateId`, `${TableName.Certificate}.id`)
|
||||||
|
.leftJoin(TableName.PkiSync, `${TableName.CertificateSync}.pkiSyncId`, `${TableName.PkiSync}.id`);
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
void baseQuery.where(buildFindFilter(filter));
|
||||||
|
}
|
||||||
|
if (pkiSyncId) {
|
||||||
|
void baseQuery.where(`${TableName.CertificateSync}.pkiSyncId`, pkiSyncId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countResult = await baseQuery.clone().count("* as count");
|
||||||
|
const totalCount = Number((countResult[0] as unknown as { count: string | number }).count);
|
||||||
|
|
||||||
|
const query = baseQuery
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync))
|
||||||
|
.select(
|
||||||
|
db.ref("serialNumber").withSchema(TableName.Certificate).as("certificateSerialNumber"),
|
||||||
|
db.ref("commonName").withSchema(TableName.Certificate).as("certificateCommonName"),
|
||||||
|
db.ref("altNames").withSchema(TableName.Certificate).as("certificateAltNames"),
|
||||||
|
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
|
||||||
|
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
|
||||||
|
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
|
||||||
|
db.ref("renewBeforeDays").withSchema(TableName.Certificate).as("certificateRenewBeforeDays"),
|
||||||
|
db.ref("renewedByCertificateId").withSchema(TableName.Certificate).as("certificateRenewedByCertificateId"),
|
||||||
|
db.ref("renewalError").withSchema(TableName.Certificate).as("certificateRenewalError"),
|
||||||
|
db.ref("name").withSchema(TableName.PkiSync).as("pkiSyncName"),
|
||||||
|
db.ref("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination")
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.CertificateSync}.createdAt`, "desc");
|
||||||
|
|
||||||
|
if (offset !== undefined) {
|
||||||
|
void query.offset(offset);
|
||||||
|
}
|
||||||
|
if (limit !== undefined) {
|
||||||
|
void query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateDetails = (await query) as (TCertificateSyncs & {
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number;
|
||||||
|
certificateRenewedByCertificateId?: string;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
|
})[];
|
||||||
|
|
||||||
|
return { certificateDetails, totalCount };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindWithDetails" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...certificateSyncOrm,
|
||||||
|
findByPkiSyncId,
|
||||||
|
findByCertificateId,
|
||||||
|
findByPkiSyncAndCertificate,
|
||||||
|
findCertificateIdsByPkiSyncId,
|
||||||
|
findPkiSyncIdsByCertificateId,
|
||||||
|
addCertificates,
|
||||||
|
removeCertificates,
|
||||||
|
removeAllCertificatesFromSync,
|
||||||
|
updateSyncStatus,
|
||||||
|
bulkUpdateSyncStatus,
|
||||||
|
findWithDetails
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum CertificateSyncStatus {
|
||||||
|
Pending = "pending",
|
||||||
|
Syncing = "syncing",
|
||||||
|
Succeeded = "succeeded",
|
||||||
|
Failed = "failed",
|
||||||
|
Running = "running"
|
||||||
|
}
|
||||||
@@ -133,7 +133,18 @@ describe("CertificateV3Service", () => {
|
|||||||
certificateProfileDAL: mockCertificateProfileDAL,
|
certificateProfileDAL: mockCertificateProfileDAL,
|
||||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||||
internalCaService: mockInternalCaService,
|
internalCaService: mockInternalCaService,
|
||||||
permissionService: mockPermissionService
|
permissionService: mockPermissionService,
|
||||||
|
certificateSyncDAL: {
|
||||||
|
findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]),
|
||||||
|
addCertificates: vi.fn().mockResolvedValue([]),
|
||||||
|
findByPkiSyncAndCertificate: vi.fn().mockResolvedValue(null)
|
||||||
|
},
|
||||||
|
pkiSyncDAL: {
|
||||||
|
find: vi.fn().mockResolvedValue([])
|
||||||
|
},
|
||||||
|
pkiSyncQueue: {
|
||||||
|
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ import {
|
|||||||
mapEnumsForValidation,
|
mapEnumsForValidation,
|
||||||
normalizeDateForApi
|
normalizeDateForApi
|
||||||
} from "../certificate-common/certificate-utils";
|
} from "../certificate-common/certificate-utils";
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||||
|
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||||
|
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
|
||||||
import {
|
import {
|
||||||
TCertificateFromProfileResponse,
|
TCertificateFromProfileResponse,
|
||||||
TCertificateOrderResponse,
|
TCertificateOrderResponse,
|
||||||
@@ -72,6 +76,12 @@ type TCertificateV3ServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
|
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||||
|
>;
|
||||||
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||||
@@ -328,7 +338,10 @@ export const certificateV3ServiceFactory = ({
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
internalCaService,
|
internalCaService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
}: TCertificateV3ServiceFactoryDep) => {
|
}: TCertificateV3ServiceFactoryDep) => {
|
||||||
const issueCertificateFromProfile = async ({
|
const issueCertificateFromProfile = async ({
|
||||||
profileId,
|
profileId,
|
||||||
@@ -872,6 +885,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
@@ -883,6 +898,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await triggerAutoSyncForCertificate(renewalResult.newCert.id, {
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: renewalResult.certificate,
|
certificate: renewalResult.certificate,
|
||||||
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TCertificates } from "@app/db/schemas";
|
import { TableName, TCertificates } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
@@ -60,11 +62,13 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
.where(`${TableName.Project}.id`, projectId);
|
.where(`${TableName.Project}.id`, projectId);
|
||||||
|
|
||||||
if (friendlyName) {
|
if (friendlyName) {
|
||||||
query = query.andWhere(`${TableName.Certificate}.friendlyName`, friendlyName);
|
const sanitizedValue = String(friendlyName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.friendlyName`, "like", `%${sanitizedValue}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commonName) {
|
if (commonName) {
|
||||||
query = query.andWhere(`${TableName.Certificate}.commonName`, commonName);
|
const sanitizedValue = String(commonName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.commonName`, "like", `%${sanitizedValue}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = await query.count("*").first();
|
const count = await query.count("*").first();
|
||||||
@@ -114,6 +118,109 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findActiveCertificatesByIds = async (certificateIds: string[]): Promise<TCertificates[]> => {
|
||||||
|
try {
|
||||||
|
if (certificateIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const certs = await db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.whereIn("id", certificateIds)
|
||||||
|
.where({ status: CertStatus.ACTIVE })
|
||||||
|
.where("notAfter", ">", new Date())
|
||||||
|
.orderBy("notBefore", "desc")
|
||||||
|
.select("*");
|
||||||
|
|
||||||
|
return certs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find active certificates by IDs" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findActiveCertificatesForSync = async (
|
||||||
|
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
|
||||||
|
options?: { limit?: number; offset?: number }
|
||||||
|
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||||
|
try {
|
||||||
|
let query = db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
||||||
|
.select(selectAllTableCols(TableName.Certificate))
|
||||||
|
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
|
||||||
|
.where({ status: CertStatus.ACTIVE })
|
||||||
|
.where("notAfter", ">", new Date())
|
||||||
|
.whereNull("renewedByCertificateId");
|
||||||
|
|
||||||
|
Object.entries(filter).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (key === "friendlyName" || key === "commonName") {
|
||||||
|
const sanitizedValue = String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, "like", `%${sanitizedValue}%`);
|
||||||
|
} else {
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options?.offset) {
|
||||||
|
query = query.offset(options.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.limit) {
|
||||||
|
query = query.limit(options.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.orderBy("createdAt", "desc");
|
||||||
|
|
||||||
|
const certs = await query;
|
||||||
|
return certs.map((cert) => ({ ...cert, hasPrivateKey: Boolean(cert.privateKeyRef) }));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find active certificates for sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const countActiveCertificatesForSync = async ({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
commonName
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName?: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
|
||||||
|
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId)
|
||||||
|
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
|
||||||
|
.where(`${TableName.Certificate}.notAfter`, ">", new Date())
|
||||||
|
.whereNull(`${TableName.Certificate}.renewedByCertificateId`);
|
||||||
|
|
||||||
|
if (friendlyName) {
|
||||||
|
const sanitizedValue = String(friendlyName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.friendlyName`, "like", `%${sanitizedValue}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commonName) {
|
||||||
|
const sanitizedValue = String(commonName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.commonName`, "like", `%${sanitizedValue}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await query.count("*").first();
|
||||||
|
|
||||||
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Count active certificates for sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findCertificatesEligibleForRenewal = async ({
|
const findCertificatesEligibleForRenewal = async ({
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
@@ -159,7 +266,7 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findWithPrivateKeyInfo = async (
|
const findWithPrivateKeyInfo = async (
|
||||||
filter: Partial<TCertificates>,
|
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
|
||||||
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
|
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
|
||||||
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||||
try {
|
try {
|
||||||
@@ -167,8 +274,18 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
.replicaNode()(TableName.Certificate)
|
.replicaNode()(TableName.Certificate)
|
||||||
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
||||||
.select(selectAllTableCols(TableName.Certificate))
|
.select(selectAllTableCols(TableName.Certificate))
|
||||||
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
|
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"));
|
||||||
.where(filter);
|
|
||||||
|
Object.entries(filter).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (key === "friendlyName" || key === "commonName") {
|
||||||
|
const sanitizedValue = String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, "like", `%${sanitizedValue}%`);
|
||||||
|
} else {
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (options?.offset) {
|
if (options?.offset) {
|
||||||
query = query.offset(options.offset);
|
query = query.offset(options.offset);
|
||||||
@@ -197,10 +314,13 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
...certificateOrm,
|
...certificateOrm,
|
||||||
countCertificatesInProject,
|
countCertificatesInProject,
|
||||||
|
countActiveCertificatesForSync,
|
||||||
countCertificatesForPkiSubscriber,
|
countCertificatesForPkiSubscriber,
|
||||||
findLatestActiveCertForSubscriber,
|
findLatestActiveCertForSubscriber,
|
||||||
findAllActiveCertsForSubscriber,
|
findAllActiveCertsForSubscriber,
|
||||||
findExpiredSyncedCertificates,
|
findExpiredSyncedCertificates,
|
||||||
|
findActiveCertificatesByIds,
|
||||||
|
findActiveCertificatesForSync,
|
||||||
findCertificatesEligibleForRenewal,
|
findCertificatesEligibleForRenewal,
|
||||||
findWithPrivateKeyInfo
|
findWithPrivateKeyInfo
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ import { TCertificateAuthorityDALFactory } from "@app/services/certificate-autho
|
|||||||
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||||
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
import { triggerAutoSyncForCertificate } from "@app/services/pki-sync/pki-sync-utils";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ type TCertificateServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
|
||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
};
|
};
|
||||||
@@ -76,6 +78,7 @@ export const certificateServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
}: TCertificateServiceFactoryDep) => {
|
}: TCertificateServiceFactoryDep) => {
|
||||||
@@ -166,10 +169,12 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||||
|
|
||||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
// Trigger auto sync for PKI syncs connected to this certificate
|
||||||
if (cert.pkiSubscriberId) {
|
await triggerAutoSyncForCertificate(cert.id, {
|
||||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
certificateSyncDAL,
|
||||||
}
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletedCert
|
deletedCert
|
||||||
@@ -235,10 +240,12 @@ export const certificateServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
// Trigger auto sync for PKI syncs connected to this certificate
|
||||||
if (cert.pkiSubscriberId) {
|
await triggerAutoSyncForCertificate(cert.id, {
|
||||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
certificateSyncDAL,
|
||||||
}
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
// Note: External CA revocation handling would go here for supported CA types
|
// Note: External CA revocation handling would go here for supported CA types
|
||||||
// Currently, only internal CAs and ACME CAs support revocation
|
// Currently, only internal CAs and ACME CAs support revocation
|
||||||
|
|||||||
+297
-85
@@ -3,7 +3,9 @@ import * as AWS from "aws-sdk";
|
|||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TCertificateSyncs } from "@app/db/schemas";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||||
@@ -14,6 +16,9 @@ import {
|
|||||||
AwsConnectionAssumeRoleCredentialsSchema
|
AwsConnectionAssumeRoleCredentialsSchema
|
||||||
} from "@app/services/app-connection/aws/aws-connection-schemas";
|
} from "@app/services/app-connection/aws/aws-connection-schemas";
|
||||||
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
|
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
|
||||||
@@ -88,39 +93,6 @@ const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): b
|
|||||||
return isAwsIssuedCertificate(certificate);
|
return isAwsIssuedCertificate(certificate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => {
|
|
||||||
if (!tags || !Array.isArray(tags)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => {
|
|
||||||
return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateCertificateIdentification = (
|
|
||||||
certName: string,
|
|
||||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }
|
|
||||||
): boolean => {
|
|
||||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const certNameTag = findInfisicalCertificateTag(existingCert.Tags);
|
|
||||||
|
|
||||||
if (!certNameTag || !certNameTag.Value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return certNameTag.Value === certName;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateCertificateNameSchema = (schema: string): void => {
|
const validateCertificateNameSchema = (schema: string): void => {
|
||||||
if (!schema.includes("{{certificateId}}")) {
|
if (!schema.includes("{{certificateId}}")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -174,6 +146,21 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
|
|||||||
return sanitizedCertificateName;
|
return sanitizedCertificateName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
| "removeCertificates"
|
||||||
|
| "addCertificates"
|
||||||
|
| "findByPkiSyncAndCertificate"
|
||||||
|
| "updateSyncStatus"
|
||||||
|
| "updateById"
|
||||||
|
| "findByPkiSyncId"
|
||||||
|
>;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
const getAwsAcmClient = async (
|
const getAwsAcmClient = async (
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
region: AWSRegion,
|
region: AWSRegion,
|
||||||
@@ -230,7 +217,9 @@ const getAwsAcmClient = async (
|
|||||||
|
|
||||||
export const awsCertificateManagerPkiSyncFactory = ({
|
export const awsCertificateManagerPkiSyncFactory = ({
|
||||||
kmsService,
|
kmsService,
|
||||||
appConnectionDAL
|
appConnectionDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
|
certificateDAL
|
||||||
}: TAwsCertificateManagerPkiSyncFactoryDeps) => {
|
}: TAwsCertificateManagerPkiSyncFactoryDeps) => {
|
||||||
const deleteCertificateFromAcm = async (
|
const deleteCertificateFromAcm = async (
|
||||||
acm: AWS.ACM,
|
acm: AWS.ACM,
|
||||||
@@ -392,79 +381,201 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
);
|
);
|
||||||
|
|
||||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
const {
|
||||||
|
acmCertificates
|
||||||
|
}: {
|
||||||
|
acmCertificates: Record<
|
||||||
|
string,
|
||||||
|
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||||
|
>;
|
||||||
|
} = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||||
|
|
||||||
|
const acmCertificatesByArn = new Map<string, (typeof acmCertificates)[string]>();
|
||||||
|
Object.values(acmCertificates).forEach((acmCert) => {
|
||||||
|
if (acmCert.arn) {
|
||||||
|
acmCertificatesByArn.set(acmCert.arn, acmCert);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
|
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||||
|
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||||
|
|
||||||
|
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||||
|
if (record.certificateId) {
|
||||||
|
syncRecordsByCertId.set(record.certificateId, record);
|
||||||
|
}
|
||||||
|
if (record.externalIdentifier) {
|
||||||
|
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const setCertificates: CertificateImportRequest[] = [];
|
const setCertificates: CertificateImportRequest[] = [];
|
||||||
|
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||||
|
|
||||||
const activeCertificateNames = Object.keys(certificateMap);
|
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean; canRemoveCertificates?: boolean } | undefined;
|
||||||
|
const preserveArn = syncOptions?.preserveArn ?? true;
|
||||||
|
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||||
|
|
||||||
Object.entries(certificateMap).forEach(([certName, certData]) => {
|
const activeExternalIdentifiers = new Set<string>();
|
||||||
const { cert, privateKey, certificateChain } = certData;
|
|
||||||
const certificateName = generateCertificateName(certName, pkiSync);
|
|
||||||
|
|
||||||
const existingCert = Object.values(acmCertificates).find((acmCert) =>
|
for (const [certName, certData] of Object.entries(certificateMap)) {
|
||||||
validateCertificateIdentification(certName, acmCert)
|
const { cert, privateKey, certificateChain, certificateId } = certData;
|
||||||
);
|
|
||||||
|
|
||||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
validateCertificateContent(cert, privateKey);
|
validateCertificateContent(cert, privateKey);
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
throw new PkiSyncError({
|
const errorMessage = validationError instanceof Error ? validationError.message : String(validationError);
|
||||||
message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
validationErrors.push({
|
||||||
shouldRetry: false,
|
name: certName,
|
||||||
context: {
|
error: `Certificate validation failed: ${errorMessage}`
|
||||||
certificateName,
|
|
||||||
certName
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdateCert) {
|
if (preserveArn && certificateId && typeof certificateId === "string") {
|
||||||
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (certificate?.renewedByCertificateId) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateName = generateCertificateName(certName, pkiSync);
|
||||||
|
|
||||||
|
let targetArn: string | undefined;
|
||||||
|
let shouldCreateNew = false;
|
||||||
|
|
||||||
|
if (!certificateId || typeof certificateId !== "string") {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
} else {
|
||||||
|
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||||
|
const isRenewal = !!currentCertificate?.renewedFromCertificateId;
|
||||||
|
|
||||||
|
if (isRenewal) {
|
||||||
|
const currentSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||||
|
const oldCertificateId = currentCertificate.renewedFromCertificateId;
|
||||||
|
const oldSyncRecord = oldCertificateId ? syncRecordsByCertId.get(oldCertificateId) : undefined;
|
||||||
|
|
||||||
|
if (currentSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAcmCert = acmCertificatesByArn.get(currentSyncRecord.externalIdentifier);
|
||||||
|
|
||||||
|
if (existingAcmCert) {
|
||||||
|
if (!preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
} else if (preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||||
|
targetArn = currentSyncRecord.externalIdentifier;
|
||||||
|
shouldCreateNew = true;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
|
||||||
|
if (oldCertificateId && oldSyncRecord) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetArn = currentSyncRecord.externalIdentifier;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
shouldCreateNew = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else if (preserveArn && oldSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAcmCert = acmCertificatesByArn.get(oldSyncRecord.externalIdentifier);
|
||||||
|
|
||||||
|
if (existingAcmCert) {
|
||||||
|
targetArn = oldSyncRecord.externalIdentifier;
|
||||||
|
shouldCreateNew = true;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
if (oldCertificateId) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||||
|
if (existingSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAcmCert = acmCertificatesByArn.get(existingSyncRecord.externalIdentifier);
|
||||||
|
if (existingAcmCert) {
|
||||||
|
targetArn = existingSyncRecord.externalIdentifier;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
shouldCreateNew = false;
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCreateNew) {
|
||||||
setCertificates.push({
|
setCertificates.push({
|
||||||
key: certName,
|
key: certName,
|
||||||
name: certificateName,
|
name: certificateName,
|
||||||
cert,
|
cert,
|
||||||
privateKey,
|
privateKey,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
existingArn: existingCert?.arn
|
existingArn: targetArn,
|
||||||
|
certificateId: certificateId as string
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Identify expired/removed certificates that need to be cleaned up from ACM
|
if (targetArn) {
|
||||||
const certificatesToRemove = Object.values(acmCertificates)
|
activeExternalIdentifiers.add(targetArn);
|
||||||
.filter((acmCert) => {
|
}
|
||||||
if (!acmCert.arn || !acmCert.Tags) {
|
}
|
||||||
return false;
|
|
||||||
|
const certificatesToRemove: string[] = [];
|
||||||
|
|
||||||
|
if (canRemoveCertificates) {
|
||||||
|
existingSyncRecords.forEach((syncRecord) => {
|
||||||
|
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||||
|
const acmCert = acmCertificatesByArn.get(syncRecord.externalIdentifier);
|
||||||
|
if (acmCert?.arn) {
|
||||||
|
certificatesToRemove.push(acmCert.arn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const certNameTag = findInfisicalCertificateTag(acmCert.Tags);
|
Object.values(acmCertificates).forEach((acmCert) => {
|
||||||
if (!certNameTag || !certNameTag.Value) {
|
if (acmCert.arn && acmCert.Tags) {
|
||||||
return false;
|
const hasInfisicalTag = acmCert.Tags.some((tag) => tag.Key === INFISICAL_CERTIFICATE_TAG && tag.Value);
|
||||||
|
|
||||||
|
if (hasInfisicalTag) {
|
||||||
|
const isTrackedInSyncRecords = existingSyncRecords.some(
|
||||||
|
(record) => record.externalIdentifier === acmCert.arn
|
||||||
|
);
|
||||||
|
const isInActiveSet = activeExternalIdentifiers.has(acmCert.arn);
|
||||||
|
if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(acmCert.arn)) {
|
||||||
|
certificatesToRemove.push(acmCert.arn);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const isActive = activeCertificateNames.includes(certNameTag.Value);
|
}
|
||||||
return !isActive;
|
|
||||||
})
|
|
||||||
.map((acmCert) => acmCert.arn!)
|
|
||||||
.filter((arn) => arn);
|
|
||||||
|
|
||||||
const uploadResults = await executeWithConcurrencyLimit(
|
const uploadResults = await executeWithConcurrencyLimit(
|
||||||
setCertificates,
|
setCertificates,
|
||||||
async ({ key, name, cert, privateKey, certificateChain, existingArn }) => {
|
async ({ key, name, cert, privateKey, certificateChain, existingArn, certificateId }) => {
|
||||||
try {
|
try {
|
||||||
const importParams: AWS.ACM.ImportCertificateRequest = {
|
const importParams: AWS.ACM.ImportCertificateRequest = {
|
||||||
Certificate: cert,
|
Certificate: cert,
|
||||||
PrivateKey: privateKey,
|
PrivateKey: privateKey
|
||||||
Tags: [
|
};
|
||||||
|
|
||||||
|
if (!existingArn) {
|
||||||
|
importParams.Tags = [
|
||||||
{
|
{
|
||||||
Key: INFISICAL_CERTIFICATE_TAG,
|
Key: INFISICAL_CERTIFICATE_TAG,
|
||||||
Value: key
|
Value: key
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
};
|
}
|
||||||
|
|
||||||
if (certificateChain && certificateChain.trim().length > 0) {
|
if (certificateChain && certificateChain.trim().length > 0) {
|
||||||
importParams.CertificateChain = certificateChain;
|
importParams.CertificateChain = certificateChain;
|
||||||
@@ -478,6 +589,57 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
syncId: pkiSync.id
|
syncId: pkiSync.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (existingArn && response.CertificateArn) {
|
||||||
|
try {
|
||||||
|
// Small delay to ensure AWS ACM has processed the certificate import
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => resolve(), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await withRateLimitRetry(
|
||||||
|
() =>
|
||||||
|
acm
|
||||||
|
.addTagsToCertificate({
|
||||||
|
CertificateArn: response.CertificateArn!,
|
||||||
|
Tags: [
|
||||||
|
{
|
||||||
|
Key: INFISICAL_CERTIFICATE_TAG,
|
||||||
|
Value: key
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.promise(),
|
||||||
|
{
|
||||||
|
operation: "add-tags-to-certificate",
|
||||||
|
syncId: pkiSync.id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (tagError) {
|
||||||
|
const errorMessage = tagError instanceof Error ? tagError.message : "Unknown tagging error";
|
||||||
|
logger.warn(
|
||||||
|
`Failed to add tags to certificate ${key} (ARN: ${response.CertificateArn}): ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.CertificateArn && certificateId) {
|
||||||
|
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||||
|
if (existingCertSync) {
|
||||||
|
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||||
|
externalIdentifier: response.CertificateArn,
|
||||||
|
syncStatus: CertificateSyncStatus.Succeeded,
|
||||||
|
lastSyncedAt: new Date()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||||
|
{
|
||||||
|
certificateId,
|
||||||
|
externalIdentifier: response.CertificateArn
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { key, name, success: true, response };
|
return { key, name, success: true, response };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -520,15 +682,21 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
const details: {
|
const details: {
|
||||||
failedUploads?: Array<{ name: string; error: string }>;
|
failedUploads?: Array<{ name: string; error: string }>;
|
||||||
failedRemovals?: Array<{ name: string; error: string }>;
|
failedRemovals?: Array<{ name: string; error: string }>;
|
||||||
|
validationErrors?: Array<{ name: string; error: string }>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
details.validationErrors = validationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
if (failedUploads.length > 0) {
|
if (failedUploads.length > 0) {
|
||||||
details.failedUploads = failedUploads.map((failure, index) => {
|
details.failedUploads = failedUploads.map((failure, index) => {
|
||||||
const certificateName = setCertificates[index]?.name || "unknown";
|
const certificateRequest = setCertificates[index];
|
||||||
|
const certificateName = certificateRequest?.name || certificateRequest?.key || "unknown";
|
||||||
let errorMessage = "Unknown error";
|
let errorMessage = "Unknown error";
|
||||||
|
|
||||||
if (failure.status === "rejected") {
|
if (failure.status === "rejected") {
|
||||||
errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
errorMessage = failure.reason instanceof Error ? failure.reason.message : String(failure.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -567,7 +735,8 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
|
|
||||||
const removeCertificates = async (
|
const removeCertificates = async (
|
||||||
pkiSync: TPkiSyncWithCredentials,
|
pkiSync: TPkiSyncWithCredentials,
|
||||||
certificateNames: string[]
|
certificateNames: string[],
|
||||||
|
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||||
): Promise<RemoveCertificatesResult> => {
|
): Promise<RemoveCertificatesResult> => {
|
||||||
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
||||||
const acm = await getAwsAcmClient(
|
const acm = await getAwsAcmClient(
|
||||||
@@ -577,22 +746,33 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
);
|
);
|
||||||
|
|
||||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
|
|
||||||
const certificateArnsToRemove: string[] = [];
|
const certificateArnsToRemove: string[] = [];
|
||||||
|
const certificateIdToArnMap = new Map<string, string>();
|
||||||
for (const certName of certificateNames) {
|
for (const certName of certificateNames) {
|
||||||
const matchingCerts = Object.values(acmCertificates).filter((acmCert) =>
|
const certificateData = deps?.certificateMap?.[certName];
|
||||||
validateCertificateIdentification(certName, acmCert)
|
if (certificateData?.certificateId) {
|
||||||
);
|
const { certificateId } = certificateData;
|
||||||
|
|
||||||
for (const acmCert of matchingCerts) {
|
if (typeof certificateId === "string") {
|
||||||
if (acmCert.arn) {
|
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId);
|
||||||
certificateArnsToRemove.push(acmCert.arn);
|
|
||||||
|
if (syncRecord?.externalIdentifier) {
|
||||||
|
certificateArnsToRemove.push(syncRecord.externalIdentifier);
|
||||||
|
certificateIdToArnMap.set(certificateId, syncRecord.externalIdentifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certificateArnsToRemove.length === 0) {
|
||||||
|
return {
|
||||||
|
removed: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: certificateNames.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const results = await executeWithConcurrencyLimit(
|
const results = await executeWithConcurrencyLimit(
|
||||||
certificateArnsToRemove,
|
certificateArnsToRemove,
|
||||||
async (certificateArn) =>
|
async (certificateArn) =>
|
||||||
@@ -602,6 +782,38 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
|
|
||||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||||
|
|
||||||
|
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||||
|
for (const failure of failedRemovals) {
|
||||||
|
if (failure.status === "rejected") {
|
||||||
|
const failedArn = certificateArnsToRemove[results.indexOf(failure)];
|
||||||
|
const certificateId = Array.from(certificateIdToArnMap.entries()).find(([, arn]) => arn === failedArn)?.[0];
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||||
|
await deps.certificateSyncDAL.updateSyncStatus(
|
||||||
|
pkiSync.id,
|
||||||
|
certificateId,
|
||||||
|
CertificateSyncStatus.Failed,
|
||||||
|
`Failed to remove from AWS: ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||||
|
if (successfulRemovals.length > 0) {
|
||||||
|
const successfulArns = new Set(successfulRemovals.map((_, index) => certificateArnsToRemove[index]));
|
||||||
|
|
||||||
|
const certificateIdsToRemove = Array.from(certificateIdToArnMap.entries())
|
||||||
|
.filter(([, arn]) => successfulArns.has(arn))
|
||||||
|
.map(([certificateId]) => certificateId);
|
||||||
|
|
||||||
|
if (certificateIdsToRemove.length > 0) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (failedRemovals.length > 0) {
|
if (failedRemovals.length > 0) {
|
||||||
const failedReasons = failedRemovals.map((failure) => {
|
const failedReasons = failedRemovals.map((failure) => {
|
||||||
if (failure.status === "rejected") {
|
if (failure.status === "rejected") {
|
||||||
|
|||||||
+9
-4
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
|
|||||||
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||||
canImportCertificates: z.boolean().default(false),
|
canImportCertificates: z.boolean().default(false),
|
||||||
canRemoveCertificates: z.boolean().default(true),
|
canRemoveCertificates: z.boolean().default(true),
|
||||||
|
preserveArn: z.boolean().default(true),
|
||||||
certificateNameSchema: z
|
certificateNameSchema: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -28,6 +29,9 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
|||||||
|
|
||||||
const testName = schema
|
const testName = schema
|
||||||
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
|
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
|
||||||
|
.replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id")
|
||||||
|
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name")
|
||||||
|
.replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name")
|
||||||
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
|
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
|
||||||
|
|
||||||
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
||||||
@@ -43,7 +47,7 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager"
|
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager. Available placeholders: {{certificateId}}, {{profileId}}, {{commonName}}, {{friendlyName}}, {{environment}}"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -60,9 +64,10 @@ export const CreateAwsCertificateManagerPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().default(true),
|
isAutoSyncEnabled: z.boolean().default(true),
|
||||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
||||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string(),
|
connectionId: z.string(),
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateIds: z.array(z.string().uuid()).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||||
@@ -71,7 +76,7 @@ export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().optional(),
|
isAutoSyncEnabled: z.boolean().optional(),
|
||||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
||||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string().optional()
|
connectionId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
@@ -39,6 +39,7 @@ export interface SyncCertificatesResult {
|
|||||||
details?: {
|
details?: {
|
||||||
failedUploads?: Array<{ name: string; error: string }>;
|
failedUploads?: Array<{ name: string; error: string }>;
|
||||||
failedRemovals?: Array<{ name: string; error: string }>;
|
failedRemovals?: Array<{ name: string; error: string }>;
|
||||||
|
validationErrors?: Array<{ name: string; error: string }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,4 +56,5 @@ export interface CertificateImportRequest {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
certificateChain?: string;
|
certificateChain?: string;
|
||||||
existingArn?: string;
|
existingArn?: string;
|
||||||
|
certificateId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
|
import { TCertificateSyncs } from "@app/db/schemas";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
|
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
|
||||||
@@ -32,7 +36,9 @@ const extractCertificateNameFromId = (certificateId: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
|
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
|
||||||
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
|
const syncOptions = pkiSync.syncOptions as
|
||||||
|
| { certificateNameSchema?: string; canRemoveCertificates?: boolean }
|
||||||
|
| undefined;
|
||||||
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
||||||
|
|
||||||
if (certificateNameSchema) {
|
if (certificateNameSchema) {
|
||||||
@@ -46,6 +52,16 @@ const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyn
|
|||||||
type TAzureKeyVaultPkiSyncFactoryDeps = {
|
type TAzureKeyVaultPkiSyncFactoryDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
| "removeCertificates"
|
||||||
|
| "addCertificates"
|
||||||
|
| "findByPkiSyncAndCertificate"
|
||||||
|
| "updateById"
|
||||||
|
| "findByPkiSyncId"
|
||||||
|
| "updateSyncStatus"
|
||||||
|
>;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseCertificateX509Props = (certPem: string) => {
|
const parseCertificateX509Props = (certPem: string) => {
|
||||||
@@ -188,7 +204,12 @@ const parseCertificateKeyProps = (certPem: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
export const azureKeyVaultPkiSyncFactory = ({
|
||||||
|
kmsService,
|
||||||
|
appConnectionDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
|
certificateDAL
|
||||||
|
}: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
||||||
const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
|
const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
|
||||||
const paginateAzureKeyVaultCertificates = async () => {
|
const paginateAzureKeyVaultCertificates = async () => {
|
||||||
let result: GetAzureKeyVaultCertificate[] = [];
|
let result: GetAzureKeyVaultCertificate[] = [];
|
||||||
@@ -325,48 +346,126 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
pkiSync.id
|
pkiSync.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
|
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||||
|
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||||
|
|
||||||
|
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||||
|
if (record.certificateId) {
|
||||||
|
syncRecordsByCertId.set(record.certificateId, record);
|
||||||
|
}
|
||||||
|
if (record.externalIdentifier) {
|
||||||
|
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const setCertificates: {
|
const setCertificates: {
|
||||||
key: string;
|
key: string;
|
||||||
cert: string;
|
cert: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
certificateChain?: string;
|
certificateChain?: string;
|
||||||
|
certificateId?: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
// Track which certificates should exist in Azure Key Vault
|
const syncOptions = pkiSync.syncOptions as
|
||||||
const activeCertificateNames = Object.keys(certificateMap);
|
| { certificateNameSchema?: string; canRemoveCertificates?: boolean; enableVersioning?: boolean }
|
||||||
|
| undefined;
|
||||||
|
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||||
|
const enableVersioning = syncOptions?.enableVersioning ?? true;
|
||||||
|
|
||||||
|
const activeExternalIdentifiers = new Set<string>();
|
||||||
|
|
||||||
// Iterate through certificates to sync to Azure Key Vault
|
// Iterate through certificates to sync to Azure Key Vault
|
||||||
Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => {
|
for (const [certName, { cert, privateKey, certificateChain, certificateId }] of Object.entries(certificateMap)) {
|
||||||
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
|
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
|
||||||
return;
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCert = vaultCertificates[certName];
|
if (enableVersioning && typeof certificateId === "string") {
|
||||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (certificate?.renewedByCertificateId) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldUpdateCert) {
|
let targetCertName = certName;
|
||||||
|
let shouldCreateNew = false;
|
||||||
|
|
||||||
|
if (typeof certificateId === "string") {
|
||||||
|
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||||
|
|
||||||
|
if (existingSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAzureCert = vaultCertificates[existingSyncRecord.externalIdentifier];
|
||||||
|
|
||||||
|
if (existingAzureCert && enableVersioning) {
|
||||||
|
targetCertName = existingSyncRecord.externalIdentifier;
|
||||||
|
activeExternalIdentifiers.add(targetCertName);
|
||||||
|
|
||||||
|
const shouldUpdateCert = existingAzureCert.cert !== cert;
|
||||||
|
if (shouldUpdateCert) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else if (!existingAzureCert) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
} else if (!enableVersioning) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCreateNew || !vaultCertificates[targetCertName] || vaultCertificates[targetCertName].cert !== cert) {
|
||||||
setCertificates.push({
|
setCertificates.push({
|
||||||
key: certName,
|
key: targetCertName,
|
||||||
cert,
|
cert,
|
||||||
privateKey,
|
privateKey,
|
||||||
certificateChain
|
certificateChain,
|
||||||
|
certificateId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Identify expired/removed certificates that need to be cleaned up from Azure Key Vault
|
if (targetCertName) {
|
||||||
// Only remove certificates that were managed by Infisical (match naming schema)
|
activeExternalIdentifiers.add(targetCertName);
|
||||||
const certificatesToRemove = Object.keys(vaultCertificates).filter(
|
}
|
||||||
(vaultCertName) =>
|
}
|
||||||
isInfisicalManagedCertificate(vaultCertName, pkiSync) &&
|
|
||||||
!activeCertificateNames.includes(vaultCertName) &&
|
const certificatesToRemove: string[] = [];
|
||||||
!disabledAzureKeyVaultCertificateKeys.includes(vaultCertName)
|
|
||||||
);
|
if (canRemoveCertificates) {
|
||||||
|
existingSyncRecords.forEach((syncRecord) => {
|
||||||
|
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||||
|
if (vaultCertificates[syncRecord.externalIdentifier]) {
|
||||||
|
certificatesToRemove.push(syncRecord.externalIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(vaultCertificates).forEach((certificateName) => {
|
||||||
|
const isInfisicalManaged = isInfisicalManagedCertificate(certificateName, pkiSync);
|
||||||
|
|
||||||
|
if (isInfisicalManaged) {
|
||||||
|
const isTrackedInSyncRecords = existingSyncRecords.some(
|
||||||
|
(record) => record.externalIdentifier === certificateName
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInActiveSet = activeExternalIdentifiers.has(certificateName);
|
||||||
|
|
||||||
|
if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(certificateName)) {
|
||||||
|
certificatesToRemove.push(certificateName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Upload certificates to Azure Key Vault with rate limiting
|
// Upload certificates to Azure Key Vault with rate limiting
|
||||||
const uploadResults = await executeWithConcurrencyLimit(
|
const uploadResults = await executeWithConcurrencyLimit(
|
||||||
setCertificates,
|
setCertificates,
|
||||||
async ({ key, cert, privateKey, certificateChain }) => {
|
async ({ key, cert, privateKey, certificateChain, certificateId }) => {
|
||||||
try {
|
try {
|
||||||
// Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
|
// Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
|
||||||
let combinedPem = "";
|
let combinedPem = "";
|
||||||
@@ -428,6 +527,31 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||||
|
if (existingCertSync) {
|
||||||
|
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||||
|
externalIdentifier: key,
|
||||||
|
syncStatus: CertificateSyncStatus.Succeeded,
|
||||||
|
lastSyncedAt: new Date()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||||
|
{
|
||||||
|
certificateId,
|
||||||
|
externalIdentifier: key
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableVersioning) {
|
||||||
|
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (currentCertificate?.renewedFromCertificateId) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { key, success: true, response: response.data as unknown };
|
return { key, success: true, response: response.data as unknown };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
@@ -599,19 +723,43 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCertificates = async (pkiSync: TPkiSyncWithCredentials, certificateNames: string[]) => {
|
const removeCertificates = async (
|
||||||
|
pkiSync: TPkiSyncWithCredentials,
|
||||||
|
certificateNames: string[],
|
||||||
|
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||||
|
) => {
|
||||||
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
|
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
// Cast destination config to Azure Key Vault config
|
// Cast destination config to Azure Key Vault config
|
||||||
const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
|
const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
|
||||||
|
|
||||||
// Only remove certificates that are managed by Infisical (match naming schema)
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
const infisicalManagedCertNames = certificateNames.filter((certName) =>
|
const certificateNamesToRemove: string[] = [];
|
||||||
isInfisicalManagedCertificate(certName, pkiSync)
|
const certificateIdToNameMap = new Map<string, string>();
|
||||||
);
|
|
||||||
|
for (const certName of certificateNames) {
|
||||||
|
if (deps?.certificateMap?.[certName]?.certificateId) {
|
||||||
|
const { certificateId } = deps.certificateMap[certName];
|
||||||
|
|
||||||
|
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId);
|
||||||
|
|
||||||
|
if (syncRecord?.externalIdentifier && typeof certificateId === "string") {
|
||||||
|
certificateNamesToRemove.push(syncRecord.externalIdentifier);
|
||||||
|
certificateIdToNameMap.set(certificateId, syncRecord.externalIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificateNamesToRemove.length === 0) {
|
||||||
|
return {
|
||||||
|
removed: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: certificateNames.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const results = await executeWithConcurrencyLimit(
|
const results = await executeWithConcurrencyLimit(
|
||||||
infisicalManagedCertNames,
|
certificateNamesToRemove,
|
||||||
async (certName) => {
|
async (certName) => {
|
||||||
try {
|
try {
|
||||||
const response = await request.delete(
|
const response = await request.delete(
|
||||||
@@ -646,8 +794,44 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
},
|
},
|
||||||
{ operation: "remove-specific-certificates", syncId: pkiSync.id }
|
{ operation: "remove-specific-certificates", syncId: pkiSync.id }
|
||||||
);
|
);
|
||||||
|
|
||||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||||
|
|
||||||
|
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||||
|
for (const failure of failedRemovals) {
|
||||||
|
if (failure.status === "rejected") {
|
||||||
|
const failedCertName = certificateNamesToRemove[results.indexOf(failure)];
|
||||||
|
|
||||||
|
const certificateId = Array.from(certificateIdToNameMap.entries()).find(
|
||||||
|
([, name]) => name === failedCertName
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const errorMessage = (failure.reason as Error)?.message || "Unknown error";
|
||||||
|
await deps.certificateSyncDAL.updateSyncStatus(
|
||||||
|
pkiSync.id,
|
||||||
|
certificateId,
|
||||||
|
CertificateSyncStatus.Failed,
|
||||||
|
`Failed to remove from Azure: ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||||
|
if (successfulRemovals.length > 0) {
|
||||||
|
const successfulCertNames = new Set(successfulRemovals.map((_, index) => certificateNamesToRemove[index]));
|
||||||
|
|
||||||
|
const certificateIdsToRemove = Array.from(certificateIdToNameMap.entries())
|
||||||
|
.filter(([, name]) => successfulCertNames.has(name))
|
||||||
|
.map(([certificateId]) => certificateId);
|
||||||
|
|
||||||
|
if (certificateIdsToRemove.length > 0) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (failedRemovals.length > 0) {
|
if (failedRemovals.length > 0) {
|
||||||
const failedReasons = failedRemovals.map((failure) => {
|
const failedReasons = failedRemovals.map((failure) => {
|
||||||
if (failure.status === "rejected") {
|
if (failure.status === "rejected") {
|
||||||
@@ -660,16 +844,16 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`,
|
message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`,
|
||||||
context: {
|
context: {
|
||||||
failedReasons,
|
failedReasons,
|
||||||
totalCertificates: infisicalManagedCertNames.length,
|
totalCertificates: certificateNamesToRemove.length,
|
||||||
failedCount: failedRemovals.length
|
failedCount: failedRemovals.length
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
removed: infisicalManagedCertNames.length - failedRemovals.length,
|
removed: certificateNamesToRemove.length - failedRemovals.length,
|
||||||
failed: failedRemovals.length,
|
failed: failedRemovals.length,
|
||||||
skipped: certificateNames.length - infisicalManagedCertNames.length
|
skipped: certificateNames.length - certificateNamesToRemove.length
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const AzureKeyVaultPkiSyncConfigSchema = z.object({
|
|||||||
const AzureKeyVaultPkiSyncOptionsSchema = z.object({
|
const AzureKeyVaultPkiSyncOptionsSchema = z.object({
|
||||||
canImportCertificates: z.boolean().default(false),
|
canImportCertificates: z.boolean().default(false),
|
||||||
canRemoveCertificates: z.boolean().default(true),
|
canRemoveCertificates: z.boolean().default(true),
|
||||||
|
enableVersioning: z.boolean().default(true),
|
||||||
certificateNameSchema: z
|
certificateNameSchema: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -50,9 +51,10 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().default(true),
|
isAutoSyncEnabled: z.boolean().default(true),
|
||||||
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
|
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
|
||||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
|
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string(),
|
connectionId: z.string(),
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateIds: z.array(z.string().uuid()).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
||||||
@@ -61,7 +63,7 @@ export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().optional(),
|
isAutoSyncEnabled: z.boolean().optional(),
|
||||||
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
|
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
|
||||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
|
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string().optional()
|
connectionId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { z, ZodSchema } from "zod";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
|
import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
|
||||||
@@ -184,6 +186,8 @@ export const PkiSyncFns = {
|
|||||||
dependencies: {
|
dependencies: {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||||
}
|
}
|
||||||
): Promise<{
|
): Promise<{
|
||||||
uploaded: number;
|
uploaded: number;
|
||||||
@@ -194,17 +198,28 @@ export const PkiSyncFns = {
|
|||||||
failedUploads?: Array<{ name: string; error: string }>;
|
failedUploads?: Array<{ name: string; error: string }>;
|
||||||
failedRemovals?: Array<{ name: string; error: string }>;
|
failedRemovals?: Array<{ name: string; error: string }>;
|
||||||
skippedCertificates?: Array<{ name: string; reason: string }>;
|
skippedCertificates?: Array<{ name: string; reason: string }>;
|
||||||
|
validationErrors?: Array<{ name: string; error: string }>;
|
||||||
};
|
};
|
||||||
}> => {
|
}> => {
|
||||||
switch (pkiSync.destination) {
|
switch (pkiSync.destination) {
|
||||||
case PkiSync.AzureKeyVault: {
|
case PkiSync.AzureKeyVault: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
|
||||||
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
|
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||||
}
|
}
|
||||||
case PkiSync.AwsCertificateManager: {
|
case PkiSync.AwsCertificateManager: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
|
||||||
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -218,19 +233,38 @@ export const PkiSyncFns = {
|
|||||||
dependencies: {
|
dependencies: {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
certificateMap: TCertificateMap;
|
||||||
}
|
}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
switch (pkiSync.destination) {
|
switch (pkiSync.destination) {
|
||||||
case PkiSync.AzureKeyVault: {
|
case PkiSync.AzureKeyVault: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
|
||||||
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames);
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
|
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||||
|
certificateMap: dependencies.certificateMap
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PkiSync.AwsCertificateManager: {
|
case PkiSync.AwsCertificateManager: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
|
||||||
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames);
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
|
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||||
|
certificateMap: dependencies.certificateMap
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AxiosError } from "axios";
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
|
import { TCertificates } from "@app/db/schemas";
|
||||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -25,6 +26,8 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
|
|||||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-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 { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
|
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
import { PkiSyncStatus } from "./pki-sync-enums";
|
import { PkiSyncStatus } from "./pki-sync-enums";
|
||||||
import { PkiSyncError } from "./pki-sync-errors";
|
import { PkiSyncError } from "./pki-sync-errors";
|
||||||
@@ -55,14 +58,12 @@ type TPkiSyncQueueFactoryDep = {
|
|||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
certificateDAL: Pick<
|
certificateDAL: TCertificateDALFactory;
|
||||||
TCertificateDALFactory,
|
|
||||||
"findLatestActiveCertForSubscriber" | "findAllActiveCertsForSubscriber" | "create"
|
|
||||||
>;
|
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
|
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PkiSyncActionJob = Job<
|
type PkiSyncActionJob = Job<
|
||||||
@@ -93,7 +94,8 @@ export const pkiSyncQueueFactory = ({
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL
|
certificateAuthorityCertDAL,
|
||||||
|
certificateSyncDAL
|
||||||
}: TPkiSyncQueueFactoryDep) => {
|
}: TPkiSyncQueueFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -153,25 +155,39 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
const $getInfisicalCertificates = async (
|
const $getInfisicalCertificates = async (
|
||||||
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
|
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
|
||||||
): Promise<TCertificateMap> => {
|
): Promise<{ certificateMap: TCertificateMap; certificateMetadata: Map<string, { id: string; name: string }> }> => {
|
||||||
const { projectId, subscriberId } = pkiSync;
|
const { projectId, subscriberId, id: pkiSyncId } = pkiSync;
|
||||||
|
|
||||||
if (!subscriberId) {
|
|
||||||
throw new PkiSyncError({
|
|
||||||
message: "Invalid PKI Sync source configuration: subscriber no longer exists. Please update source subscriber.",
|
|
||||||
shouldRetry: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const certificateMap: TCertificateMap = {};
|
const certificateMap: TCertificateMap = {};
|
||||||
|
const certificateMetadata = new Map<string, { id: string; name: string }>();
|
||||||
|
let certificates: Array<{ id: string; projectId: string; caCertId?: string | null }> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all active certificates for the subscriber (not just the latest)
|
if (subscriberId) {
|
||||||
const certificates = await certificateDAL.findAllActiveCertsForSubscriber({
|
const subscriberCertificates = await certificateDAL.findAllActiveCertsForSubscriber({
|
||||||
subscriberId
|
subscriberId
|
||||||
});
|
});
|
||||||
|
certificates.push(...subscriberCertificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateIds = await certificateSyncDAL.findCertificateIdsByPkiSyncId(pkiSyncId);
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
const directCertificates = await certificateDAL.findActiveCertificatesByIds(certificateIds);
|
||||||
|
certificates.push(...directCertificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueCertificates = certificates.filter(
|
||||||
|
(cert, index, self) => self.findIndex((c) => c.id === cert.id) === index
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uniqueCertificates.length === 0) {
|
||||||
|
return { certificateMap, certificateMetadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
certificates = uniqueCertificates;
|
||||||
|
|
||||||
for (const certificate of certificates) {
|
for (const certificate of certificates) {
|
||||||
|
const cert = certificate as TCertificates;
|
||||||
try {
|
try {
|
||||||
// Get the certificate body and decrypt the certificate data
|
// Get the certificate body and decrypt the certificate data
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
|
||||||
@@ -246,19 +262,45 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
if (certificateNameSchema) {
|
if (certificateNameSchema) {
|
||||||
const environment = "global";
|
const environment = "global";
|
||||||
certificateName = handlebars.compile(certificateNameSchema)({
|
const templateData = {
|
||||||
certificateId: certificate.id.replace(/-/g, ""),
|
certificateId: certificate.id.replace(/-/g, ""),
|
||||||
|
profileId: cert.profileId?.replace(/-/g, "") || certificate.id.replace(/-/g, ""),
|
||||||
|
commonName: cert.commonName || "",
|
||||||
|
friendlyName: cert.friendlyName || "",
|
||||||
environment
|
environment
|
||||||
});
|
};
|
||||||
|
certificateName = handlebars.compile(certificateNameSchema)(templateData);
|
||||||
} else {
|
} else {
|
||||||
certificateName = `Infisical-${certificate.id.replace(/-/g, "")}`;
|
const stableId = cert.profileId
|
||||||
|
? `${cert.profileId.replace(/-/g, "")}-${(cert.commonName || "").replace(/[^a-zA-Z0-9]/g, "")}`
|
||||||
|
: certificate.id.replace(/-/g, "");
|
||||||
|
certificateName = `Infisical-${stableId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alternativeNames: string[] = [];
|
||||||
|
|
||||||
|
const legacyName = `Infisical-${certificate.id.replace(/-/g, "")}`;
|
||||||
|
if (legacyName !== certificateName) {
|
||||||
|
alternativeNames.push(legacyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert.renewedFromCertificateId) {
|
||||||
|
const originalLegacyName = `Infisical-${cert.renewedFromCertificateId.replace(/-/g, "")}`;
|
||||||
|
alternativeNames.push(originalLegacyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
certificateMap[certificateName] = {
|
certificateMap[certificateName] = {
|
||||||
cert: certificatePem,
|
cert: certificatePem,
|
||||||
privateKey: certPrivateKey || "",
|
privateKey: certPrivateKey || "",
|
||||||
certificateChain
|
certificateChain,
|
||||||
|
alternativeNames,
|
||||||
|
certificateId: certificate.id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
certificateMetadata.set(certificateName, {
|
||||||
|
id: certificate.id,
|
||||||
|
name: certificateName
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
|
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
|
||||||
}
|
}
|
||||||
@@ -281,7 +323,7 @@ export const pkiSyncQueueFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return certificateMap;
|
return { certificateMap, certificateMetadata };
|
||||||
};
|
};
|
||||||
|
|
||||||
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
|
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
|
||||||
@@ -348,12 +390,17 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
|
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
|
||||||
} = pkiSync;
|
} = pkiSync;
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new Error(`App connection not found: ${connectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await decryptAppConnectionCredentials({
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
orgId,
|
orgId,
|
||||||
encryptedCredentials,
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectId: appConnectionProjectId
|
projectId: appConnectionProjectId
|
||||||
});
|
});
|
||||||
@@ -366,11 +413,24 @@ export const pkiSyncQueueFactory = ({
|
|||||||
}
|
}
|
||||||
} as TPkiSyncWithCredentials;
|
} as TPkiSyncWithCredentials;
|
||||||
|
|
||||||
const certificateMap = await $getInfisicalCertificates(pkiSync);
|
const { certificateMap, certificateMetadata } = await $getInfisicalCertificates(pkiSync);
|
||||||
|
|
||||||
|
const statusUpdates = Array.from(certificateMetadata.entries()).map(([, metadata]) => ({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Running,
|
||||||
|
message: "Syncing certificate to destination"
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (statusUpdates.length > 0) {
|
||||||
|
await certificateSyncDAL.bulkUpdateSyncStatus(statusUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
|
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -384,6 +444,60 @@ export const pkiSyncQueueFactory = ({
|
|||||||
"PKI sync operation completed with certificate cleanup"
|
"PKI sync operation completed with certificate cleanup"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const postSyncUpdates: Array<{
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [, metadata] of certificateMetadata.entries()) {
|
||||||
|
postSyncUpdates.push({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Succeeded,
|
||||||
|
message: "Certificate successfully synced to destination"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncResult.details?.validationErrors) {
|
||||||
|
for (const validationError of syncResult.details.validationErrors) {
|
||||||
|
const metadata = certificateMetadata.get(validationError.name);
|
||||||
|
if (metadata) {
|
||||||
|
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
|
||||||
|
if (updateIndex >= 0) {
|
||||||
|
postSyncUpdates[updateIndex] = {
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Failed,
|
||||||
|
message: `${validationError.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncResult.details?.failedUploads) {
|
||||||
|
for (const failure of syncResult.details.failedUploads) {
|
||||||
|
const metadata = certificateMetadata.get(failure.name);
|
||||||
|
if (metadata) {
|
||||||
|
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
|
||||||
|
if (updateIndex >= 0) {
|
||||||
|
postSyncUpdates[updateIndex] = {
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Failed,
|
||||||
|
message: `Failed to sync certificate: ${failure.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postSyncUpdates.length > 0) {
|
||||||
|
await certificateSyncDAL.bulkUpdateSyncStatus(postSyncUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
isSynced = true;
|
isSynced = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -550,17 +664,22 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
|
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
|
||||||
} = pkiSync;
|
} = pkiSync;
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new Error(`App connection not found: ${connectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await decryptAppConnectionCredentials({
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
orgId,
|
orgId,
|
||||||
encryptedCredentials,
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectId: appConnectionProjectId
|
projectId: appConnectionProjectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateMap = await $getInfisicalCertificates(pkiSync);
|
const { certificateMap } = await $getInfisicalCertificates(pkiSync);
|
||||||
|
|
||||||
await PkiSyncFns.removeCertificates(
|
await PkiSyncFns.removeCertificates(
|
||||||
{
|
{
|
||||||
@@ -573,7 +692,10 @@ export const pkiSyncQueueFactory = ({
|
|||||||
Object.keys(certificateMap),
|
Object.keys(certificateMap),
|
||||||
{
|
{
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateMap
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@@ -10,17 +10,24 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
|||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||||
|
|
||||||
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
|
import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
|
||||||
import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
|
import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
|
||||||
import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
|
import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
|
||||||
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
||||||
import {
|
import {
|
||||||
|
TAddCertificatesToPkiSyncDTO,
|
||||||
TCreatePkiSyncDTO,
|
TCreatePkiSyncDTO,
|
||||||
TDeletePkiSyncDTO,
|
TDeletePkiSyncDTO,
|
||||||
TFindPkiSyncByIdDTO,
|
TFindPkiSyncByIdDTO,
|
||||||
|
TListPkiSyncCertificatesDTO,
|
||||||
TListPkiSyncsByProjectId,
|
TListPkiSyncsByProjectId,
|
||||||
TPkiSync,
|
TPkiSync,
|
||||||
|
TPkiSyncCertificate,
|
||||||
|
TRemoveCertificatesFromPkiSyncDTO,
|
||||||
TTriggerPkiSyncImportCertificatesByIdDTO,
|
TTriggerPkiSyncImportCertificatesByIdDTO,
|
||||||
TTriggerPkiSyncRemoveCertificatesByIdDTO,
|
TTriggerPkiSyncRemoveCertificatesByIdDTO,
|
||||||
TTriggerPkiSyncSyncCertificatesByIdDTO,
|
TTriggerPkiSyncSyncCertificatesByIdDTO,
|
||||||
@@ -42,6 +49,17 @@ type TPkiSyncServiceFactoryDep = {
|
|||||||
TPkiSyncDALFactory,
|
TPkiSyncDALFactory,
|
||||||
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
||||||
>;
|
>;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findActiveCertificatesByIds">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
| "findByPkiSyncId"
|
||||||
|
| "findByCertificateId"
|
||||||
|
| "findCertificateIdsByPkiSyncId"
|
||||||
|
| "addCertificates"
|
||||||
|
| "removeCertificates"
|
||||||
|
| "removeAllCertificatesFromSync"
|
||||||
|
| "findWithDetails"
|
||||||
|
>;
|
||||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
@@ -56,12 +74,41 @@ export type TPkiSyncServiceFactory = ReturnType<typeof pkiSyncServiceFactory>;
|
|||||||
|
|
||||||
export const pkiSyncServiceFactory = ({
|
export const pkiSyncServiceFactory = ({
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
}: TPkiSyncServiceFactoryDep) => {
|
}: TPkiSyncServiceFactoryDep) => {
|
||||||
|
const validateCertificatesProjectOwnership = async (certificateIds: string[], expectedProjectId: string) => {
|
||||||
|
if (certificateIds.length === 0) return;
|
||||||
|
|
||||||
|
const certificates = await certificateDAL.findActiveCertificatesByIds(certificateIds);
|
||||||
|
|
||||||
|
if (certificates.length !== certificateIds.length) {
|
||||||
|
const foundIds = certificates.map((cert) => cert.id);
|
||||||
|
const missingIds = certificateIds.filter((id) => !foundIds.includes(id));
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Certificates not found or not active: ${missingIds.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidProjectCertificates = certificates.filter((cert) => cert.projectId !== expectedProjectId);
|
||||||
|
if (invalidProjectCertificates.length > 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificates do not belong to the same project: ${invalidProjectCertificates.map((cert) => cert.id).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidRenewedCertificates = certificates.filter((cert) => cert.renewedByCertificateId);
|
||||||
|
if (invalidRenewedCertificates.length > 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Cannot add renewed certificates to PKI sync: ${invalidRenewedCertificates.map((cert) => cert.id).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
const createPkiSync = async (
|
const createPkiSync = async (
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@@ -72,7 +119,8 @@ export const pkiSyncServiceFactory = ({
|
|||||||
syncOptions = {},
|
syncOptions = {},
|
||||||
subscriberId,
|
subscriberId,
|
||||||
connectionId,
|
connectionId,
|
||||||
projectId
|
projectId,
|
||||||
|
certificateIds = []
|
||||||
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
|
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
): Promise<TPkiSync> => {
|
): Promise<TPkiSync> => {
|
||||||
@@ -114,6 +162,10 @@ export const pkiSyncServiceFactory = ({
|
|||||||
...syncOptions
|
...syncOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await validateCertificatesProjectOwnership(certificateIds, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pkiSync = await pkiSyncDAL.create({
|
const pkiSync = await pkiSyncDAL.create({
|
||||||
name,
|
name,
|
||||||
@@ -128,6 +180,13 @@ export const pkiSyncServiceFactory = ({
|
|||||||
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
|
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await certificateSyncDAL.addCertificates(
|
||||||
|
pkiSync.id,
|
||||||
|
certificateIds.map((id) => ({ certificateId: id }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (pkiSync.isAutoSyncEnabled) {
|
if (pkiSync.isAutoSyncEnabled) {
|
||||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
|
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
|
||||||
}
|
}
|
||||||
@@ -152,7 +211,8 @@ export const pkiSyncServiceFactory = ({
|
|||||||
destinationConfig,
|
destinationConfig,
|
||||||
syncOptions,
|
syncOptions,
|
||||||
subscriberId,
|
subscriberId,
|
||||||
connectionId
|
connectionId,
|
||||||
|
certificateIds
|
||||||
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
|
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
): Promise<TPkiSync> => {
|
): Promise<TPkiSync> => {
|
||||||
@@ -221,6 +281,20 @@ export const pkiSyncServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certificateIds !== undefined) {
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateSyncDAL.removeAllCertificatesFromSync(id);
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await certificateSyncDAL.addCertificates(
|
||||||
|
id,
|
||||||
|
certificateIds.map((certId) => ({ certificateId: certId }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
|
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -266,7 +340,7 @@ export const pkiSyncServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listPkiSyncsByProjectId = async (
|
const listPkiSyncsByProjectId = async (
|
||||||
{ projectId }: TListPkiSyncsByProjectId,
|
{ projectId, certificateId }: TListPkiSyncsByProjectId,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
): Promise<TPkiSync[]> => {
|
): Promise<TPkiSync[]> => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
@@ -282,6 +356,29 @@ export const pkiSyncServiceFactory = ({
|
|||||||
|
|
||||||
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
|
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const syncsWithCertificateInfo = await Promise.all(
|
||||||
|
pkiSyncsWithSubscribers.map(async (sync) => {
|
||||||
|
try {
|
||||||
|
const certificateSyncs = await certificateSyncDAL.findByPkiSyncId(sync.id);
|
||||||
|
const hasCertificate = certificateSyncs.some((certSync) => certSync.certificateId === certificateId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sync,
|
||||||
|
hasCertificate
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...sync,
|
||||||
|
hasCertificate: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return syncsWithCertificateInfo as TPkiSync[];
|
||||||
|
}
|
||||||
|
|
||||||
return pkiSyncsWithSubscribers as TPkiSync[];
|
return pkiSyncsWithSubscribers as TPkiSync[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -433,6 +530,145 @@ export const pkiSyncServiceFactory = ({
|
|||||||
return listPkiSyncOptions();
|
return listPkiSyncOptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addCertificatesToPkiSync = async (
|
||||||
|
{ pkiSyncId, certificateIds }: Omit<TAddCertificatesToPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
): Promise<{
|
||||||
|
addedCertificates: TCertificateSyncs[];
|
||||||
|
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||||
|
}> => {
|
||||||
|
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||||
|
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager,
|
||||||
|
projectId: pkiSync.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||||
|
|
||||||
|
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||||
|
|
||||||
|
const addedCertificates = await certificateSyncDAL.addCertificates(
|
||||||
|
pkiSyncId,
|
||||||
|
certificateIds.map((id) => ({ certificateId: id }))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pkiSync.isAutoSyncEnabled) {
|
||||||
|
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addedCertificates,
|
||||||
|
pkiSyncInfo: {
|
||||||
|
projectId: pkiSync.projectId,
|
||||||
|
destination: pkiSync.destination,
|
||||||
|
name: pkiSync.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCertificatesFromPkiSync = async (
|
||||||
|
{ pkiSyncId, certificateIds }: Omit<TRemoveCertificatesFromPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
): Promise<{ removedCount: number; pkiSyncInfo: { projectId: string; destination: string; name: string } }> => {
|
||||||
|
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||||
|
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager,
|
||||||
|
projectId: pkiSync.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||||
|
|
||||||
|
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
|
||||||
|
|
||||||
|
if (pkiSync.isAutoSyncEnabled) {
|
||||||
|
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
removedCount,
|
||||||
|
pkiSyncInfo: {
|
||||||
|
projectId: pkiSync.projectId,
|
||||||
|
destination: pkiSync.destination,
|
||||||
|
name: pkiSync.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const listPkiSyncCertificates = async (
|
||||||
|
{ pkiSyncId, offset = 0, limit = 20 }: Omit<TListPkiSyncCertificatesDTO, "projectId">,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
): Promise<{
|
||||||
|
certificates: TPkiSyncCertificate[];
|
||||||
|
totalCount: number;
|
||||||
|
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||||
|
}> => {
|
||||||
|
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||||
|
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager,
|
||||||
|
projectId: pkiSync.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||||
|
|
||||||
|
const result = await certificateSyncDAL.findWithDetails({
|
||||||
|
pkiSyncId,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
const { certificateDetails, totalCount } = result;
|
||||||
|
|
||||||
|
const certificates = certificateDetails.map((detail) => ({
|
||||||
|
id: detail.id,
|
||||||
|
pkiSyncId: detail.pkiSyncId,
|
||||||
|
certificateId: detail.certificateId,
|
||||||
|
syncStatus: (detail.syncStatus as CertificateSyncStatus) || CertificateSyncStatus.Pending,
|
||||||
|
lastSyncMessage: detail.lastSyncMessage || undefined,
|
||||||
|
lastSyncedAt: detail.lastSyncedAt || undefined,
|
||||||
|
createdAt: detail.createdAt,
|
||||||
|
updatedAt: detail.updatedAt,
|
||||||
|
certificateSerialNumber: detail.certificateSerialNumber || undefined,
|
||||||
|
certificateCommonName: detail.certificateCommonName || undefined,
|
||||||
|
certificateAltNames: detail.certificateAltNames || undefined,
|
||||||
|
certificateStatus: detail.certificateStatus || undefined,
|
||||||
|
certificateNotBefore: detail.certificateNotBefore || undefined,
|
||||||
|
certificateNotAfter: detail.certificateNotAfter || undefined,
|
||||||
|
certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId
|
||||||
|
? detail.certificateRenewBeforeDays || undefined
|
||||||
|
: undefined,
|
||||||
|
certificateRenewalError: detail.certificateRenewalError || undefined,
|
||||||
|
pkiSyncName: detail.pkiSyncName || undefined,
|
||||||
|
pkiSyncDestination: detail.pkiSyncDestination || undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificates,
|
||||||
|
totalCount,
|
||||||
|
pkiSyncInfo: {
|
||||||
|
projectId: pkiSync.projectId,
|
||||||
|
destination: pkiSync.destination,
|
||||||
|
name: pkiSync.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createPkiSync,
|
createPkiSync,
|
||||||
updatePkiSync,
|
updatePkiSync,
|
||||||
@@ -442,6 +678,9 @@ export const pkiSyncServiceFactory = ({
|
|||||||
triggerPkiSyncSyncCertificatesById,
|
triggerPkiSyncSyncCertificatesById,
|
||||||
triggerPkiSyncImportCertificatesById,
|
triggerPkiSyncImportCertificatesById,
|
||||||
triggerPkiSyncRemoveCertificatesById,
|
triggerPkiSyncRemoveCertificatesById,
|
||||||
getPkiSyncOptions
|
getPkiSyncOptions,
|
||||||
|
addCertificatesToPkiSync,
|
||||||
|
removeCertificatesFromPkiSync,
|
||||||
|
listPkiSyncCertificates
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
|
|||||||
|
|
||||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { QueueJobs } from "@app/queue";
|
import { QueueJobs } from "@app/queue";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
@@ -70,7 +71,10 @@ export type TPkiSyncListItem = TPkiSync & {
|
|||||||
appConnectionApp: string;
|
appConnectionApp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateMap = Record<string, { cert: string; privateKey: string; certificateChain?: string }>;
|
export type TCertificateMap = Record<
|
||||||
|
string,
|
||||||
|
{ cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[]; certificateId?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export type TCreatePkiSyncDTO = {
|
export type TCreatePkiSyncDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -79,9 +83,10 @@ export type TCreatePkiSyncDTO = {
|
|||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
destinationConfig: Record<string, unknown>;
|
destinationConfig: Record<string, unknown>;
|
||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
certificateIds?: string[];
|
||||||
auditLogInfo: AuditLogInfo;
|
auditLogInfo: AuditLogInfo;
|
||||||
resourceMetadata?: ResourceMetadataDTO;
|
resourceMetadata?: ResourceMetadataDTO;
|
||||||
};
|
};
|
||||||
@@ -94,8 +99,9 @@ export type TUpdatePkiSyncDTO = {
|
|||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
destinationConfig?: Record<string, unknown>;
|
destinationConfig?: Record<string, unknown>;
|
||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
|
certificateIds?: string[];
|
||||||
auditLogInfo: AuditLogInfo;
|
auditLogInfo: AuditLogInfo;
|
||||||
resourceMetadata?: ResourceMetadataDTO;
|
resourceMetadata?: ResourceMetadataDTO;
|
||||||
};
|
};
|
||||||
@@ -108,6 +114,7 @@ export type TDeletePkiSyncDTO = {
|
|||||||
|
|
||||||
export type TListPkiSyncsByProjectId = {
|
export type TListPkiSyncsByProjectId = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
certificateId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TFindPkiSyncByIdDTO = {
|
export type TFindPkiSyncByIdDTO = {
|
||||||
@@ -133,6 +140,48 @@ export type TTriggerPkiSyncRemoveCertificatesByIdDTO = {
|
|||||||
auditLogInfo: AuditLogInfo;
|
auditLogInfo: AuditLogInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAddCertificatesToPkiSyncDTO = {
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateIds: string[];
|
||||||
|
projectId?: string;
|
||||||
|
auditLogInfo: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRemoveCertificatesFromPkiSyncDTO = {
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateIds: string[];
|
||||||
|
projectId?: string;
|
||||||
|
auditLogInfo: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListPkiSyncCertificatesDTO = {
|
||||||
|
pkiSyncId: string;
|
||||||
|
projectId?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPkiSyncCertificate = {
|
||||||
|
id: string;
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
syncStatus: CertificateSyncStatus;
|
||||||
|
lastSyncMessage?: string;
|
||||||
|
lastSyncedAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
||||||
|
|
||||||
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
|
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
||||||
|
|
||||||
@@ -25,3 +28,78 @@ export const triggerAutoSyncForSubscriber = async (
|
|||||||
logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
|
logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const triggerAutoSyncForCertificate = async (
|
||||||
|
certificateId: string,
|
||||||
|
dependencies: {
|
||||||
|
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
|
||||||
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(certificateId);
|
||||||
|
|
||||||
|
if (pkiSyncIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPkiSyncs = await dependencies.pkiSyncDAL.find({
|
||||||
|
isAutoSyncEnabled: true,
|
||||||
|
$in: {
|
||||||
|
id: pkiSyncIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncPromises = allPkiSyncs.map((pkiSync) =>
|
||||||
|
dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id })
|
||||||
|
);
|
||||||
|
await Promise.all(syncPromises);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to trigger auto sync for certificate ${certificateId}:`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addRenewedCertificateToSyncs = async (
|
||||||
|
oldCertificateId: string,
|
||||||
|
newCertificateId: string,
|
||||||
|
dependencies: {
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||||
|
>;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(oldCertificateId);
|
||||||
|
|
||||||
|
if (pkiSyncIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPromises = pkiSyncIds.map(async (pkiSyncId) => {
|
||||||
|
const oldCertificateRecord = await dependencies.certificateSyncDAL.findByPkiSyncAndCertificate(
|
||||||
|
pkiSyncId,
|
||||||
|
oldCertificateId
|
||||||
|
);
|
||||||
|
|
||||||
|
await dependencies.certificateSyncDAL.addCertificates(
|
||||||
|
pkiSyncId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
certificateId: newCertificateId,
|
||||||
|
externalIdentifier: oldCertificateRecord?.externalIdentifier || undefined
|
||||||
|
}
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(addPromises);
|
||||||
|
|
||||||
|
logger.info(`Successfully added renewed certificate ${newCertificateId} to PKI sync(s)`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to add renewed certificate ${newCertificateId} to syncs:`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -156,7 +156,14 @@ type TProjectServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject" | "findWithPrivateKeyInfo">;
|
certificateDAL: Pick<
|
||||||
|
TCertificateDALFactory,
|
||||||
|
| "find"
|
||||||
|
| "countCertificatesInProject"
|
||||||
|
| "findWithPrivateKeyInfo"
|
||||||
|
| "findActiveCertificatesForSync"
|
||||||
|
| "countActiveCertificatesForSync"
|
||||||
|
>;
|
||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
@@ -929,6 +936,7 @@ export const projectServiceFactory = ({
|
|||||||
offset = 0,
|
offset = 0,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
|
forPkiSync = false,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -952,20 +960,35 @@ export const projectServiceFactory = ({
|
|||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
const certificates = await certificateDAL.findWithPrivateKeyInfo(
|
const certificates = forPkiSync
|
||||||
{
|
? await certificateDAL.findActiveCertificatesForSync(
|
||||||
projectId,
|
{
|
||||||
...(friendlyName && { friendlyName }),
|
projectId,
|
||||||
...(commonName && { commonName })
|
...(friendlyName && { friendlyName }),
|
||||||
},
|
...(commonName && { commonName })
|
||||||
{ offset, limit, sort: [["notAfter", "desc"]] }
|
},
|
||||||
);
|
{ offset, limit }
|
||||||
|
)
|
||||||
|
: await certificateDAL.findWithPrivateKeyInfo(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
...(friendlyName && { friendlyName }),
|
||||||
|
...(commonName && { commonName })
|
||||||
|
},
|
||||||
|
{ offset, limit, sort: [["notAfter", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
const count = await certificateDAL.countCertificatesInProject({
|
const count = forPkiSync
|
||||||
projectId,
|
? await certificateDAL.countActiveCertificatesForSync({
|
||||||
friendlyName,
|
projectId,
|
||||||
commonName
|
friendlyName,
|
||||||
});
|
commonName
|
||||||
|
})
|
||||||
|
: await certificateDAL.countCertificatesInProject({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
commonName
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificates,
|
certificates,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export type TListProjectCertsDTO = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
|
forPkiSync?: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TListProjectAlertsDTO = TProjectPermission;
|
export type TListProjectAlertsDTO = TProjectPermission;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ This section walks you through the complete end-to-end process of setting up Azu
|
|||||||
|
|
||||||
<Step title="Create New Azure ADCS Certificate Service CA">
|
<Step title="Create New Azure ADCS Certificate Service CA">
|
||||||
Click **Create CA** and configure:
|
Click **Create CA** and configure:
|
||||||
- **Type**: Choose **Azure AD Certificate Service**
|
- **Type**: Choose **Active Directory Certificate Services (AD CS)**
|
||||||
- **Name**: Friendly name for this CA (e.g., "Production ADCS CA")
|
- **Name**: Friendly name for this CA (e.g., "Production ADCS CA")
|
||||||
- **App Connection**: Choose your ADCS connection from the dropdown
|
- **App Connection**: Choose your ADCS connection from the dropdown
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { faSearch, faX } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import {
|
||||||
|
CertStatus,
|
||||||
|
useAddCertificatesToPkiSync,
|
||||||
|
useListPkiSyncCertificates,
|
||||||
|
useRemoveCertificatesFromPkiSync
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||||
|
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
pkiSync?: TPkiSync;
|
||||||
|
onCertificatesUpdated?: () => void;
|
||||||
|
selectedCertificateIds?: string[];
|
||||||
|
onCertificateSelectionChange?: (certificateIds: string[]) => void;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
saveButtonText?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CertificateManagementModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
pkiSync,
|
||||||
|
onCertificatesUpdated,
|
||||||
|
selectedCertificateIds,
|
||||||
|
onCertificateSelectionChange,
|
||||||
|
title = "Manage Certificate Sync",
|
||||||
|
subtitle = "Select which certificates should be synced.",
|
||||||
|
saveButtonText = "Save Changes"
|
||||||
|
}: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const isCreateMode = !pkiSync;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedSearchTerm(searchTerm);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
const { data } = useListWorkspaceCertificates({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
limit: pageSize,
|
||||||
|
commonName: debouncedSearchTerm || undefined,
|
||||||
|
friendlyName: debouncedSearchTerm || undefined,
|
||||||
|
forPkiSync: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const allCertificates = data?.certificates || [];
|
||||||
|
const totalCount = data?.totalCount || 0;
|
||||||
|
|
||||||
|
const { data: syncData } = useListPkiSyncCertificates(pkiSync?.id || "");
|
||||||
|
const syncCertificates = syncData?.certificates || [];
|
||||||
|
const addCertificatesToSync = useAddCertificatesToPkiSync();
|
||||||
|
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||||
|
|
||||||
|
const syncedCertificateIds = isCreateMode
|
||||||
|
? selectedCertificateIds || []
|
||||||
|
: syncCertificates.map((sc) => sc.certificateId);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSelectedIds(syncedCertificateIds);
|
||||||
|
}, [JSON.stringify(syncedCertificateIds)]);
|
||||||
|
|
||||||
|
const handleToggleSelection = (certId: string) => {
|
||||||
|
setSelectedIds((prev) =>
|
||||||
|
prev.includes(certId) ? prev.filter((id) => id !== certId) : [...prev, certId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
const currentPageIds = allCertificates.map((cert) => cert.id);
|
||||||
|
const allCurrentPageSelected = currentPageIds.every((id) => selectedIds.includes(id));
|
||||||
|
|
||||||
|
if (allCurrentPageSelected) {
|
||||||
|
setSelectedIds((prev) => prev.filter((id) => !currentPageIds.includes(id)));
|
||||||
|
} else {
|
||||||
|
setSelectedIds((prev) => [...new Set([...prev, ...currentPageIds])]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchTerm("");
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleSaveCertificates = async () => {
|
||||||
|
try {
|
||||||
|
if (isCreateMode) {
|
||||||
|
if (onCertificateSelectionChange) {
|
||||||
|
onCertificateSelectionChange(selectedIds);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkiSync) return;
|
||||||
|
|
||||||
|
const certificatesToAdd = selectedIds.filter((id) => !syncedCertificateIds.includes(id));
|
||||||
|
const certificatesToRemove = syncedCertificateIds.filter((id) => !selectedIds.includes(id));
|
||||||
|
|
||||||
|
const invalidCertificates = certificatesToAdd
|
||||||
|
.map((id) => allCertificates.find((cert) => cert.id === id))
|
||||||
|
.filter((cert) => {
|
||||||
|
if (!cert) return false;
|
||||||
|
const isExpired = new Date(cert.notAfter) < new Date();
|
||||||
|
const isRevoked = cert.status === CertStatus.REVOKED;
|
||||||
|
return isExpired || isRevoked;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invalidCertificates.length > 0) {
|
||||||
|
const invalidNames = invalidCertificates.map((cert) => cert?.commonName).join(", ");
|
||||||
|
createNotification({
|
||||||
|
text: `Cannot add expired or revoked certificates: ${invalidNames}`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operations = [];
|
||||||
|
|
||||||
|
if (certificatesToAdd.length > 0) {
|
||||||
|
operations.push(
|
||||||
|
addCertificatesToSync
|
||||||
|
.mutateAsync({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateIds: certificatesToAdd
|
||||||
|
})
|
||||||
|
.then(() => ({
|
||||||
|
type: "add",
|
||||||
|
count: certificatesToAdd.length,
|
||||||
|
success: true
|
||||||
|
}))
|
||||||
|
.catch((error) => ({
|
||||||
|
type: "add",
|
||||||
|
count: certificatesToAdd.length,
|
||||||
|
success: false,
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificatesToRemove.length > 0) {
|
||||||
|
operations.push(
|
||||||
|
removeCertificatesFromSync
|
||||||
|
.mutateAsync({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateIds: certificatesToRemove
|
||||||
|
})
|
||||||
|
.then(() => ({
|
||||||
|
type: "remove",
|
||||||
|
count: certificatesToRemove.length,
|
||||||
|
success: true
|
||||||
|
}))
|
||||||
|
.catch((error) => ({
|
||||||
|
type: "remove",
|
||||||
|
count: certificatesToRemove.length,
|
||||||
|
success: false,
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operations.length === 0) {
|
||||||
|
createNotification({
|
||||||
|
text: "No changes to save",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(operations);
|
||||||
|
const failures = results.filter((r) => !r.success);
|
||||||
|
const successes = results.filter((r) => r.success);
|
||||||
|
|
||||||
|
if (failures.length === 0) {
|
||||||
|
const addCount = successes.find((r) => r.type === "add")?.count || 0;
|
||||||
|
const removeCount = successes.find((r) => r.type === "remove")?.count || 0;
|
||||||
|
|
||||||
|
let message = "Certificate selection updated successfully";
|
||||||
|
if (addCount > 0 && removeCount > 0) {
|
||||||
|
message = `Added ${addCount} and removed ${removeCount} certificate(s)`;
|
||||||
|
} else if (addCount > 0) {
|
||||||
|
message = `Added ${addCount} certificate(s)`;
|
||||||
|
} else if (removeCount > 0) {
|
||||||
|
message = `Removed ${removeCount} certificate(s)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: message,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onCertificatesUpdated) {
|
||||||
|
onCertificatesUpdated();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
const partialSuccess = successes.length > 0;
|
||||||
|
console.error("Certificate sync operation failures:", failures);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: partialSuccess
|
||||||
|
? "Some certificate changes failed. Check console for details."
|
||||||
|
: "Failed to update certificate selection",
|
||||||
|
type: partialSuccess ? "warning" : "error"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialSuccess && onCertificatesUpdated) {
|
||||||
|
onCertificatesUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unexpected error during certificate sync operation:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "An unexpected error occurred while updating certificates",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = addCertificatesToSync.isPending || removeCertificatesFromSync.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<ModalContent title={title} subTitle={subtitle} className="max-w-4xl">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
placeholder="Search by common name, serial number, or SAN..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSearch}
|
||||||
|
className="absolute top-1/2 left-3 h-3 w-3 -translate-y-1/2 transform text-bunker-300"
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearSearch}
|
||||||
|
className="absolute top-1/2 right-3 -translate-y-1/2 transform text-bunker-300 hover:text-bunker-100"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faX} className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-12">
|
||||||
|
<Checkbox
|
||||||
|
id="select-all-certificates"
|
||||||
|
isChecked={
|
||||||
|
allCertificates.length > 0 &&
|
||||||
|
allCertificates.every((cert) => selectedIds.includes(cert.id))
|
||||||
|
}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
/>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-1/3">SAN / CN</Th>
|
||||||
|
<Th className="w-1/4">Serial Number</Th>
|
||||||
|
<Th className="w-1/6">Issued At</Th>
|
||||||
|
<Th className="w-1/6">Expires At</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{allCertificates.map((cert) => {
|
||||||
|
const isExpired = new Date(cert.notAfter) < new Date();
|
||||||
|
const isRevoked = cert.status === CertStatus.REVOKED;
|
||||||
|
const cannotBeAdded = isExpired || isRevoked;
|
||||||
|
const isAlreadySynced = syncedCertificateIds.includes(cert.id);
|
||||||
|
|
||||||
|
let originalDisplayName = "—";
|
||||||
|
if (cert.altNames && cert.altNames.trim()) {
|
||||||
|
originalDisplayName = cert.altNames.trim();
|
||||||
|
} else if (cert.commonName && cert.commonName.trim()) {
|
||||||
|
originalDisplayName = cert.commonName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayName = originalDisplayName;
|
||||||
|
let isTruncated = false;
|
||||||
|
if (originalDisplayName.length > 34) {
|
||||||
|
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||||
|
isTruncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncatedSerial =
|
||||||
|
cert.serialNumber.length > 8
|
||||||
|
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||||
|
: cert.serialNumber;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
key={cert.id}
|
||||||
|
className={`cursor-pointer hover:bg-mineshaft-700 ${
|
||||||
|
cannotBeAdded && !isAlreadySynced ? "opacity-50" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!cannotBeAdded || isAlreadySynced) {
|
||||||
|
handleToggleSelection(cert.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Td className="max-w-0" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Checkbox
|
||||||
|
id={cert.id}
|
||||||
|
isChecked={selectedIds.includes(cert.id)}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
if (!cannotBeAdded || isAlreadySynced) {
|
||||||
|
handleToggleSelection(cert.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDisabled={cannotBeAdded && !isAlreadySynced}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
{isTruncated ? (
|
||||||
|
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||||
|
<div className="truncate">{displayName}</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div className="truncate">{displayName}</div>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<div
|
||||||
|
className="font-mono text-xs text-bunker-300"
|
||||||
|
title={cert.serialNumber}
|
||||||
|
>
|
||||||
|
{truncatedSerial}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<span className="text-sm text-bunker-300">
|
||||||
|
{new Date(cert.notBefore).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<span
|
||||||
|
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||||
|
>
|
||||||
|
{new Date(cert.notAfter).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{allCertificates.length === 0 && (
|
||||||
|
<EmptyState title="No certificates found">
|
||||||
|
{searchTerm
|
||||||
|
? "No certificates match your search criteria."
|
||||||
|
: "No certificates available for sync."}
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<Pagination
|
||||||
|
count={totalCount}
|
||||||
|
page={currentPage}
|
||||||
|
perPage={pageSize}
|
||||||
|
onChangePage={(page: number) => setCurrentPage(page)}
|
||||||
|
onChangePerPage={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-2">
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
colorSchema="primary"
|
||||||
|
onClick={handleSaveCertificates}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{saveButtonText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,21 +11,24 @@ type Props = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
selectSync?: PkiSync | null;
|
selectSync?: PkiSync | null;
|
||||||
|
initialData?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContentProps = {
|
type ContentProps = {
|
||||||
onComplete: (pkiSync: TPkiSync) => void;
|
onComplete: (pkiSync: TPkiSync) => void;
|
||||||
selectedSync: PkiSync | null;
|
selectedSync: PkiSync | null;
|
||||||
setSelectedSync: (selectedSync: PkiSync | null) => void;
|
setSelectedSync: (selectedSync: PkiSync | null) => void;
|
||||||
|
initialData?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
|
const Content = ({ onComplete, setSelectedSync, selectedSync, initialData }: ContentProps) => {
|
||||||
if (selectedSync) {
|
if (selectedSync) {
|
||||||
return (
|
return (
|
||||||
<CreatePkiSyncForm
|
<CreatePkiSyncForm
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
onCancel={() => setSelectedSync(null)}
|
onCancel={() => setSelectedSync(null)}
|
||||||
destination={selectedSync}
|
destination={selectedSync}
|
||||||
|
initialData={initialData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -33,7 +36,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
|
|||||||
return <PkiSyncSelect onSelect={setSelectedSync} />;
|
return <PkiSyncSelect onSelect={setSelectedSync} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
|
export const CreatePkiSyncModal = ({
|
||||||
|
onOpenChange,
|
||||||
|
selectSync = null,
|
||||||
|
initialData,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
const [selectedSync, setSelectedSync] = useState<PkiSync | null>(selectSync);
|
const [selectedSync, setSelectedSync] = useState<PkiSync | null>(selectSync);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -69,6 +77,7 @@ export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }
|
|||||||
}}
|
}}
|
||||||
selectedSync={selectedSync}
|
selectedSync={selectedSync}
|
||||||
setSelectedSync={setSelectedSync}
|
setSelectedSync={setSelectedSync}
|
||||||
|
initialData={initialData}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -13,27 +13,28 @@ import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
|||||||
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||||
|
|
||||||
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
|
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||||
|
import { PkiSyncCertificatesFields } from "./PkiSyncCertificatesFields";
|
||||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||||
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onComplete: (pkiSync: TPkiSync) => void;
|
onComplete: (pkiSync: TPkiSync) => void;
|
||||||
destination: PkiSync;
|
destination: PkiSync;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
initialData?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
|
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
|
||||||
{ name: "Source", key: "source", fields: ["subscriberId"] },
|
|
||||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||||
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||||
|
{ name: "Certificates", key: "certificates", fields: ["certificateIds"] },
|
||||||
{ name: "Review", key: "review", fields: [] }
|
{ name: "Review", key: "review", fields: [] }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => {
|
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => {
|
||||||
const createPkiSync = useCreatePkiSync();
|
const createPkiSync = useCreatePkiSync();
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { name: destinationName } = PKI_SYNC_MAP[destination];
|
const { name: destinationName } = PKI_SYNC_MAP[destination];
|
||||||
@@ -49,34 +50,47 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
destination,
|
destination,
|
||||||
isAutoSyncEnabled: false,
|
isAutoSyncEnabled: false,
|
||||||
|
certificateIds: [],
|
||||||
syncOptions: {
|
syncOptions: {
|
||||||
canImportCertificates: false,
|
canImportCertificates: false,
|
||||||
canRemoveCertificates: false,
|
canRemoveCertificates: false,
|
||||||
|
preserveArn: true,
|
||||||
certificateNameSchema: syncOption?.defaultCertificateNameSchema
|
certificateNameSchema: syncOption?.defaultCertificateNameSchema
|
||||||
}
|
},
|
||||||
|
...initialData
|
||||||
} as Partial<TPkiSyncForm>,
|
} as Partial<TPkiSyncForm>,
|
||||||
reValidateMode: "onChange"
|
reValidateMode: "onChange"
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async ({ connection, destinationConfig, ...formData }: TPkiSyncForm) => {
|
const onSubmit = async ({
|
||||||
|
connection,
|
||||||
|
destinationConfig,
|
||||||
|
certificateIds,
|
||||||
|
...formData
|
||||||
|
}: TPkiSyncForm) => {
|
||||||
try {
|
try {
|
||||||
const pkiSync = await createPkiSync.mutateAsync({
|
const pkiSync = await createPkiSync.mutateAsync({
|
||||||
...formData,
|
...formData,
|
||||||
connectionId: connection.id,
|
connectionId: connection.id,
|
||||||
projectId: currentProject.id,
|
projectId: currentProject.id,
|
||||||
destinationConfig
|
destinationConfig,
|
||||||
|
certificateIds: certificateIds || []
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Successfully added ${destinationName} Certificate Sync`,
|
text: `Successfully created ${destinationName} Certificate Sync${
|
||||||
|
certificateIds && certificateIds.length > 0
|
||||||
|
? ` with ${certificateIds.length} certificate(s)`
|
||||||
|
: ""
|
||||||
|
}`,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
onComplete(pkiSync);
|
onComplete(pkiSync);
|
||||||
} catch (err: Error | unknown) {
|
} catch (err: Error | unknown) {
|
||||||
console.error(err);
|
console.error("PKI sync creation failed:", err);
|
||||||
setShowConfirmation(false);
|
setShowConfirmation(false);
|
||||||
createNotification({
|
createNotification({
|
||||||
title: `Failed to add ${destinationName} Certificate Sync`,
|
title: `Failed to create ${destinationName} Certificate Sync`,
|
||||||
text: err instanceof Error ? err.message : "An unknown error occurred",
|
text: err instanceof Error ? err.message : "An unknown error occurred",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
@@ -184,9 +198,6 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
|||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel>
|
|
||||||
<PkiSyncSourceFields />
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PkiSyncDestinationFields />
|
<PkiSyncDestinationFields />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
@@ -200,8 +211,8 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
|||||||
<FormControl
|
<FormControl
|
||||||
helperText={
|
helperText={
|
||||||
value
|
value
|
||||||
? "Certificates will automatically be synced when changes occur in the source subscriber."
|
? "Certificates will automatically be synced when changes occur in the selected certificates."
|
||||||
: "Certificates will not automatically be synced when changes occur in the source subscriber. You can still trigger syncs manually."
|
: "Certificates will not automatically be synced when changes occur. You can still trigger syncs manually."
|
||||||
}
|
}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
@@ -223,6 +234,9 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PkiSyncDetailsFields />
|
<PkiSyncDetailsFields />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<PkiSyncCertificatesFields />
|
||||||
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<PkiSyncReviewFields />
|
<PkiSyncReviewFields />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|||||||
@@ -27,12 +27,16 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
|
|||||||
const formMethods = useForm<TUpdatePkiSyncForm>({
|
const formMethods = useForm<TUpdatePkiSyncForm>({
|
||||||
resolver: zodResolver(UpdatePkiSyncFormSchema),
|
resolver: zodResolver(UpdatePkiSyncFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...pkiSync,
|
name: pkiSync.name,
|
||||||
|
destination: pkiSync.destination,
|
||||||
description: pkiSync.description ?? "",
|
description: pkiSync.description ?? "",
|
||||||
connection: {
|
connection: {
|
||||||
id: pkiSync.connectionId,
|
id: pkiSync.connectionId,
|
||||||
name: pkiSync.appConnectionName
|
name: pkiSync.appConnectionName
|
||||||
}
|
},
|
||||||
|
syncOptions: pkiSync.syncOptions,
|
||||||
|
destinationConfig: pkiSync.destinationConfig,
|
||||||
|
isAutoSyncEnabled: pkiSync.isAutoSyncEnabled
|
||||||
} as Partial<TUpdatePkiSyncForm>,
|
} as Partial<TUpdatePkiSyncForm>,
|
||||||
reValidateMode: "onChange"
|
reValidateMode: "onChange"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { faCertificate, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import { CertStatus } from "@app/hooks/api";
|
||||||
|
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||||
|
|
||||||
|
import { CertificateManagementModal } from "../CertificateManagementModal";
|
||||||
|
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||||
|
|
||||||
|
export const PkiSyncCertificatesFields = () => {
|
||||||
|
const { control, watch, setValue } = useFormContext<TPkiSyncForm>();
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const certificateIds = watch("certificateIds") || [];
|
||||||
|
|
||||||
|
const { data, isLoading } = useListWorkspaceCertificates({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
offset: 0,
|
||||||
|
limit: 100,
|
||||||
|
forPkiSync: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificates = data?.certificates || [];
|
||||||
|
|
||||||
|
const activeCertificates = useMemo(
|
||||||
|
() => certificates.filter((cert) => cert.status === CertStatus.ACTIVE),
|
||||||
|
[certificates]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCertificates = useMemo(
|
||||||
|
() => activeCertificates.filter((cert) => certificateIds.includes(cert.id)),
|
||||||
|
[activeCertificates, certificateIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="text-sm text-bunker-300">Loading certificates...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="mb-4 text-sm text-bunker-300">
|
||||||
|
Select certificates to sync with this integration. Only active certificates can be synced.
|
||||||
|
You can modify this selection after creating the sync.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="certificateIds"
|
||||||
|
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
onClick={() => setIsSelectionModalOpen(true)}
|
||||||
|
>
|
||||||
|
Add Certificates
|
||||||
|
</Button>
|
||||||
|
<div className="max-h-64 overflow-y-auto">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-1/3">SAN / CN</Th>
|
||||||
|
<Th className="w-1/4">Serial Number</Th>
|
||||||
|
<Th className="w-1/6">Issued At</Th>
|
||||||
|
<Th className="w-1/6">Expires At</Th>
|
||||||
|
<Th className="w-12">Remove</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{selectedCertificates.map((cert) => {
|
||||||
|
let originalDisplayName = "—";
|
||||||
|
if (cert.altNames && cert.altNames.trim()) {
|
||||||
|
originalDisplayName = cert.altNames.trim();
|
||||||
|
} else if (cert.commonName && cert.commonName.trim()) {
|
||||||
|
originalDisplayName = cert.commonName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayName = originalDisplayName;
|
||||||
|
let isTruncated = false;
|
||||||
|
if (originalDisplayName.length > 34) {
|
||||||
|
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||||
|
isTruncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncatedSerial =
|
||||||
|
cert.serialNumber.length > 8
|
||||||
|
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||||
|
: cert.serialNumber;
|
||||||
|
|
||||||
|
const isExpired = new Date(cert.notAfter) < new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={cert.id}>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
{isTruncated ? (
|
||||||
|
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||||
|
<div className="truncate">{displayName}</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div className="truncate">{displayName}</div>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<div
|
||||||
|
className="font-mono text-xs text-bunker-300"
|
||||||
|
title={cert.serialNumber}
|
||||||
|
>
|
||||||
|
{truncatedSerial}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<span className="text-sm text-bunker-300">
|
||||||
|
{new Date(cert.notBefore).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<span
|
||||||
|
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||||
|
>
|
||||||
|
{new Date(cert.notAfter).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="pl-5"
|
||||||
|
aria-label="Remove certificate"
|
||||||
|
onClick={() => {
|
||||||
|
const newIds = value.filter((id: string) => id !== cert.id);
|
||||||
|
onChange(newIds);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{selectedCertificates.length === 0 && (
|
||||||
|
<EmptyState title="No certificates selected" icon={faCertificate} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CertificateManagementModal
|
||||||
|
isOpen={isSelectionModalOpen}
|
||||||
|
onClose={() => setIsSelectionModalOpen(false)}
|
||||||
|
selectedCertificateIds={certificateIds}
|
||||||
|
onCertificateSelectionChange={(newCertificateIds) => {
|
||||||
|
setValue("certificateIds", newCertificateIds);
|
||||||
|
}}
|
||||||
|
title="Select Certificates for Sync"
|
||||||
|
subtitle="Choose which certificates you want to include in this sync. You can modify this selection after creating the sync."
|
||||||
|
saveButtonText="Update Selection"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { SingleValue } from "react-select";
|
||||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { useRouterState } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { AppConnectionOption } from "@app/components/app-connections";
|
||||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
|
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
|
||||||
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
|
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||||
|
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
|
||||||
|
|
||||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||||
|
|
||||||
@@ -18,12 +22,30 @@ type Props = {
|
|||||||
|
|
||||||
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { control, watch } = useFormContext<TPkiSyncForm>();
|
const { control, watch, setValue } = useFormContext<TPkiSyncForm>();
|
||||||
const { currentProject } = useProject();
|
|
||||||
|
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
|
||||||
|
|
||||||
const destination = watch("destination");
|
const destination = watch("destination");
|
||||||
const app = PKI_SYNC_CONNECTION_MAP[destination];
|
const app = PKI_SYNC_CONNECTION_MAP[destination];
|
||||||
|
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
|
const {
|
||||||
|
location: { pathname }
|
||||||
|
} = useRouterState();
|
||||||
|
|
||||||
|
const getPkiSyncReturnUrl = () => {
|
||||||
|
if (pathname.includes("selectedTab=secret-syncs")) {
|
||||||
|
return pathname.replace("selectedTab=secret-syncs", "selectedTab=pki-syncs");
|
||||||
|
}
|
||||||
|
if (!pathname.includes("selectedTab=")) {
|
||||||
|
const separator = pathname.includes("?") ? "&" : "?";
|
||||||
|
return `${pathname}${separator}selectedTab=pki-syncs`;
|
||||||
|
}
|
||||||
|
return pathname;
|
||||||
|
};
|
||||||
|
|
||||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
||||||
app,
|
app,
|
||||||
currentProject.id
|
currentProject.id
|
||||||
@@ -47,6 +69,7 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
|
tooltipText="App Connections can be created from the Project Settings page."
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
label={`${connectionName} Connection`}
|
label={`${connectionName} Connection`}
|
||||||
@@ -54,36 +77,54 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
|||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
|
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
|
||||||
|
handlePopUpOpen("addConnection");
|
||||||
|
onChange(null);
|
||||||
|
const formData = { ...watch(), returnUrl: getPkiSyncReturnUrl() };
|
||||||
|
localStorage.setItem("pkiSyncFormData", JSON.stringify(formData));
|
||||||
|
if (callback) callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}}
|
}}
|
||||||
isLoading={isPending}
|
isLoading={isPending}
|
||||||
options={availableConnections}
|
options={[
|
||||||
|
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
|
||||||
|
...(availableConnections ?? [])
|
||||||
|
]}
|
||||||
placeholder="Select connection..."
|
placeholder="Select connection..."
|
||||||
getOptionLabel={(option) => option.name}
|
getOptionLabel={(option) => option.name}
|
||||||
getOptionValue={(option) => option.id}
|
getOptionValue={(option) => option.id}
|
||||||
|
components={{ Option: AppConnectionOption }}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
name="connection"
|
name="connection"
|
||||||
/>
|
/>
|
||||||
{availableConnections?.length === 0 && (
|
{!isPending && !availableConnections?.length && !canCreateConnection && (
|
||||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
||||||
{canCreateConnection ? (
|
You do not have access to any {appName} Connections. Contact an admin to create one.
|
||||||
<>
|
|
||||||
You do not have access to any {appName} Connections. Create one from the{" "}
|
|
||||||
<Link to="/organization/app-connections" className="underline">
|
|
||||||
App Connections
|
|
||||||
</Link>{" "}
|
|
||||||
page.
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<AddAppConnectionModal
|
||||||
|
isOpen={popUp.addConnection.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
localStorage.removeItem("pkiSyncFormData");
|
||||||
|
handlePopUpToggle("addConnection", isOpen);
|
||||||
|
}}
|
||||||
|
projectType={currentProject.type}
|
||||||
|
projectId={currentProject.id}
|
||||||
|
app={app}
|
||||||
|
onComplete={(connection) => {
|
||||||
|
if (connection) {
|
||||||
|
setValue("connection", connection);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+90
-2
@@ -71,14 +71,14 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
|||||||
isChecked={value}
|
isChecked={value}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Enable Certificate Removal{" "}
|
Enable Removal of Active/Revoked Certificates{" "}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="max-w-md"
|
className="max-w-md"
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
When enabled, Infisical will remove certificates from the destination during
|
When enabled, Infisical will remove certificates from the destination during
|
||||||
a sync if they are no longer managed by Infisical.
|
a sync if they are no longer active in Infisical.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
Disable this option if you intend to manage some certificates manually
|
Disable this option if you intend to manage some certificates manually
|
||||||
@@ -95,6 +95,94 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{currentDestination === PkiSync.AwsCertificateManager && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="syncOptions.preserveArn"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||||
|
id="preserve-arn"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Preserve ARN on Renewal{" "}
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
When enabled, Infisical will replace the contents of existing certificates
|
||||||
|
while preserving the same ARN during certificate renewal syncs.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
This allows consuming services like load balancers to continue using the
|
||||||
|
same ARN without requiring manual updates.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
When disabled, new certificates will be created with new ARNs, and old
|
||||||
|
certificates will be removed.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||||
|
</Tooltip>
|
||||||
|
</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentDestination === PkiSync.AzureKeyVault && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="syncOptions.enableVersioning"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Switch
|
||||||
|
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||||
|
id="preserve-version"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Preserve Version on Renewal{" "}
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-md"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
When enabled, Infisical will create a new version of the existing
|
||||||
|
certificate in Azure Key Vault during certificate renewal syncs,
|
||||||
|
preserving the original certificate name.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
This allows consuming services to continue using the same certificate name
|
||||||
|
while automatically using the latest version without requiring manual
|
||||||
|
updates.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4">
|
||||||
|
When disabled, new certificates will be created with new names, and old
|
||||||
|
certificates will be removed.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||||
|
</Tooltip>
|
||||||
|
</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="syncOptions.certificateNameSchema"
|
name="syncOptions.certificateNameSchema"
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { GenericFieldLabel } from "@app/components/v2";
|
import {
|
||||||
|
GenericFieldLabel,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v3";
|
import { Badge } from "@app/components/v3";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||||
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
|
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||||
|
|
||||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||||
|
|
||||||
@@ -12,18 +22,24 @@ export const PkiSyncReviewFields = () => {
|
|||||||
const { watch } = useFormContext<TPkiSyncForm>();
|
const { watch } = useFormContext<TPkiSyncForm>();
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
const { data: pkiSubscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
const { data } = useListWorkspaceCertificates({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
offset: 0,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
|
||||||
const getSubscriberName = (subscriberId?: string) => {
|
const certificates = data?.certificates || [];
|
||||||
const subscriber = pkiSubscribers.find((sub) => sub.id === subscriberId);
|
|
||||||
return subscriber?.name || "Unknown";
|
const getSelectedCertificates = (certificateIds?: string[]) => {
|
||||||
|
if (!certificateIds || certificateIds.length === 0) return [];
|
||||||
|
return certificates.filter((cert) => certificateIds.includes(cert.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
connection,
|
connection,
|
||||||
subscriberId,
|
certificateIds,
|
||||||
syncOptions,
|
syncOptions,
|
||||||
destination,
|
destination,
|
||||||
destinationConfig,
|
destinationConfig,
|
||||||
@@ -31,17 +47,79 @@ export const PkiSyncReviewFields = () => {
|
|||||||
} = watch();
|
} = watch();
|
||||||
|
|
||||||
const destinationName = PKI_SYNC_MAP[destination].name;
|
const destinationName = PKI_SYNC_MAP[destination].name;
|
||||||
|
const selectedCertificates = getSelectedCertificates(certificateIds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 flex flex-col gap-6">
|
<div className="mb-4 flex flex-col gap-6">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="w-full border-b border-mineshaft-600">
|
<div className="w-full border-b border-mineshaft-600">
|
||||||
<span className="text-sm text-mineshaft-300">Source</span>
|
<span className="text-sm text-mineshaft-300">Certificates</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
<div className="w-full">
|
||||||
<GenericFieldLabel label="PKI Subscriber">
|
{selectedCertificates.length === 0 ? (
|
||||||
{getSubscriberName(subscriberId)}
|
<span className="text-bunker-400">No certificates selected</span>
|
||||||
</GenericFieldLabel>
|
) : (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-1/2">SAN / CN</Th>
|
||||||
|
<Th className="w-1/4">Serial Number</Th>
|
||||||
|
<Th className="w-1/4">Expires At</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{selectedCertificates.map((cert) => {
|
||||||
|
let originalDisplayName = "—";
|
||||||
|
if (cert.altNames && cert.altNames.trim()) {
|
||||||
|
originalDisplayName = cert.altNames.trim();
|
||||||
|
} else if (cert.commonName && cert.commonName.trim()) {
|
||||||
|
originalDisplayName = cert.commonName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let displayName = originalDisplayName;
|
||||||
|
let isTruncated = false;
|
||||||
|
if (originalDisplayName.length > 34) {
|
||||||
|
displayName = `${originalDisplayName.substring(0, 34)}...`;
|
||||||
|
isTruncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncatedSerial =
|
||||||
|
cert.serialNumber.length > 8
|
||||||
|
? `${cert.serialNumber.slice(0, 4)}...${cert.serialNumber.slice(-4)}`
|
||||||
|
: cert.serialNumber;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={cert.id}>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
{isTruncated ? (
|
||||||
|
<Tooltip content={originalDisplayName} className="max-w-lg">
|
||||||
|
<div className="truncate">{displayName}</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div className="truncate">{displayName}</div>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<div
|
||||||
|
className="font-mono text-xs text-bunker-300"
|
||||||
|
title={cert.serialNumber}
|
||||||
|
>
|
||||||
|
{truncatedSerial}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<span className="text-sm text-bunker-300">
|
||||||
|
{new Date(cert.notAfter).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -62,11 +140,13 @@ export const PkiSyncReviewFields = () => {
|
|||||||
<div className="w-full border-b border-mineshaft-600">
|
<div className="w-full border-b border-mineshaft-600">
|
||||||
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
<span className="text-sm text-mineshaft-300">Sync Options</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
<div className="flex flex-wrap gap-x-8 gap-y-3">
|
||||||
<GenericFieldLabel label="Auto-Sync">
|
<GenericFieldLabel label="Auto-Sync">
|
||||||
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
<div className="mt-1">
|
||||||
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
<Badge variant={isAutoSyncEnabled ? "success" : "danger"}>
|
||||||
</Badge>
|
{isAutoSyncEnabled ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
{/* Hidden for now - Import certificates functionality disabled
|
{/* Hidden for now - Import certificates functionality disabled
|
||||||
{syncOptions?.canImportCertificates !== undefined && (
|
{syncOptions?.canImportCertificates !== undefined && (
|
||||||
@@ -79,9 +159,11 @@ export const PkiSyncReviewFields = () => {
|
|||||||
*/}
|
*/}
|
||||||
{syncOptions?.canRemoveCertificates !== undefined && (
|
{syncOptions?.canRemoveCertificates !== undefined && (
|
||||||
<GenericFieldLabel label="Remove Certificates">
|
<GenericFieldLabel label="Remove Certificates">
|
||||||
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
<div className="mt-1">
|
||||||
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
<Badge variant={syncOptions.canRemoveCertificates ? "success" : "danger"}>
|
||||||
</Badge>
|
{syncOptions.canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
@@ -7,6 +7,7 @@ import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
|||||||
const AwsCertificateManagerSyncOptionsSchema = z.object({
|
const AwsCertificateManagerSyncOptionsSchema = z.object({
|
||||||
canImportCertificates: z.boolean().default(false),
|
canImportCertificates: z.boolean().default(false),
|
||||||
canRemoveCertificates: z.boolean().default(false),
|
canRemoveCertificates: z.boolean().default(false),
|
||||||
|
preserveArn: z.boolean().default(true),
|
||||||
certificateNameSchema: z
|
certificateNameSchema: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
+40
-1
@@ -4,7 +4,46 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
|||||||
|
|
||||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||||
|
|
||||||
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema().merge(
|
const AzureKeyVaultSyncOptionsSchema = z.object({
|
||||||
|
canImportCertificates: z.boolean().default(false),
|
||||||
|
canRemoveCertificates: z.boolean().default(true),
|
||||||
|
enableVersioning: z.boolean().default(true),
|
||||||
|
certificateNameSchema: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
if (!val) return true;
|
||||||
|
|
||||||
|
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||||
|
|
||||||
|
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
|
||||||
|
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||||
|
.join("|");
|
||||||
|
|
||||||
|
const allowedContentRegex = new RegExp(
|
||||||
|
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
|
||||||
|
);
|
||||||
|
const contentIsValid = allowedContentRegex.test(val);
|
||||||
|
|
||||||
|
if (val.trim()) {
|
||||||
|
const certificateIdRegex = /\{\{certificateId\}\}/;
|
||||||
|
const certificateIdIsPresent = certificateIdRegex.test(val);
|
||||||
|
return contentIsValid && certificateIdIsPresent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentIsValid;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema(
|
||||||
|
AzureKeyVaultSyncOptionsSchema
|
||||||
|
).merge(
|
||||||
z.object({
|
z.object({
|
||||||
destination: z.literal(PkiSync.AzureKeyVault),
|
destination: z.literal(PkiSync.AzureKeyVault),
|
||||||
destinationConfig: z.object({
|
destinationConfig: z.object({
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export const BasePkiSyncSchema = <T extends AnyZodObject | undefined = undefined
|
|||||||
.max(255, "Name must be less than 255 characters"),
|
.max(255, "Name must be less than 255 characters"),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
isAutoSyncEnabled: z.boolean().default(true),
|
isAutoSyncEnabled: z.boolean().default(true),
|
||||||
subscriberId: z.string().min(1, "PKI Subscriber is required"),
|
subscriberId: z.string().nullable().optional(),
|
||||||
|
certificateIds: z.array(z.string()).optional(),
|
||||||
connection: z.object({
|
connection: z.object({
|
||||||
id: z.string().uuid("Invalid connection ID format"),
|
id: z.string().uuid("Invalid connection ID format"),
|
||||||
name: z.string().max(255, "Connection name must be less than 255 characters")
|
name: z.string().max(255, "Connection name must be less than 255 characters")
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Tooltip } from "@app/components/v2";
|
||||||
|
|
||||||
|
interface CertificateNameData {
|
||||||
|
altNames?: string | null;
|
||||||
|
commonName?: string | null;
|
||||||
|
certificateAltNames?: string | null;
|
||||||
|
certificateCommonName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisplayNameResult {
|
||||||
|
originalDisplayName: string;
|
||||||
|
displayName: string;
|
||||||
|
isTruncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and formats the display name for a certificate from SAN/CN data
|
||||||
|
* @param cert - Certificate object with potential altNames/commonName fields
|
||||||
|
* @param maxLength - Maximum length before truncating (default: 64)
|
||||||
|
* @param fallback - Fallback text when no name is found (default: "—")
|
||||||
|
* @returns Object with original name, truncated name, and truncation flag
|
||||||
|
*/
|
||||||
|
export const getCertificateDisplayName = (
|
||||||
|
cert: CertificateNameData,
|
||||||
|
maxLength: number = 64,
|
||||||
|
fallback: string = "—"
|
||||||
|
): DisplayNameResult => {
|
||||||
|
// Extract original display name - prioritize SAN over CN
|
||||||
|
let originalDisplayName = fallback;
|
||||||
|
|
||||||
|
// Handle different property name variations
|
||||||
|
const altNames = cert.altNames || cert.certificateAltNames;
|
||||||
|
const commonName = cert.commonName || cert.certificateCommonName;
|
||||||
|
|
||||||
|
if (altNames && altNames.trim()) {
|
||||||
|
originalDisplayName = altNames.trim();
|
||||||
|
} else if (commonName && commonName.trim()) {
|
||||||
|
originalDisplayName = commonName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle truncation
|
||||||
|
let displayName = originalDisplayName;
|
||||||
|
let isTruncated = false;
|
||||||
|
|
||||||
|
if (originalDisplayName.length > maxLength) {
|
||||||
|
displayName = `${originalDisplayName.substring(0, maxLength)}...`;
|
||||||
|
isTruncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
originalDisplayName,
|
||||||
|
displayName,
|
||||||
|
isTruncated
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a certificate display name with optional tooltip for truncated names
|
||||||
|
* @param cert - Certificate object with potential altNames/commonName fields
|
||||||
|
* @param maxLength - Maximum length before truncating (default: 64)
|
||||||
|
* @param fallback - Fallback text when no name is found (default: "—")
|
||||||
|
* @param className - Optional CSS class for the display element
|
||||||
|
* @param tooltipClassName - Optional CSS class for the tooltip (default: "max-w-lg")
|
||||||
|
* @returns JSX element with certificate name and optional tooltip
|
||||||
|
*/
|
||||||
|
export const CertificateDisplayName = ({
|
||||||
|
cert,
|
||||||
|
maxLength = 64,
|
||||||
|
fallback = "—",
|
||||||
|
className = "truncate",
|
||||||
|
tooltipClassName = "max-w-lg"
|
||||||
|
}: {
|
||||||
|
cert: CertificateNameData;
|
||||||
|
maxLength?: number;
|
||||||
|
fallback?: string;
|
||||||
|
className?: string;
|
||||||
|
tooltipClassName?: string;
|
||||||
|
}): ReactNode => {
|
||||||
|
const { originalDisplayName, displayName, isTruncated } = getCertificateDisplayName(
|
||||||
|
cert,
|
||||||
|
maxLength,
|
||||||
|
fallback
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTruncated) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={originalDisplayName} className={tooltipClassName}>
|
||||||
|
<div className={className}>{displayName}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} title={originalDisplayName}>
|
||||||
|
{displayName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -301,10 +301,6 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/projects/cert-management/$projectId/subscribers",
|
"/projects/cert-management/$projectId/subscribers",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
||||||
),
|
),
|
||||||
CertificatesPage: setRoute(
|
|
||||||
"/projects/cert-management/$projectId/certificates",
|
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
|
|
||||||
),
|
|
||||||
CertificateAuthoritiesPage: setRoute(
|
CertificateAuthoritiesPage: setRoute(
|
||||||
"/projects/cert-management/$projectId/certificate-authorities",
|
"/projects/cert-management/$projectId/certificate-authorities",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export const useCreateCertificate = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateCertificateV3 = () => {
|
export const useCreateCertificateV3 = (options?: { projectId?: string }) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TCreateCertificateV3Response, object, TCreateCertificateV3DTO>({
|
return useMutation<TCreateCertificateV3Response, object, TCreateCertificateV3DTO>({
|
||||||
mutationFn: async (body) => {
|
mutationFn: async (body) => {
|
||||||
@@ -167,6 +167,12 @@ export const useCreateCertificateV3 = () => {
|
|||||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options?.projectId) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectKeys.forProjectCertificates(options.projectId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["certificate-profiles"]
|
queryKey: ["certificate-profiles"]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export {
|
|||||||
useGetCertificateProfileById,
|
useGetCertificateProfileById,
|
||||||
useGetCertificateProfileBySlug,
|
useGetCertificateProfileBySlug,
|
||||||
useGetProfileCertificates,
|
useGetProfileCertificates,
|
||||||
useGetProfileMetrics,
|
|
||||||
useListCertificateProfiles
|
useListCertificateProfiles
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
export type * from "./types";
|
export type * from "./types";
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileMetrics,
|
|
||||||
TCertificateProfileWithDetails,
|
TCertificateProfileWithDetails,
|
||||||
TGetCertificateProfileByIdDTO,
|
TGetCertificateProfileByIdDTO,
|
||||||
TGetCertificateProfileBySlugDTO,
|
TGetCertificateProfileBySlugDTO,
|
||||||
@@ -20,7 +19,6 @@ export const certificateProfileKeys = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
includeMetrics?: boolean;
|
|
||||||
includeConfigs?: boolean;
|
includeConfigs?: boolean;
|
||||||
enrollmentType?: string;
|
enrollmentType?: string;
|
||||||
expiringDays?: number;
|
expiringDays?: number;
|
||||||
@@ -51,10 +49,8 @@ export const useListCertificateProfiles = ({
|
|||||||
limit = 20,
|
limit = 20,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
search,
|
search,
|
||||||
includeMetrics = false,
|
|
||||||
includeConfigs = false,
|
includeConfigs = false,
|
||||||
enrollmentType,
|
enrollmentType
|
||||||
expiringDays = 7
|
|
||||||
}: TListCertificateProfilesDTO) => {
|
}: TListCertificateProfilesDTO) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: certificateProfileKeys.list({
|
queryKey: certificateProfileKeys.list({
|
||||||
@@ -62,10 +58,8 @@ export const useListCertificateProfiles = ({
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
search,
|
search,
|
||||||
includeMetrics,
|
|
||||||
includeConfigs,
|
includeConfigs,
|
||||||
enrollmentType,
|
enrollmentType
|
||||||
expiringDays
|
|
||||||
}),
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{
|
||||||
@@ -77,10 +71,8 @@ export const useListCertificateProfiles = ({
|
|||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
search,
|
search,
|
||||||
includeMetrics,
|
|
||||||
includeConfigs,
|
includeConfigs,
|
||||||
enrollmentType,
|
enrollmentType
|
||||||
expiringDays
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -145,18 +137,3 @@ export const useGetProfileCertificates = ({
|
|||||||
enabled: Boolean(profileId)
|
enabled: Boolean(profileId)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProfileMetrics = ({ profileId, expiringDays = 7 }: TGetProfileMetricsDTO) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: certificateProfileKeys.getMetrics(profileId, { expiringDays }),
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await apiRequest.get<{
|
|
||||||
metrics: TCertificateProfileMetrics;
|
|
||||||
}>(`/api/v1/pki/certificate-profiles/${profileId}/metrics`, {
|
|
||||||
params: { expiringDays }
|
|
||||||
});
|
|
||||||
return data.metrics;
|
|
||||||
},
|
|
||||||
enabled: Boolean(profileId)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export type TCertificateProfile = {
|
|||||||
apiConfigId?: string;
|
apiConfigId?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
metrics?: TCertificateProfileMetrics;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||||
@@ -81,10 +80,8 @@ export type TListCertificateProfilesDTO = {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
includeMetrics?: boolean;
|
|
||||||
includeConfigs?: boolean;
|
includeConfigs?: boolean;
|
||||||
enrollmentType?: "api" | "est";
|
enrollmentType?: "api" | "est";
|
||||||
expiringDays?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetCertificateProfileByIdDTO = {
|
export type TGetCertificateProfileByIdDTO = {
|
||||||
@@ -96,15 +93,6 @@ export type TGetCertificateProfileBySlugDTO = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileMetrics = {
|
|
||||||
profileId: string;
|
|
||||||
totalCertificates: number;
|
|
||||||
activeCertificates: number;
|
|
||||||
expiredCertificates: number;
|
|
||||||
expiringCertificates: number;
|
|
||||||
revokedCertificates: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TProfileCertificate = {
|
export type TProfileCertificate = {
|
||||||
id: string;
|
id: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
@@ -126,5 +114,4 @@ export type TGetProfileCertificatesDTO = {
|
|||||||
|
|
||||||
export type TGetProfileMetricsDTO = {
|
export type TGetProfileMetricsDTO = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
expiringDays?: number;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { CertStatus } from "./enums";
|
||||||
export {
|
export {
|
||||||
useDeleteCert,
|
useDeleteCert,
|
||||||
useImportCertificate,
|
useImportCertificate,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type TCertificate = {
|
|||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
subjectAltNames: string;
|
subjectAltNames: string;
|
||||||
|
altNames?: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
notBefore: string;
|
notBefore: string;
|
||||||
notAfter: string;
|
notAfter: string;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export * from "./organization";
|
|||||||
export * from "./pkiAlerts";
|
export * from "./pkiAlerts";
|
||||||
export * from "./pkiCollections";
|
export * from "./pkiCollections";
|
||||||
export * from "./pkiSubscriber";
|
export * from "./pkiSubscriber";
|
||||||
|
export * from "./pkiSyncs";
|
||||||
export * from "./projects";
|
export * from "./projects";
|
||||||
export * from "./projectUserAdditionalPrivilege";
|
export * from "./projectUserAdditionalPrivilege";
|
||||||
export * from "./rateLimit";
|
export * from "./rateLimit";
|
||||||
|
|||||||
@@ -9,3 +9,10 @@ export enum PkiSyncStatus {
|
|||||||
Succeeded = "succeeded",
|
Succeeded = "succeeded",
|
||||||
Failed = "failed"
|
Failed = "failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertificateSyncStatus {
|
||||||
|
Pending = "pending",
|
||||||
|
Syncing = "syncing",
|
||||||
|
Succeeded = "succeeded",
|
||||||
|
Failed = "failed"
|
||||||
|
}
|
||||||
|
|||||||
@@ -198,3 +198,47 @@ export const useTriggerPkiSyncRemoveCertificates = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAddCertificatesToPkiSync = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
pkiSyncId,
|
||||||
|
certificateIds
|
||||||
|
}: {
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateIds: string[];
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v1/pki/syncs/${pkiSyncId}/certificates`, {
|
||||||
|
certificateIds
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { pkiSyncId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.certificates(pkiSyncId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveCertificatesFromPkiSync = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
pkiSyncId,
|
||||||
|
certificateIds
|
||||||
|
}: {
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateIds: string[];
|
||||||
|
}) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/pki/syncs/${pkiSyncId}/certificates`, {
|
||||||
|
data: { certificateIds }
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { pkiSyncId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: pkiSyncKeys.certificates(pkiSyncId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,14 +2,25 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { PkiSync, TPkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
import { PkiSync, TPkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||||
import { TListPkiSyncOptions, TListPkiSyncs, TPkiSync } from "@app/hooks/api/pkiSyncs/types";
|
import {
|
||||||
|
TListPkiSyncOptions,
|
||||||
|
TListPkiSyncs,
|
||||||
|
TPkiSync,
|
||||||
|
TPkiSyncCertificate
|
||||||
|
} from "@app/hooks/api/pkiSyncs/types";
|
||||||
|
|
||||||
export const pkiSyncKeys = {
|
export const pkiSyncKeys = {
|
||||||
all: ["pki-sync"] as const,
|
all: ["pki-sync"] as const,
|
||||||
options: () => [...pkiSyncKeys.all, "options"] as const,
|
options: () => [...pkiSyncKeys.all, "options"] as const,
|
||||||
list: (projectId: string) => [...pkiSyncKeys.all, "list", projectId] as const,
|
list: (projectId: string) => [...pkiSyncKeys.all, "list", projectId] as const,
|
||||||
|
listWithCertificate: (projectId: string, certificateId: string) =>
|
||||||
|
[...pkiSyncKeys.all, "list", projectId, "with-certificate", certificateId] as const,
|
||||||
byId: (syncId: string, projectId: string) =>
|
byId: (syncId: string, projectId: string) =>
|
||||||
[...pkiSyncKeys.all, "by-id", syncId, projectId] as const
|
[...pkiSyncKeys.all, "by-id", syncId, projectId] as const,
|
||||||
|
certificates: (syncId: string, pagination?: { offset: number; limit: number }) =>
|
||||||
|
pagination
|
||||||
|
? ([...pkiSyncKeys.all, "certificates", syncId, pagination] as const)
|
||||||
|
: ([...pkiSyncKeys.all, "certificates", syncId] as const)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePkiSyncOptions = (
|
export const usePkiSyncOptions = (
|
||||||
@@ -41,9 +52,14 @@ export const usePkiSyncOption = (destination: PkiSync) => {
|
|||||||
return { syncOption, isPending };
|
return { syncOption, isPending };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPkiSyncsByProjectId = async (projectId: string) => {
|
export const fetchPkiSyncsByProjectId = async (projectId: string, certificateId?: string) => {
|
||||||
|
const params: { projectId: string; certificateId?: string } = { projectId };
|
||||||
|
if (certificateId) {
|
||||||
|
params.certificateId = certificateId;
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await apiRequest.get<TListPkiSyncs>("/api/v1/pki/syncs", {
|
const { data } = await apiRequest.get<TListPkiSyncs>("/api/v1/pki/syncs", {
|
||||||
params: { projectId }
|
params
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.pkiSyncs;
|
return data.pkiSyncs;
|
||||||
@@ -63,6 +79,27 @@ export const useListPkiSyncs = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListPkiSyncsWithCertificate = (
|
||||||
|
projectId: string,
|
||||||
|
certificateId: string,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TPkiSync[],
|
||||||
|
unknown,
|
||||||
|
TPkiSync[],
|
||||||
|
ReturnType<typeof pkiSyncKeys.listWithCertificate>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: pkiSyncKeys.listWithCertificate(projectId, certificateId),
|
||||||
|
queryFn: () => fetchPkiSyncsByProjectId(projectId, certificateId),
|
||||||
|
enabled: !!projectId && !!certificateId,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetPkiSync = (
|
export const useGetPkiSync = (
|
||||||
{ syncId, projectId }: { syncId: string; projectId: string },
|
{ syncId, projectId }: { syncId: string; projectId: string },
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
@@ -82,3 +119,33 @@ export const useGetPkiSync = (
|
|||||||
...options
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListPkiSyncCertificates = (
|
||||||
|
syncId: string,
|
||||||
|
pagination?: { offset?: number; limit?: number },
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
{ certificates: TPkiSyncCertificate[]; totalCount: number },
|
||||||
|
unknown,
|
||||||
|
{ certificates: TPkiSyncCertificate[]; totalCount: number },
|
||||||
|
ReturnType<typeof pkiSyncKeys.certificates>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { offset = 0, limit = 20 } = pagination || {};
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: pkiSyncKeys.certificates(syncId, { offset, limit }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get(`/api/v1/pki/syncs/${syncId}/certificates`, {
|
||||||
|
params: { offset, limit }
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
certificates: data.certificates || [],
|
||||||
|
totalCount: data.totalCount || 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
import { PkiSyncStatus } from "../enums";
|
import { CertificateSyncStatus, PkiSyncStatus } from "../enums";
|
||||||
|
|
||||||
export type RootPkiSyncOptions = {
|
export type RootPkiSyncOptions = {
|
||||||
canImportCertificates: boolean;
|
canImportCertificates: boolean;
|
||||||
@@ -43,4 +43,26 @@ export type TRootPkiSync = {
|
|||||||
} | null;
|
} | null;
|
||||||
appConnectionName?: string;
|
appConnectionName?: string;
|
||||||
appConnectionApp?: string;
|
appConnectionApp?: string;
|
||||||
|
hasCertificate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPkiSyncCertificate = {
|
||||||
|
id: string;
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
syncStatus?: CertificateSyncStatus | null;
|
||||||
|
lastSyncMessage?: string | null;
|
||||||
|
lastSyncedAt?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ type TCreatePkiSyncDTOBase = {
|
|||||||
certificateNameSchema?: string;
|
certificateNameSchema?: string;
|
||||||
};
|
};
|
||||||
isAutoSyncEnabled: boolean;
|
isAutoSyncEnabled: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
|
certificateIds?: string[];
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -664,17 +664,26 @@ export const useListWorkspaceCas = ({
|
|||||||
export const useListWorkspaceCertificates = ({
|
export const useListWorkspaceCertificates = ({
|
||||||
projectId,
|
projectId,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
forPkiSync
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName?: string;
|
||||||
|
forPkiSync?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: projectKeys.specificProjectCertificates({
|
queryKey: projectKeys.specificProjectCertificates({
|
||||||
projectId,
|
projectId,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
forPkiSync
|
||||||
}),
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -682,6 +691,16 @@ export const useListWorkspaceCertificates = ({
|
|||||||
limit: String(limit)
|
limit: String(limit)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (friendlyName) {
|
||||||
|
params.append("friendlyName", friendlyName);
|
||||||
|
}
|
||||||
|
if (commonName) {
|
||||||
|
params.append("commonName", commonName);
|
||||||
|
}
|
||||||
|
if (forPkiSync) {
|
||||||
|
params.append("forPkiSync", "true");
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: { certificates, totalCount }
|
data: { certificates, totalCount }
|
||||||
} = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>(
|
} = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>(
|
||||||
@@ -693,7 +712,8 @@ export const useListWorkspaceCertificates = ({
|
|||||||
|
|
||||||
return { certificates, totalCount };
|
return { certificates, totalCount };
|
||||||
},
|
},
|
||||||
enabled: Boolean(projectId)
|
enabled: Boolean(projectId),
|
||||||
|
placeholderData: (previousData) => previousData
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,22 @@ export const projectKeys = {
|
|||||||
specificProjectCertificates: ({
|
specificProjectCertificates: ({
|
||||||
projectId,
|
projectId,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
|
friendlyName,
|
||||||
|
commonName,
|
||||||
|
forPkiSync
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}) => [...projectKeys.forProjectCertificates(projectId), { offset, limit }] as const,
|
friendlyName?: string;
|
||||||
|
commonName?: string;
|
||||||
|
forPkiSync?: boolean;
|
||||||
|
}) =>
|
||||||
|
[
|
||||||
|
...projectKeys.forProjectCertificates(projectId),
|
||||||
|
{ offset, limit, friendlyName, commonName, forPkiSync }
|
||||||
|
] as const,
|
||||||
getProjectPkiAlerts: (projectId: string) => [{ projectId }, "project-pki-alerts"] as const,
|
getProjectPkiAlerts: (projectId: string) => [{ projectId }, "project-pki-alerts"] as const,
|
||||||
getProjectPkiSubscribers: (projectId: string) =>
|
getProjectPkiSubscribers: (projectId: string) =>
|
||||||
[{ projectId }, "project-pki-subscribers"] as const,
|
[{ projectId }, "project-pki-subscribers"] as const,
|
||||||
|
|||||||
@@ -52,25 +52,7 @@ export const PkiManagerLayout = () => {
|
|||||||
projectId: currentProject.id
|
projectId: currentProject.id
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Policies</Tab>}
|
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Certificates</Tab>}
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/projects/cert-management/$projectId/certificates"
|
|
||||||
params={{
|
|
||||||
projectId: currentProject.id
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<Tab
|
|
||||||
value={
|
|
||||||
isActive || location.pathname.match(/\/pki-collections\//)
|
|
||||||
? "selected"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Certificates
|
|
||||||
</Tab>
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/projects/cert-management/$projectId/certificate-authorities"
|
to="/projects/cert-management/$projectId/certificate-authorities"
|
||||||
|
|||||||
+1
-1
@@ -124,7 +124,7 @@ type Props = {
|
|||||||
|
|
||||||
const caTypes = [
|
const caTypes = [
|
||||||
{ label: "ACME", value: CaType.ACME },
|
{ label: "ACME", value: CaType.ACME },
|
||||||
{ label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS }
|
{ label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Helmet } from "react-helmet";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { PageHeader } from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionCertificateActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
useProjectPermission
|
|
||||||
} from "@app/context";
|
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
|
||||||
|
|
||||||
import { PkiCollectionSection } from "../AlertingPage/components";
|
|
||||||
import { CertificatesSection } from "./components";
|
|
||||||
|
|
||||||
export const CertificatesPage = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { permission } = useProjectPermission();
|
|
||||||
|
|
||||||
const canAccessPkiColl = permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.PkiCollections
|
|
||||||
);
|
|
||||||
const canAccessCerts = permission.can(
|
|
||||||
ProjectPermissionCertificateActions.Read,
|
|
||||||
ProjectPermissionSub.Certificates
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
|
||||||
<Helmet>
|
|
||||||
<title>{t("common.head-title", { title: "Certificates" })}</title>
|
|
||||||
</Helmet>
|
|
||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
|
||||||
<PageHeader
|
|
||||||
scope={ProjectType.CertificateManager}
|
|
||||||
title="Certificates"
|
|
||||||
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
|
||||||
/>
|
|
||||||
{/* If both are false, the section does not render. This is to prevent duplicate banners. */}
|
|
||||||
{(canAccessCerts || canAccessPkiColl) && (
|
|
||||||
<ProjectPermissionCan
|
|
||||||
renderGuardBanner
|
|
||||||
I={ProjectPermissionActions.Read}
|
|
||||||
a={ProjectPermissionSub.PkiCollections}
|
|
||||||
>
|
|
||||||
<PkiCollectionSection />
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
)}
|
|
||||||
<ProjectPermissionCan
|
|
||||||
renderGuardBanner
|
|
||||||
I={ProjectPermissionCertificateActions.Read}
|
|
||||||
a={ProjectPermissionSub.Certificates}
|
|
||||||
>
|
|
||||||
<CertificatesSection />
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+3
-2
@@ -122,11 +122,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
|
|
||||||
const { data: profilesData } = useListCertificateProfiles({
|
const { data: profilesData } = useListCertificateProfiles({
|
||||||
projectId: currentProject?.id || "",
|
projectId: currentProject?.id || "",
|
||||||
includeMetrics: false,
|
|
||||||
enrollmentType: "api"
|
enrollmentType: "api"
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
const { mutateAsync: createCertificate } = useCreateCertificateV3({
|
||||||
|
projectId: currentProject?.id
|
||||||
|
});
|
||||||
|
|
||||||
const formResolver = useMemo(() => {
|
const formResolver = useMemo(() => {
|
||||||
return zodResolver(createSchema(shouldShowSubjectSection));
|
return zodResolver(createSchema(shouldShowSubjectSection));
|
||||||
|
|||||||
+296
@@ -0,0 +1,296 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import {
|
||||||
|
PkiSync,
|
||||||
|
useAddCertificatesToPkiSync,
|
||||||
|
useListPkiSyncsWithCertificate,
|
||||||
|
useRemoveCertificatesFromPkiSync
|
||||||
|
} from "@app/hooks/api/pkiSyncs";
|
||||||
|
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: {
|
||||||
|
isOpen: boolean;
|
||||||
|
data?: {
|
||||||
|
certificateId?: string;
|
||||||
|
commonName?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
handlePopUpToggle: (popUpName: "managePkiSyncs", state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PER_PAGE = 10;
|
||||||
|
|
||||||
|
export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
const [selectedSyncIds, setSelectedSyncIds] = useState<Set<string>>(new Set());
|
||||||
|
const [initialSyncIds, setInitialSyncIds] = useState<Set<string>>(new Set());
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { certificateId, commonName } = popUp.data || {};
|
||||||
|
|
||||||
|
const { data: pkiSyncs = [], isPending } = useListPkiSyncsWithCertificate(
|
||||||
|
currentProject?.id || "",
|
||||||
|
certificateId || "",
|
||||||
|
{
|
||||||
|
enabled: !!currentProject?.id && !!certificateId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const addCertificatesToSync = useAddCertificatesToPkiSync();
|
||||||
|
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||||
|
|
||||||
|
const filteredSyncs = useMemo(() => {
|
||||||
|
if (!searchTerm.trim()) return pkiSyncs;
|
||||||
|
|
||||||
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
return pkiSyncs.filter((sync) => sync.name.toLowerCase().includes(searchLower));
|
||||||
|
}, [pkiSyncs, searchTerm]);
|
||||||
|
|
||||||
|
const startIndex = (currentPage - 1) * PER_PAGE;
|
||||||
|
const endIndex = startIndex + PER_PAGE;
|
||||||
|
const paginatedSyncs = filteredSyncs.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
handlePopUpToggle("managePkiSyncs", false);
|
||||||
|
setSelectedSyncIds(new Set());
|
||||||
|
setInitialSyncIds(new Set());
|
||||||
|
setSearchTerm("");
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToPkiSyncs = () => {
|
||||||
|
if (!currentProject?.id) return;
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||||
|
params: {
|
||||||
|
projectId: currentProject.id
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
selectedTab: IntegrationsListPageTabs.PkiSyncs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDestinationDisplayName = (destination: string) => {
|
||||||
|
switch (destination) {
|
||||||
|
case PkiSync.AzureKeyVault:
|
||||||
|
return "Azure Key Vault";
|
||||||
|
case PkiSync.AwsCertificateManager:
|
||||||
|
return "AWS Certificate Manager";
|
||||||
|
default:
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!certificateId || !pkiSyncs || pkiSyncs.length === 0) return;
|
||||||
|
|
||||||
|
const currentSyncIds = new Set(
|
||||||
|
pkiSyncs.filter((sync) => sync.hasCertificate).map((sync) => sync.id)
|
||||||
|
);
|
||||||
|
setSelectedSyncIds(currentSyncIds);
|
||||||
|
setInitialSyncIds(new Set(currentSyncIds));
|
||||||
|
}, [certificateId, pkiSyncs]);
|
||||||
|
|
||||||
|
const handleSyncToggle = (syncId: string) => {
|
||||||
|
setSelectedSyncIds((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(syncId)) {
|
||||||
|
newSet.delete(syncId);
|
||||||
|
} else {
|
||||||
|
newSet.add(syncId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveChanges = async () => {
|
||||||
|
if (!certificateId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const syncsToAdd = Array.from(selectedSyncIds).filter((id) => !initialSyncIds.has(id));
|
||||||
|
const syncsToRemove = Array.from(initialSyncIds).filter((id) => !selectedSyncIds.has(id));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
syncsToAdd.map((syncId) =>
|
||||||
|
addCertificatesToSync.mutateAsync({
|
||||||
|
pkiSyncId: syncId,
|
||||||
|
certificateIds: [certificateId]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
syncsToRemove.map((syncId) =>
|
||||||
|
removeCertificatesFromSync.mutateAsync({
|
||||||
|
pkiSyncId: syncId,
|
||||||
|
certificateIds: [certificateId]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `PKI sync settings updated for certificate "${commonName}"`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update PKI sync settings",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={popUp.isOpen} onOpenChange={handleClose}>
|
||||||
|
<ModalContent
|
||||||
|
title="Manage PKI Syncs"
|
||||||
|
subTitle={`Select which PKI syncs "${commonName}" should be part of`}
|
||||||
|
className="max-w-3xl"
|
||||||
|
>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Input
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search PKI syncs by name..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 max-h-96 overflow-y-auto">
|
||||||
|
{isPending && (
|
||||||
|
<div className="flex h-32 items-center justify-center">
|
||||||
|
<div className="text-bunker-300">Loading PKI syncs...</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isPending && pkiSyncs.length === 0 && (
|
||||||
|
<EmptyState title="No PKI syncs available" icon={faPlus}>
|
||||||
|
<div className="mt-1">
|
||||||
|
Create a{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleNavigateToPkiSyncs}
|
||||||
|
className="cursor-pointer underline hover:text-mineshaft-300"
|
||||||
|
>
|
||||||
|
PKI sync
|
||||||
|
</button>{" "}
|
||||||
|
first to manage certificate syncing.
|
||||||
|
</div>
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
|
||||||
|
<EmptyState title="No PKI syncs found" icon={faSearch}>
|
||||||
|
<div className="mt-1">
|
||||||
|
No PKI syncs match your search criteria. Try a different search term.
|
||||||
|
</div>
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
|
{!isPending && filteredSyncs.length > 0 && (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-12" />
|
||||||
|
<Th className="w-1/2">Name</Th>
|
||||||
|
<Th className="w-1/2">Destination</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{paginatedSyncs.map((sync) => (
|
||||||
|
<Tr
|
||||||
|
key={sync.id}
|
||||||
|
className="cursor-pointer hover:bg-mineshaft-700"
|
||||||
|
onClick={() => handleSyncToggle(sync.id)}
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedSyncIds.has(sync.id)}
|
||||||
|
onCheckedChange={() => handleSyncToggle(sync.id)}
|
||||||
|
id={`sync-${sync.id}`}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td className="w-1/2 max-w-0">
|
||||||
|
<div className="truncate" title={sync.name}>
|
||||||
|
{sync.name}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="w-1/2 max-w-0">
|
||||||
|
<div
|
||||||
|
className="truncate capitalize"
|
||||||
|
title={getDestinationDisplayName(sync.destination)}
|
||||||
|
>
|
||||||
|
{getDestinationDisplayName(sync.destination)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
{!isPending && filteredSyncs.length > PER_PAGE && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<Pagination
|
||||||
|
count={filteredSyncs.length}
|
||||||
|
page={currentPage}
|
||||||
|
perPage={PER_PAGE}
|
||||||
|
onChangePage={setCurrentPage}
|
||||||
|
onChangePerPage={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end gap-3">
|
||||||
|
<Button variant="outline_bg" onClick={handleClose} isDisabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
colorSchema="primary"
|
||||||
|
onClick={handleSaveChanges}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
|||||||
import { CertificateCertModal } from "./CertificateCertModal";
|
import { CertificateCertModal } from "./CertificateCertModal";
|
||||||
import { CertificateImportModal } from "./CertificateImportModal";
|
import { CertificateImportModal } from "./CertificateImportModal";
|
||||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||||
|
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||||
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
|
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
|
||||||
import { CertificateModal } from "./CertificateModal";
|
import { CertificateModal } from "./CertificateModal";
|
||||||
import { CertificateRenewalModal } from "./CertificateRenewalModal";
|
import { CertificateRenewalModal } from "./CertificateRenewalModal";
|
||||||
@@ -36,7 +37,8 @@ export const CertificatesSection = () => {
|
|||||||
"deleteCertificate",
|
"deleteCertificate",
|
||||||
"revokeCertificate",
|
"revokeCertificate",
|
||||||
"manageRenewal",
|
"manageRenewal",
|
||||||
"renewCertificate"
|
"renewCertificate",
|
||||||
|
"managePkiSyncs"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
||||||
@@ -104,6 +106,10 @@ export const CertificatesSection = () => {
|
|||||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<CertificateManagePkiSyncsModal
|
||||||
|
popUp={popUp.managePkiSyncs}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteCertificate.isOpen}
|
isOpen={popUp.deleteCertificate.isOpen}
|
||||||
title={`Are you sure you want to remove the certificate ${
|
title={`Are you sure you want to remove the certificate ${
|
||||||
|
|||||||
+118
-117
@@ -2,19 +2,24 @@ import { useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
faBan,
|
faBan,
|
||||||
faCertificate,
|
faCertificate,
|
||||||
|
faClockRotateLeft,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEye,
|
faEye,
|
||||||
faFileExport,
|
faFileExport,
|
||||||
|
faLink,
|
||||||
faRedo,
|
faRedo,
|
||||||
faTrash
|
faTrash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { CircleQuestionMarkIcon } from "lucide-react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
CertificateDisplayName,
|
||||||
|
getCertificateDisplayName
|
||||||
|
} from "@app/components/utilities/certificateDisplayUtils";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -38,12 +43,12 @@ import {
|
|||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProject
|
useProject
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useListWorkspaceCertificates, useUpdateRenewalConfig } from "@app/hooks/api";
|
import { useUpdateRenewalConfig } from "@app/hooks/api";
|
||||||
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
|
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
|
||||||
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
|
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
|
||||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||||
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
import { CertStatus } from "@app/hooks/api/certificates/enums";
|
||||||
import { TCertificate } from "@app/hooks/api/certificates/types";
|
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
|
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
|
||||||
@@ -55,93 +60,6 @@ const isExpiringWithinOneDay = (notAfter: string): boolean => {
|
|||||||
return expiryDate <= oneDayFromNow;
|
return expiryDate <= oneDayFromNow;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAutoRenewalInfo = (certificate: TCertificate) => {
|
|
||||||
if (certificate.renewedByCertificateId) {
|
|
||||||
return { text: "Renewed", variant: "neutral" as const };
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
|
||||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
|
||||||
const hasNoProfile = !certificate.profileId;
|
|
||||||
const isExpiringWithinDay = isExpiringWithinOneDay(certificate.notAfter);
|
|
||||||
|
|
||||||
if (isRevoked) {
|
|
||||||
return {
|
|
||||||
text: "Not Available",
|
|
||||||
variant: "neutral" as const,
|
|
||||||
tooltip: "Renewal is not available for revoked certificates"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExpired) {
|
|
||||||
return {
|
|
||||||
text: "Not Available",
|
|
||||||
variant: "neutral" as const,
|
|
||||||
tooltip: "Renewal is not available for expired certificates"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNoProfile) {
|
|
||||||
return {
|
|
||||||
text: "Not Available",
|
|
||||||
variant: "neutral" as const,
|
|
||||||
tooltip: "Renewal requires a certificate profile"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certificate.hasPrivateKey === false) {
|
|
||||||
return {
|
|
||||||
text: "Not Available",
|
|
||||||
variant: "neutral" as const,
|
|
||||||
tooltip: "Renewal is not available for certificates with externally generated private keys"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExpiringWithinDay) {
|
|
||||||
return {
|
|
||||||
text: "Not Available",
|
|
||||||
variant: "neutral" as const,
|
|
||||||
tooltip: "Auto-renewal is not available for certificates expiring within 24 hours"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certificate.renewalError) {
|
|
||||||
return {
|
|
||||||
text: "Failed",
|
|
||||||
variant: "danger" as const,
|
|
||||||
tooltip: certificate.renewalError
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!certificate.renewBeforeDays) {
|
|
||||||
return { text: "Auto-Renewal Disabled", variant: "warning" as const };
|
|
||||||
}
|
|
||||||
|
|
||||||
const notAfterDate = new Date(certificate.notAfter);
|
|
||||||
const renewalDate = new Date(
|
|
||||||
notAfterDate.getTime() - certificate.renewBeforeDays * 24 * 60 * 60 * 1000
|
|
||||||
);
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
if (renewalDate <= now) {
|
|
||||||
return { text: "Due Now", variant: "danger" as const };
|
|
||||||
}
|
|
||||||
|
|
||||||
const daysUntilRenewal = Math.floor(
|
|
||||||
(renewalDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (daysUntilRenewal === 0) {
|
|
||||||
return { text: "Renews today", variant: "warning" as const };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (daysUntilRenewal <= 7) {
|
|
||||||
return { text: `Renews in ${daysUntilRenewal}d`, variant: "warning" as const };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { text: `Renews in ${daysUntilRenewal}d`, variant: "success" as const };
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<
|
popUpName: keyof UsePopUpState<
|
||||||
@@ -151,7 +69,8 @@ type Props = {
|
|||||||
"revokeCertificate",
|
"revokeCertificate",
|
||||||
"certificateCert",
|
"certificateCert",
|
||||||
"manageRenewal",
|
"manageRenewal",
|
||||||
"renewCertificate"
|
"renewCertificate",
|
||||||
|
"managePkiSyncs"
|
||||||
]
|
]
|
||||||
>,
|
>,
|
||||||
data?: {
|
data?: {
|
||||||
@@ -232,20 +151,18 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Common Name</Th>
|
<Th className="w-1/2">SAN / CN</Th>
|
||||||
<Th>Status</Th>
|
<Th className="w-1/6">Status</Th>
|
||||||
<Th>Not Before</Th>
|
<Th className="w-1/6">Not Before</Th>
|
||||||
<Th>Not After</Th>
|
<Th className="w-1/6">Not After</Th>
|
||||||
<Th>Renewal Status</Th>
|
<Th className="w-12" />
|
||||||
<Th />
|
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isPending && <TableSkeleton columns={5} innerKey="project-cas" />}
|
{isPending && <TableSkeleton columns={4} innerKey="project-cas" />}
|
||||||
{!isPending &&
|
{!isPending &&
|
||||||
data?.certificates.map((certificate) => {
|
data?.certificates.map((certificate) => {
|
||||||
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
const { variant, label } = getCertValidUntilBadgeDetails(certificate.notAfter);
|
||||||
const autoRenewalInfo = getAutoRenewalInfo(certificate);
|
|
||||||
|
|
||||||
const isRevoked = certificate.status === CertStatus.REVOKED;
|
const isRevoked = certificate.status === CertStatus.REVOKED;
|
||||||
const isExpired = new Date(certificate.notAfter) < new Date();
|
const isExpired = new Date(certificate.notAfter) < new Date();
|
||||||
@@ -254,9 +171,24 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
const isAutoRenewalEnabled = Boolean(
|
const isAutoRenewalEnabled = Boolean(
|
||||||
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
certificate.renewBeforeDays && certificate.renewBeforeDays > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canShowAutoRenewalIcon = Boolean(
|
||||||
|
certificate.profileId &&
|
||||||
|
certificate.hasPrivateKey !== false &&
|
||||||
|
!certificate.renewedByCertificateId &&
|
||||||
|
!isRevoked &&
|
||||||
|
!isExpired &&
|
||||||
|
!isExpiringWithinDay
|
||||||
|
);
|
||||||
|
|
||||||
|
// Still need originalDisplayName for other uses in the component
|
||||||
|
const { originalDisplayName } = getCertificateDisplayName(certificate, 64, "—");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`certificate-${certificate.id}`}>
|
<Tr className="group h-10" key={`certificate-${certificate.id}`}>
|
||||||
<Td>{certificate.commonName}</Td>
|
<Td className="max-w-0">
|
||||||
|
<CertificateDisplayName cert={certificate} maxLength={64} fallback="—" />
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{certificate.status === CertStatus.REVOKED ? (
|
{certificate.status === CertStatus.REVOKED ? (
|
||||||
<Badge variant="danger">Revoked</Badge>
|
<Badge variant="danger">Revoked</Badge>
|
||||||
@@ -274,22 +206,64 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
|
||||||
: "-"}
|
: "-"}
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td className="flex items-center justify-end gap-2">
|
||||||
{autoRenewalInfo &&
|
<div
|
||||||
(autoRenewalInfo.tooltip ? (
|
className={`transition-opacity ${(() => {
|
||||||
<div className="flex items-center gap-2">
|
if (!canShowAutoRenewalIcon) return "";
|
||||||
<Tooltip content={autoRenewalInfo.tooltip}>
|
if (isAutoRenewalEnabled) return "opacity-100";
|
||||||
<Badge variant={autoRenewalInfo.variant}>
|
return "opacity-0 group-hover:opacity-100";
|
||||||
{autoRenewalInfo.text}
|
})()}`}
|
||||||
<CircleQuestionMarkIcon />
|
>
|
||||||
</Badge>
|
{canShowAutoRenewalIcon && (
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
</div>
|
content={(() => {
|
||||||
) : (
|
if (hasFailed && certificate.renewalError) {
|
||||||
<Badge variant={autoRenewalInfo.variant}>{autoRenewalInfo.text}</Badge>
|
return `Auto-renewal failed: ${certificate.renewalError}`;
|
||||||
))}
|
}
|
||||||
</Td>
|
if (isAutoRenewalEnabled) {
|
||||||
<Td className="flex justify-end">
|
const expiryDate = new Date(certificate.notAfter);
|
||||||
|
const now = new Date();
|
||||||
|
const daysUntilExpiry = Math.ceil(
|
||||||
|
(expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
|
const daysUntilRenewal = Math.max(
|
||||||
|
0,
|
||||||
|
daysUntilExpiry - (certificate.renewBeforeDays || 0)
|
||||||
|
);
|
||||||
|
return `Auto-renews in ${daysUntilRenewal}d`;
|
||||||
|
}
|
||||||
|
return "Set auto renewal";
|
||||||
|
})()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={(() => {
|
||||||
|
if (hasFailed) return "pr-1 text-red-500 hover:text-red-400";
|
||||||
|
return "pr-1 text-primary-500 hover:text-primary-400";
|
||||||
|
})()}
|
||||||
|
aria-label="Certificate auto-renewal"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (hasFailed) return;
|
||||||
|
|
||||||
|
handlePopUpOpen("manageRenewal", {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
commonName: originalDisplayName,
|
||||||
|
profileId: certificate.profileId || "",
|
||||||
|
renewBeforeDays: certificate.renewBeforeDays || 7,
|
||||||
|
ttlDays: Math.ceil(
|
||||||
|
(new Date(certificate.notAfter).getTime() -
|
||||||
|
new Date(certificate.notBefore).getTime()) /
|
||||||
|
(24 * 60 * 60 * 1000)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClockRotateLeft} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
@@ -483,6 +457,33 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
{/* PKI Sync management - only for active certificates that are not renewed */}
|
||||||
|
{certificate.status === CertStatus.ACTIVE &&
|
||||||
|
!certificate.renewedByCertificateId && (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionCertificateActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Certificates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed &&
|
||||||
|
"pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={async () =>
|
||||||
|
handlePopUpOpen("managePkiSyncs", {
|
||||||
|
certificateId: certificate.id,
|
||||||
|
commonName: certificate.commonName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faLink} />}
|
||||||
|
>
|
||||||
|
Manage PKI Syncs
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
{/* Only show revoke button if CA supports revocation */}
|
{/* Only show revoke button if CA supports revocation */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const caType = caCapabilityMap[certificate.caId];
|
const caType = caCapabilityMap[certificate.caId];
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
import { CertificatesPage } from "./CertificatesPage";
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates"
|
|
||||||
)({
|
|
||||||
component: CertificatesPage,
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
return {
|
|
||||||
breadcrumbs: [
|
|
||||||
...context.breadcrumbs,
|
|
||||||
{
|
|
||||||
label: "Certificates"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
-19
@@ -12,7 +12,6 @@ import {
|
|||||||
faToggleOff,
|
faToggleOff,
|
||||||
faToggleOn,
|
faToggleOn,
|
||||||
faTrash,
|
faTrash,
|
||||||
faTriangleExclamation,
|
|
||||||
faXmark
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -47,7 +46,6 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { PkiSyncStatus, TPkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
import { PkiSyncStatus, TPkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||||
|
|
||||||
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
|
import { PkiSyncDestinationCol } from "./PkiSyncDestinationCol";
|
||||||
import { PkiSyncTableCell } from "./PkiSyncTableCell";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pkiSync: TPkiSync;
|
pkiSync: TPkiSync;
|
||||||
@@ -163,23 +161,6 @@ export const PkiSyncRow = ({
|
|||||||
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
|
<p className="truncate text-xs leading-4 text-bunker-300">{destinationDetails.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
{subscriberId ? (
|
|
||||||
<PkiSyncTableCell
|
|
||||||
primaryText={pkiSync.subscriber?.name || subscriberId}
|
|
||||||
secondaryText="PKI Subscriber"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Td>
|
|
||||||
<Tooltip content="The PKI subscriber for this sync has been deleted. Configure a new source or remove this sync.">
|
|
||||||
<div className="w-min">
|
|
||||||
<Badge variant="warning">
|
|
||||||
<FontAwesomeIcon icon={faTriangleExclamation} />
|
|
||||||
<span>Source Deleted</span>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
<PkiSyncDestinationCol pkiSync={pkiSync} />
|
<PkiSyncDestinationCol pkiSync={pkiSync} />
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
+3
-25
@@ -57,7 +57,6 @@ import { PkiSyncRow } from "./PkiSyncRow";
|
|||||||
|
|
||||||
enum PkiSyncsOrderBy {
|
enum PkiSyncsOrderBy {
|
||||||
Destination = "destination",
|
Destination = "destination",
|
||||||
Source = "source",
|
|
||||||
Name = "name",
|
Name = "name",
|
||||||
Status = "status"
|
Status = "status"
|
||||||
}
|
}
|
||||||
@@ -160,14 +159,6 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
|||||||
const [syncOne, syncTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
const [syncOne, syncTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
switch (orderBy) {
|
switch (orderBy) {
|
||||||
case PkiSyncsOrderBy.Source:
|
|
||||||
return (syncOne.subscriber?.name ?? syncOne.subscriberId ?? "")
|
|
||||||
.toLowerCase()
|
|
||||||
.localeCompare(
|
|
||||||
syncTwo.subscriber?.name?.toLowerCase() ??
|
|
||||||
syncTwo.subscriberId?.toLowerCase() ??
|
|
||||||
""
|
|
||||||
);
|
|
||||||
case PkiSyncsOrderBy.Destination:
|
case PkiSyncsOrderBy.Destination:
|
||||||
return getPkiSyncDestinationColValues(syncOne)
|
return getPkiSyncDestinationColValues(syncOne)
|
||||||
.primaryText.toLowerCase()
|
.primaryText.toLowerCase()
|
||||||
@@ -370,7 +361,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
|||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="w-2" />
|
<Th className="w-2" />
|
||||||
<Th className="w-1/4">
|
<Th className="w-1/2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Name
|
Name
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -383,20 +374,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th className="w-1/3">
|
<Th className="w-1/4">
|
||||||
<div className="flex items-center">
|
|
||||||
Source
|
|
||||||
<IconButton
|
|
||||||
variant="plain"
|
|
||||||
className={getClassName(PkiSyncsOrderBy.Source)}
|
|
||||||
ariaLabel="sort"
|
|
||||||
onClick={() => handleSort(PkiSyncsOrderBy.Source)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={getColSortIcon(PkiSyncsOrderBy.Source)} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Th>
|
|
||||||
<Th className="w-1/3">
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Destination
|
Destination
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -409,7 +387,7 @@ export const PkiSyncsTable = ({ pkiSyncs }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th className="min-w-42">
|
<Th className="w-1/4 min-w-42">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Status
|
Status
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
+40
-2
@@ -12,13 +12,14 @@ import { ProjectPermissionSub, useProject } from "@app/context";
|
|||||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
|
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
|
||||||
|
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||||
|
|
||||||
import { PkiSyncsTable } from "./PkiSyncTable";
|
import { PkiSyncsTable } from "./PkiSyncTable";
|
||||||
|
|
||||||
export const PkiSyncsTab = () => {
|
export const PkiSyncsTab = () => {
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
|
||||||
|
|
||||||
const { addSync, ...search } = useSearch({
|
const { addSync, connectionId, connectionName, ...search } = useSearch({
|
||||||
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
|
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,6 +46,42 @@ export const PkiSyncsTab = () => {
|
|||||||
navigateToBase();
|
navigateToBase();
|
||||||
}, [addSync, handlePopUpOpen, navigateToBase]);
|
}, [addSync, handlePopUpOpen, navigateToBase]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedFormData = localStorage.getItem("pkiSyncFormData");
|
||||||
|
if (storedFormData && !popUp.addSync.isOpen) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(storedFormData);
|
||||||
|
if (connectionId && connectionName) {
|
||||||
|
const initialData = {
|
||||||
|
...parsedData,
|
||||||
|
connection: { id: connectionId, name: connectionName }
|
||||||
|
};
|
||||||
|
handlePopUpOpen("addSync", { destination: parsedData.destination, initialData });
|
||||||
|
navigate({
|
||||||
|
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||||
|
params: { projectId: currentProject?.id },
|
||||||
|
search: { selectedTab: IntegrationsListPageTabs.PkiSyncs },
|
||||||
|
replace: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("addSync", { destination: parsedData.destination });
|
||||||
|
}
|
||||||
|
localStorage.removeItem("pkiSyncFormData");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse stored PKI sync form data:", error);
|
||||||
|
localStorage.removeItem("pkiSyncFormData");
|
||||||
|
handlePopUpOpen("addSync");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
handlePopUpOpen,
|
||||||
|
popUp.addSync.isOpen,
|
||||||
|
connectionId,
|
||||||
|
connectionName,
|
||||||
|
navigate,
|
||||||
|
currentProject?.id
|
||||||
|
]);
|
||||||
|
|
||||||
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
|
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
|
||||||
currentProject?.id || "",
|
currentProject?.id || "",
|
||||||
{
|
{
|
||||||
@@ -94,7 +131,8 @@ export const PkiSyncsTab = () => {
|
|||||||
<PkiSyncsTable pkiSyncs={pkiSyncs} />
|
<PkiSyncsTable pkiSyncs={pkiSyncs} />
|
||||||
</div>
|
</div>
|
||||||
<CreatePkiSyncModal
|
<CreatePkiSyncModal
|
||||||
selectSync={popUp.addSync.data}
|
selectSync={popUp.addSync.data?.destination || popUp.addSync.data}
|
||||||
|
initialData={popUp.addSync.data?.initialData}
|
||||||
isOpen={popUp.addSync.isOpen}
|
isOpen={popUp.addSync.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
|
|||||||
|
|
||||||
const IntegrationsListPageQuerySchema = z.object({
|
const IntegrationsListPageQuerySchema = z.object({
|
||||||
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
|
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
|
||||||
addSync: z.nativeEnum(PkiSync).optional()
|
addSync: z.nativeEnum(PkiSync).optional(),
|
||||||
|
connectionId: z.string().optional(),
|
||||||
|
connectionName: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
|
|||||||
+4
-4
@@ -59,9 +59,9 @@ export const PkiCollectionPage = () => {
|
|||||||
});
|
});
|
||||||
handlePopUpClose("deletePkiCollection");
|
handlePopUpClose("deletePkiCollection");
|
||||||
navigate({
|
navigate({
|
||||||
to: "/projects/cert-management/$projectId/certificates",
|
to: "/projects/cert-management/$projectId/policies",
|
||||||
params: {
|
params: {
|
||||||
projectId
|
projectId: params.projectId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -77,9 +77,9 @@ export const PkiCollectionPage = () => {
|
|||||||
{data && (
|
{data && (
|
||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||||
<Link
|
<Link
|
||||||
to="/projects/cert-management/$projectId/certificates"
|
to="/projects/cert-management/$projectId/policies"
|
||||||
params={{
|
params={{
|
||||||
projectId
|
projectId: params.projectId
|
||||||
}}
|
}}
|
||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-6
@@ -5,13 +5,9 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import {
|
import { CaStatus, useAddItemToPkiCollection, useListWorkspaceCas } from "@app/hooks/api";
|
||||||
CaStatus,
|
|
||||||
useAddItemToPkiCollection,
|
|
||||||
useListWorkspaceCas,
|
|
||||||
useListWorkspaceCertificates
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
|
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
|
||||||
|
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
|||||||
{
|
{
|
||||||
label: "Certificate Collections",
|
label: "Certificate Collections",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
to: "/projects/cert-management/$projectId/certificates",
|
to: "/projects/cert-management/$projectId/policies",
|
||||||
params: {
|
params: {
|
||||||
projectId: params.projectId
|
projectId: params.projectId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
|||||||
import {
|
import {
|
||||||
PkiSyncActionTriggers,
|
PkiSyncActionTriggers,
|
||||||
PkiSyncAuditLogsSection,
|
PkiSyncAuditLogsSection,
|
||||||
|
PkiSyncCertificatesSection,
|
||||||
PkiSyncDestinationSection,
|
PkiSyncDestinationSection,
|
||||||
PkiSyncDetailsSection,
|
PkiSyncDetailsSection,
|
||||||
PkiSyncOptionsSection,
|
PkiSyncOptionsSection
|
||||||
PkiSyncSourceSection
|
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
const PageContent = () => {
|
const PageContent = () => {
|
||||||
@@ -62,7 +62,6 @@ const PageContent = () => {
|
|||||||
const destinationDetails = PKI_SYNC_MAP[pkiSync.destination];
|
const destinationDetails = PKI_SYNC_MAP[pkiSync.destination];
|
||||||
|
|
||||||
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
|
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
|
||||||
const handleEditSource = () => handlePopUpOpen("editSync", PkiSyncEditFields.Source);
|
|
||||||
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
|
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
|
||||||
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
|
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
|
||||||
|
|
||||||
@@ -103,7 +102,6 @@ const PageContent = () => {
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||||
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
|
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
|
||||||
<PkiSyncSourceSection pkiSync={pkiSync} onEditSource={handleEditSource} />
|
|
||||||
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
|
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
@@ -111,6 +109,7 @@ const PageContent = () => {
|
|||||||
pkiSync={pkiSync}
|
pkiSync={pkiSync}
|
||||||
onEditDestination={handleEditDestination}
|
onEditDestination={handleEditDestination}
|
||||||
/>
|
/>
|
||||||
|
<PkiSyncCertificatesSection pkiSync={pkiSync} />
|
||||||
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
|
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ export const PkiSyncAuditLogsSection = ({ pkiSync }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex max-h-full w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
<div className="flex max-h-full w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
<h3 className="font-medium text-mineshaft-100">Sync Logs</h3>
|
<h3 className="text-lg font-medium text-mineshaft-100">Sync Logs</h3>
|
||||||
{subscription.auditLogs && (
|
{subscription.auditLogs && (
|
||||||
<p className="text-xs text-bunker-300">
|
<p className="text-xs text-bunker-300">
|
||||||
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
|
Displaying audit logs from the last {Math.min(auditLogsRetentionDays, 60)} days
|
||||||
|
|||||||
+328
@@ -0,0 +1,328 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
|
import {
|
||||||
|
faCertificate,
|
||||||
|
faClockRotateLeft,
|
||||||
|
faEdit,
|
||||||
|
faTrash
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { CertificateManagementModal } from "@app/components/pki-syncs/CertificateManagementModal";
|
||||||
|
import {
|
||||||
|
CertificateDisplayName,
|
||||||
|
getCertificateDisplayName
|
||||||
|
} from "@app/components/utilities/certificateDisplayUtils";
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { Badge } from "@app/components/v3";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { useListPkiSyncCertificates, useRemoveCertificatesFromPkiSync } from "@app/hooks/api";
|
||||||
|
import { CertificateSyncStatus, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pkiSync: TPkiSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSyncStatusVariant = (status?: CertificateSyncStatus | null) => {
|
||||||
|
if (status === CertificateSyncStatus.Succeeded) return "success";
|
||||||
|
if (status === CertificateSyncStatus.Failed) return "danger";
|
||||||
|
if (status === CertificateSyncStatus.Syncing) return "neutral";
|
||||||
|
return "project";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSyncStatusText = (status?: CertificateSyncStatus | null) => {
|
||||||
|
if (status === CertificateSyncStatus.Succeeded) return "Synced";
|
||||||
|
if (status === CertificateSyncStatus.Failed) return "Failed";
|
||||||
|
if (status === CertificateSyncStatus.Syncing) return "Syncing";
|
||||||
|
if (status === CertificateSyncStatus.Pending) return "Pending";
|
||||||
|
return "Unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCertificateStatusVariant = (isExpired: boolean, isRevoked: boolean) => {
|
||||||
|
if (isRevoked) return "danger";
|
||||||
|
if (isExpired) return "danger";
|
||||||
|
return "success";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCertificateStatusText = (isExpired: boolean, isRevoked: boolean) => {
|
||||||
|
if (isRevoked) return "Revoked";
|
||||||
|
if (isExpired) return "Expired";
|
||||||
|
return "Active";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||||
|
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [certificateToDelete, setCertificateToDelete] = useState<{
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const { data, refetch: refetchSyncCertificates } = useListPkiSyncCertificates(pkiSync.id, {
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
limit: pageSize
|
||||||
|
});
|
||||||
|
const syncCertificates = data?.certificates || [];
|
||||||
|
const totalCount = data?.totalCount || 0;
|
||||||
|
const removeCertificatesFromSync = useRemoveCertificatesFromPkiSync();
|
||||||
|
|
||||||
|
const permissionSubject = subject(ProjectPermissionSub.PkiSyncs, {
|
||||||
|
subscriberId: pkiSync.subscriberId || ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemoveCertificate = async (certificateId: string) => {
|
||||||
|
try {
|
||||||
|
await removeCertificatesFromSync.mutateAsync({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateIds: [certificateId]
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetchSyncCertificates();
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Certificate removed from sync",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setCertificateToDelete(null);
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove certificate from sync",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (certificateId: string, displayName: string) => {
|
||||||
|
setCertificateToDelete({ id: certificateId, displayName });
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<h3 className="text-lg font-medium text-mineshaft-100">Certificates</h3>
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
ariaLabel="Edit certificates"
|
||||||
|
onClick={() => setIsManageModalOpen(true)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="w-2/8">SAN / CN</Th>
|
||||||
|
<Th className="w-3/16">Certificate Status</Th>
|
||||||
|
<Th className="w-3/16">Serial Number</Th>
|
||||||
|
<Th className="w-1/8">Sync Status</Th>
|
||||||
|
<Th className="w-1/8">Expires At</Th>
|
||||||
|
<Th className="w-1/8" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{syncCertificates.map((syncCert) => {
|
||||||
|
const isExpired = syncCert.certificateNotAfter
|
||||||
|
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||||
|
: false;
|
||||||
|
const isRevoked = syncCert.certificateStatus === "revoked";
|
||||||
|
|
||||||
|
// Calculate auto-renewal timeline
|
||||||
|
const hasAutoRenewal = Boolean(
|
||||||
|
syncCert.certificateRenewBeforeDays &&
|
||||||
|
syncCert.certificateRenewBeforeDays > 0 &&
|
||||||
|
!syncCert.certificateRenewalError &&
|
||||||
|
syncCert.certificateNotAfter
|
||||||
|
);
|
||||||
|
|
||||||
|
const daysUntilRenewal =
|
||||||
|
hasAutoRenewal && syncCert.certificateNotAfter
|
||||||
|
? (() => {
|
||||||
|
const expiryDate = new Date(syncCert.certificateNotAfter);
|
||||||
|
const renewalDate = new Date(
|
||||||
|
expiryDate.getTime() -
|
||||||
|
syncCert.certificateRenewBeforeDays! * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInMs = renewalDate.getTime() - now.getTime();
|
||||||
|
return Math.max(0, Math.ceil(diffInMs / (24 * 60 * 60 * 1000)));
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const { originalDisplayName } = getCertificateDisplayName(
|
||||||
|
{
|
||||||
|
altNames: syncCert.certificateAltNames,
|
||||||
|
commonName: syncCert.certificateCommonName
|
||||||
|
},
|
||||||
|
34,
|
||||||
|
"Unknown"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={syncCert.id}>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<CertificateDisplayName
|
||||||
|
cert={{
|
||||||
|
altNames: syncCert.certificateAltNames,
|
||||||
|
commonName: syncCert.certificateCommonName
|
||||||
|
}}
|
||||||
|
maxLength={34}
|
||||||
|
fallback="Unknown"
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={getCertificateStatusVariant(isExpired, isRevoked)}>
|
||||||
|
{getCertificateStatusText(isExpired, isRevoked)}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td className="max-w-0">
|
||||||
|
<div
|
||||||
|
className="truncate text-xs"
|
||||||
|
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
const serial = syncCert.certificateSerialNumber;
|
||||||
|
if (!serial || serial === "Unknown") return "Unknown";
|
||||||
|
if (serial.length <= 8) return serial;
|
||||||
|
return `${serial.substring(0, 4)}...${serial.substring(serial.length - 4)}`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{syncCert.lastSyncMessage &&
|
||||||
|
syncCert.syncStatus === CertificateSyncStatus.Failed ? (
|
||||||
|
<Tooltip content={syncCert.lastSyncMessage}>
|
||||||
|
<Badge variant="danger">Failed</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Badge variant={getSyncStatusVariant(syncCert.syncStatus)}>
|
||||||
|
{getSyncStatusText(syncCert.syncStatus)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span
|
||||||
|
className={`text-sm ${isExpired ? "text-red-400" : "text-bunker-300"}`}
|
||||||
|
>
|
||||||
|
{syncCert.certificateNotAfter
|
||||||
|
? new Date(syncCert.certificateNotAfter).toLocaleDateString()
|
||||||
|
: "Unknown"}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td className="flex items-center justify-end gap-2 pr-4">
|
||||||
|
{hasAutoRenewal && daysUntilRenewal !== null && (
|
||||||
|
<Tooltip content={`Auto-renews in ${daysUntilRenewal}d`}>
|
||||||
|
<div className="text-primary-500">
|
||||||
|
<FontAwesomeIcon icon={faClockRotateLeft} size="sm" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionPkiSyncActions.Edit}
|
||||||
|
a={permissionSubject}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="Remove certificate"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteClick(syncCert.certificateId, originalDisplayName)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{syncCertificates.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title="No certificates are part of this certificate sync"
|
||||||
|
icon={faCertificate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Pagination
|
||||||
|
count={totalCount}
|
||||||
|
page={currentPage}
|
||||||
|
perPage={pageSize}
|
||||||
|
onChangePage={(page: number) => setCurrentPage(page)}
|
||||||
|
onChangePerPage={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CertificateManagementModal
|
||||||
|
pkiSync={pkiSync}
|
||||||
|
isOpen={isManageModalOpen}
|
||||||
|
onClose={() => setIsManageModalOpen(false)}
|
||||||
|
onCertificatesUpdated={() => {
|
||||||
|
refetchSyncCertificates();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setCertificateToDelete(null);
|
||||||
|
}}
|
||||||
|
title="Remove Certificate from Sync"
|
||||||
|
subTitle={`Are you sure you want to remove "${certificateToDelete?.displayName}" from this PKI sync?`}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={async () => {
|
||||||
|
if (certificateToDelete) {
|
||||||
|
await handleRemoveCertificate(certificateToDelete.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
buttonText="Remove Certificate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
-6
@@ -11,12 +11,15 @@ import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionC
|
|||||||
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||||
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
import { PkiSync, TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||||
|
|
||||||
import { AzureKeyVaultPkiSyncDestinationSection } from "./PkiSyncDestinationSection/index";
|
import {
|
||||||
|
AwsCertificateManagerPkiSyncDestinationSection,
|
||||||
|
AzureKeyVaultPkiSyncDestinationSection
|
||||||
|
} from "./PkiSyncDestinationSection/index";
|
||||||
|
|
||||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<label className="text-sm text-bunker-300">{label}</label>
|
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||||
<div className="mt-1">{children}</div>
|
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -32,6 +35,9 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
|||||||
|
|
||||||
let DestinationComponents: ReactNode;
|
let DestinationComponents: ReactNode;
|
||||||
switch (destination) {
|
switch (destination) {
|
||||||
|
case PkiSync.AwsCertificateManager:
|
||||||
|
DestinationComponents = <AwsCertificateManagerPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||||
|
break;
|
||||||
case PkiSync.AzureKeyVault:
|
case PkiSync.AzureKeyVault:
|
||||||
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
DestinationComponents = <AzureKeyVaultPkiSyncDestinationSection pkiSync={pkiSync} />;
|
||||||
break;
|
break;
|
||||||
@@ -47,7 +53,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
<h3 className="font-medium text-mineshaft-100">Destination Configuration</h3>
|
<h3 className="text-lg font-medium text-mineshaft-100">Destination Configuration</h3>
|
||||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -62,7 +68,7 @@ export const PkiSyncDestinationSection = ({ pkiSync, onEditDestination }: Props)
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-wrap gap-8">
|
<div className="flex w-full flex-wrap gap-8 pt-2">
|
||||||
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
|
<GenericFieldLabel label={`${destinationDetails.name} Connection`}>
|
||||||
{pkiSync.appConnectionName || "Default Connection"}
|
{pkiSync.appConnectionName || "Default Connection"}
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||||
|
|
||||||
|
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||||
|
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pkiSync: TPkiSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsCertificateManagerPkiSyncDestinationSection = ({ pkiSync }: Props) => {
|
||||||
|
const region =
|
||||||
|
pkiSync.destinationConfig && "region" in pkiSync.destinationConfig
|
||||||
|
? pkiSync.destinationConfig.region
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return <GenericFieldLabel label="AWS Region">{region || "Not specified"}</GenericFieldLabel>;
|
||||||
|
};
|
||||||
+3
-3
@@ -2,9 +2,9 @@
|
|||||||
import { TAzureKeyVaultPkiSync } from "@app/hooks/api/pkiSyncs/types/azure-key-vault-sync";
|
import { TAzureKeyVaultPkiSync } from "@app/hooks/api/pkiSyncs/types/azure-key-vault-sync";
|
||||||
|
|
||||||
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
const GenericFieldLabel = ({ label, children }: { label: string; children: React.ReactNode }) => (
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<label className="text-sm text-bunker-300">{label}</label>
|
<p className="text-sm font-medium text-mineshaft-300">{label}</p>
|
||||||
<div className="mt-1">{children}</div>
|
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
@@ -1 +1,2 @@
|
|||||||
|
export { AwsCertificateManagerPkiSyncDestinationSection } from "./AwsCertificateManagerPkiSyncDestinationSection";
|
||||||
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
export { AzureKeyVaultPkiSyncDestinationSection } from "./AzureKeyVaultPkiSyncDestinationSection";
|
||||||
|
|||||||
+24
-28
@@ -21,9 +21,9 @@ const GenericFieldLabel = ({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<label className={`text-sm text-bunker-300 ${labelClassName || ""}`}>{label}</label>
|
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||||
<div className="mt-1">{children}</div>
|
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
<h3 className="font-medium text-mineshaft-100">Details</h3>
|
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
|
||||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -72,31 +72,27 @@ export const PkiSyncDetailsSection = ({ pkiSync, onEditDetails }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="pt-2">
|
||||||
<div className="space-y-3">
|
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Name">{name}</GenericFieldLabel>
|
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
||||||
<GenericFieldLabel label="Description">{description || "None"}</GenericFieldLabel>
|
{subscriber && (
|
||||||
<GenericFieldLabel label="Source Subscriber">
|
<GenericFieldLabel label="Source Subscriber">{subscriber.name}</GenericFieldLabel>
|
||||||
{subscriber ? subscriber.name : "Subscriber deleted"}
|
)}
|
||||||
|
{syncStatus && (
|
||||||
|
<GenericFieldLabel label="Status">
|
||||||
|
<PkiSyncStatusBadge status={syncStatus} />
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
{syncStatus && (
|
)}
|
||||||
<GenericFieldLabel label="Status">
|
{lastSyncedAt && (
|
||||||
<PkiSyncStatusBadge status={syncStatus} />
|
<GenericFieldLabel label="Last Synced">
|
||||||
</GenericFieldLabel>
|
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
||||||
)}
|
</GenericFieldLabel>
|
||||||
{lastSyncedAt && (
|
)}
|
||||||
<GenericFieldLabel label="Last Synced">
|
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
||||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, h:mm aaa")}
|
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
||||||
</GenericFieldLabel>
|
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">{failureMessage}</p>
|
||||||
)}
|
</GenericFieldLabel>
|
||||||
{syncStatus === PkiSyncStatus.Failed && failureMessage && (
|
)}
|
||||||
<GenericFieldLabel labelClassName="text-red" label="Last Sync Error">
|
|
||||||
<p className="rounded-sm bg-mineshaft-600 p-2 text-xs break-words">
|
|
||||||
{failureMessage}
|
|
||||||
</p>
|
|
||||||
</GenericFieldLabel>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+23
-11
@@ -3,13 +3,27 @@ import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { GenericFieldLabel } from "@app/components/secret-syncs";
|
|
||||||
import { IconButton } from "@app/components/v2";
|
import { IconButton } from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v3";
|
import { Badge } from "@app/components/v3";
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
import { TPkiSync } from "@app/hooks/api/pkiSyncs";
|
||||||
|
|
||||||
|
const GenericFieldLabel = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
labelClassName
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
labelClassName?: string;
|
||||||
|
}) => (
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className={`text-sm font-medium text-mineshaft-300 ${labelClassName || ""}`}>{label}</p>
|
||||||
|
<div className="text-sm text-mineshaft-300">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pkiSync: TPkiSync;
|
pkiSync: TPkiSync;
|
||||||
onEditOptions: VoidFunction;
|
onEditOptions: VoidFunction;
|
||||||
@@ -28,7 +42,7 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
<h3 className="font-medium text-mineshaft-100">Sync Options</h3>
|
<h3 className="text-lg font-medium text-mineshaft-100">Sync Options</h3>
|
||||||
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
<ProjectPermissionCan I={ProjectPermissionPkiSyncActions.Edit} a={permissionSubject}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -43,21 +57,19 @@ export const PkiSyncOptionsSection = ({ pkiSync, onEditOptions }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="pt-1">
|
||||||
<div className="space-y-3">
|
{/* Hidden for now - Import certificates functionality disabled
|
||||||
{/* Hidden for now - Import certificates functionality disabled
|
|
||||||
<GenericFieldLabel label="Certificate Import">
|
<GenericFieldLabel label="Certificate Import">
|
||||||
<Badge variant={canImportCertificates ? "success" : "danger"}>
|
<Badge variant={canImportCertificates ? "success" : "danger"}>
|
||||||
{canImportCertificates ? "Enabled" : "Disabled"}
|
{canImportCertificates ? "Enabled" : "Disabled"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
*/}
|
*/}
|
||||||
<GenericFieldLabel label="Certificate Removal">
|
<GenericFieldLabel label="Inactive Certificate Removal" labelClassName="mb-1">
|
||||||
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
<Badge variant={canRemoveCertificates ? "success" : "danger"}>
|
||||||
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
{canRemoveCertificates ? "Enabled" : "Disabled"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</GenericFieldLabel>
|
</GenericFieldLabel>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
|
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
|
||||||
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
|
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
|
||||||
|
export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection";
|
||||||
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
|
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
|
||||||
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
|
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
|
||||||
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
|
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
|
||||||
|
|||||||
@@ -2,17 +2,20 @@ import { useState } from "react";
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
|
|
||||||
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
||||||
|
import { CertificatesTab } from "./components/CertificatesTab";
|
||||||
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
||||||
|
import { PkiCollectionsTab } from "./components/PkiCollectionsTab";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
CertificateProfiles = "profiles",
|
CertificateProfiles = "profiles",
|
||||||
CertificateTemplatesV2 = "templates-v2"
|
CertificateTemplatesV2 = "templates-v2",
|
||||||
|
Certificates = "certificates",
|
||||||
|
PkiCollections = "pki-collections"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PoliciesPage = () => {
|
export const PoliciesPage = () => {
|
||||||
@@ -25,59 +28,54 @@ export const PoliciesPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectPermissionCan
|
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||||
I={ProjectPermissionActions.Read}
|
<Helmet>
|
||||||
a={ProjectPermissionSub.CertificateAuthorities}
|
<title>{t("common.head-title", { title: "Certificate Management" })}</title>
|
||||||
>
|
</Helmet>
|
||||||
{(isAllowed) => {
|
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||||
if (!isAllowed) {
|
<PageHeader
|
||||||
return (
|
scope={ProjectType.CertificateManager}
|
||||||
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
|
title="Certificate Management"
|
||||||
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
|
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
|
||||||
<p>You don't have permission to access certificate policies.</p>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<Tabs
|
||||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
orientation="vertical"
|
||||||
<Helmet>
|
value={activeTab}
|
||||||
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
|
onValueChange={(value) => setActiveTab(value as TabSections)}
|
||||||
</Helmet>
|
>
|
||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
<TabList>
|
||||||
<PageHeader
|
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
||||||
scope={ProjectType.CertificateManager}
|
Certificate Profiles
|
||||||
title="Certificate Policies"
|
</Tab>
|
||||||
description="Manage certificate templates and profiles for unified certificate issuance"
|
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
||||||
/>
|
Certificate Templates
|
||||||
|
</Tab>
|
||||||
|
<Tab variant="project" value={TabSections.Certificates}>
|
||||||
|
Certificates
|
||||||
|
</Tab>
|
||||||
|
<Tab variant="project" value={TabSections.PkiCollections}>
|
||||||
|
Certificate Collections
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
<Tabs
|
<TabPanel value={TabSections.CertificateProfiles}>
|
||||||
orientation="vertical"
|
<CertificateProfilesTab />
|
||||||
value={activeTab}
|
</TabPanel>
|
||||||
onValueChange={(value) => setActiveTab(value as TabSections)}
|
|
||||||
>
|
|
||||||
<TabList>
|
|
||||||
<Tab variant="project" value={TabSections.CertificateProfiles}>
|
|
||||||
Certificate Profiles
|
|
||||||
</Tab>
|
|
||||||
<Tab variant="project" value={TabSections.CertificateTemplatesV2}>
|
|
||||||
Certificate Templates
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanel value={TabSections.CertificateProfiles}>
|
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||||
<CertificateProfilesTab />
|
<CertificateTemplatesV2Tab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
<TabPanel value={TabSections.Certificates}>
|
||||||
<CertificateTemplatesV2Tab />
|
<CertificatesTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
|
||||||
</div>
|
<TabPanel value={TabSections.PkiCollections}>
|
||||||
</div>
|
<PkiCollectionsTab />
|
||||||
);
|
</TabPanel>
|
||||||
}}
|
</Tabs>
|
||||||
</ProjectPermissionCan>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -418,7 +418,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
name="enrollmentType"
|
name="enrollmentType"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Enrollment Type"
|
label="Enrollment Method"
|
||||||
isRequired
|
isRequired
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
|||||||
+3
-6
@@ -29,8 +29,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
projectId: currentProject?.id || "",
|
projectId: currentProject?.id || "",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
includeConfigs: true,
|
includeConfigs: true
|
||||||
includeMetrics: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const profiles = data?.certificateProfiles || [];
|
const profiles = data?.certificateProfiles || [];
|
||||||
@@ -42,10 +41,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Enrollment Type</Th>
|
<Th>Enrollment Method</Th>
|
||||||
<Th>Issuing CA</Th>
|
<Th>Issuing CA</Th>
|
||||||
<Th>Certificate Template</Th>
|
<Th>Certificate Template</Th>
|
||||||
<Th>Certificates</Th>
|
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -67,10 +65,9 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Enrollment Type</Th>
|
<Th>Enrollment Method</Th>
|
||||||
<Th>Issuing CA</Th>
|
<Th>Issuing CA</Th>
|
||||||
<Th>Certificate Template</Th>
|
<Th>Certificate Template</Th>
|
||||||
<Th>Certificates</Th>
|
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
|
|||||||
+2
-44
@@ -33,43 +33,6 @@ import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
|||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||||
|
|
||||||
const MetricsBadges = ({
|
|
||||||
metrics
|
|
||||||
}: {
|
|
||||||
metrics?: {
|
|
||||||
totalCertificates: number;
|
|
||||||
activeCertificates: number;
|
|
||||||
expiringCertificates: number;
|
|
||||||
expiredCertificates: number;
|
|
||||||
revokedCertificates: number;
|
|
||||||
};
|
|
||||||
}) => {
|
|
||||||
if (!metrics) {
|
|
||||||
return <Badge variant="warning">No metrics</Badge>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metrics.totalCertificates === 0) {
|
|
||||||
return <Badge variant="warning">No certificates</Badge>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{metrics.activeCertificates > 0 && (
|
|
||||||
<Badge variant="success">{metrics.activeCertificates} active</Badge>
|
|
||||||
)}
|
|
||||||
{metrics.expiringCertificates > 0 && (
|
|
||||||
<Badge variant="warning">{metrics.expiringCertificates} expiring</Badge>
|
|
||||||
)}
|
|
||||||
{metrics.expiredCertificates > 0 && (
|
|
||||||
<Badge variant="danger">{metrics.expiredCertificates} expired</Badge>
|
|
||||||
)}
|
|
||||||
{metrics.revokedCertificates > 0 && (
|
|
||||||
<Badge variant="danger">{metrics.revokedCertificates} revoked</Badge>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: TCertificateProfile;
|
profile: TCertificateProfile;
|
||||||
onEditProfile: (profile: TCertificateProfile) => void;
|
onEditProfile: (profile: TCertificateProfile) => void;
|
||||||
@@ -118,8 +81,8 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
|
|
||||||
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
||||||
const config = {
|
const config = {
|
||||||
api: { variant: "success" as const, label: "API" },
|
api: { variant: "ghost" as const, label: "API" },
|
||||||
est: { variant: "warning" as const, label: "EST" }
|
est: { variant: "ghost" as const, label: "EST" }
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const configKey = Object.keys(config).includes(enrollmentType)
|
const configKey = Object.keys(config).includes(enrollmentType)
|
||||||
@@ -153,11 +116,6 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
{templateData?.name || profile.certificateTemplateId}
|
{templateData?.name || profile.certificateTemplateId}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
<MetricsBadges metrics={profile.metrics} />
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
<Td className="text-right">
|
<Td className="text-right">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { CertificatesSection } from "../../../CertificatesPage/components/CertificatesSection";
|
||||||
|
|
||||||
|
export const CertificatesTab = () => {
|
||||||
|
return <CertificatesSection />;
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CertificatesTab } from "./CertificatesTab";
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import { PkiCollectionSection } from "../../../AlertingPage/components/PkiCollectionSection";
|
||||||
|
|
||||||
|
export const PkiCollectionsTab = () => {
|
||||||
|
return <PkiCollectionSection />;
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
||||||
|
export { CertificatesTab } from "./CertificatesTab";
|
||||||
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||||
|
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||||
|
|||||||
+3
-1
@@ -633,7 +633,9 @@ export const OAuthCallbackPage = () => {
|
|||||||
connectionName: data.connection.name,
|
connectionName: data.connection.name,
|
||||||
...(data.returnUrl.includes("integrations")
|
...(data.returnUrl.includes("integrations")
|
||||||
? {
|
? {
|
||||||
selectedTab: IntegrationsListPageTabs.SecretSyncs
|
selectedTab: localStorage.getItem("pkiSyncFormData")
|
||||||
|
? IntegrationsListPageTabs.PkiSyncs
|
||||||
|
: IntegrationsListPageTabs.SecretSyncs
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user