mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
PKI Syncs revamp
This commit is contained in:
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@@ -62,6 +62,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateSyncs,
|
||||
TCertificateSyncsInsert,
|
||||
TCertificateSyncsUpdate,
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate,
|
||||
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
|
||||
TPkiSubscribersUpdate
|
||||
>;
|
||||
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
||||
[TableName.CertificateSync]: KnexOriginal.CompositeTableType<
|
||||
TCertificateSyncs,
|
||||
TCertificateSyncsInsert,
|
||||
TCertificateSyncsUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
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);
|
||||
}
|
||||
23
backend/src/db/schemas/certificate-syncs.ts
Normal file
23
backend/src/db/schemas/certificate-syncs.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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()
|
||||
});
|
||||
|
||||
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-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-syncs";
|
||||
export * from "./certificate-template-est-configs";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
|
||||
@@ -161,6 +161,7 @@ export enum TableName {
|
||||
AppConnection = "app_connections",
|
||||
SecretSync = "secret_syncs",
|
||||
PkiSync = "pki_syncs",
|
||||
CertificateSync = "certificate_syncs",
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
|
||||
@@ -172,6 +172,7 @@ import { internalCertificateAuthorityServiceFactory } from "@app/services/certif
|
||||
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
||||
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
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 { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
@@ -1060,6 +1061,7 @@ export const registerRoutes = async (
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||
const certificateSyncDAL = certificateSyncDALFactory(db);
|
||||
|
||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||
@@ -2020,7 +2022,8 @@ export const registerRoutes = async (
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL
|
||||
certificateAuthorityCertDAL,
|
||||
certificateSyncDAL
|
||||
});
|
||||
|
||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||
@@ -2131,6 +2134,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
@@ -2142,7 +2146,10 @@ export const registerRoutes = async (
|
||||
certificateProfileDAL,
|
||||
certificateTemplateV2Service,
|
||||
internalCaService: internalCertificateAuthorityService,
|
||||
permissionService
|
||||
permissionService,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||
@@ -2188,7 +2195,8 @@ export const registerRoutes = async (
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
licenseService,
|
||||
pkiSyncQueue
|
||||
pkiSyncQueue,
|
||||
certificateSyncDAL
|
||||
});
|
||||
|
||||
const pkiTemplateService = pkiTemplatesServiceFactory({
|
||||
|
||||
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
|
||||
syncOptions?: Record<string, unknown>;
|
||||
description?: string;
|
||||
isAutoSyncEnabled?: boolean;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
connectionId?: string;
|
||||
@@ -35,7 +35,7 @@ export const registerSyncPkiEndpoints = ({
|
||||
syncOptions?: Record<string, unknown>;
|
||||
description?: string;
|
||||
isAutoSyncEnabled?: boolean;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
syncOptions: {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
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";
|
||||
|
||||
const PkiSyncSchema = z.object({
|
||||
@@ -60,7 +61,8 @@ const PkiSyncSchema = z.object({
|
||||
name: z.string()
|
||||
})
|
||||
.nullable()
|
||||
.optional()
|
||||
.optional(),
|
||||
hasCertificate: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PkiSyncOptionsSchema = z.object({
|
||||
@@ -76,6 +78,24 @@ const PkiSyncOptionsSchema = z.object({
|
||||
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(),
|
||||
certificateStatus: z.string().optional(),
|
||||
certificateNotBefore: z.date().optional(),
|
||||
certificateNotAfter: z.date().optional(),
|
||||
pkiSyncName: z.string().optional(),
|
||||
pkiSyncDestination: z.string().optional()
|
||||
});
|
||||
|
||||
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@@ -111,7 +131,8 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "List all the PKI Syncs for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1)
|
||||
projectId: z.string().trim().min(1),
|
||||
certificateId: z.string().uuid().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
||||
@@ -120,11 +141,11 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
query: { projectId, certificateId },
|
||||
permission
|
||||
} = 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({
|
||||
...req.auditLogInfo,
|
||||
@@ -179,4 +200,167 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
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 result = await server.services.pkiSync.listPkiSyncCertificates(
|
||||
{ pkiSyncId, offset, limit },
|
||||
req.permission
|
||||
);
|
||||
|
||||
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNC,
|
||||
metadata: {
|
||||
syncId: pkiSyncId,
|
||||
destination: pkiSync.destination
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
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 = await server.services.pkiSync.addCertificatesToPkiSync(
|
||||
{ pkiSyncId, certificateIds },
|
||||
req.permission
|
||||
);
|
||||
|
||||
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSync.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 result = await server.services.pkiSync.removeCertificatesFromPkiSync(
|
||||
{ pkiSyncId, certificateIds },
|
||||
req.permission
|
||||
);
|
||||
|
||||
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSync.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1195,8 +1195,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
|
||||
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),
|
||||
forPkiSync: z.coerce
|
||||
.boolean()
|
||||
.default(false)
|
||||
.optional()
|
||||
.describe("Retrieve only certificates available for PKI sync")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
259
backend/src/services/certificate-sync/certificate-sync-dal.ts
Normal file
259
backend/src/services/certificate-sync/certificate-sync-dal.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
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,
|
||||
certificateIds: string[],
|
||||
tx?: Knex
|
||||
): Promise<TCertificateSyncs[]> => {
|
||||
try {
|
||||
const insertData = certificateIds.map((certificateId) => ({
|
||||
pkiSyncId,
|
||||
certificateId,
|
||||
syncStatus: CertificateSyncStatus.Pending
|
||||
}));
|
||||
|
||||
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;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
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("status").withSchema(TableName.Certificate).as("certificateStatus"),
|
||||
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
|
||||
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
|
||||
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;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
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,6 @@
|
||||
export enum CertificateSyncStatus {
|
||||
Pending = "pending",
|
||||
Syncing = "syncing",
|
||||
Succeeded = "succeeded",
|
||||
Failed = "failed"
|
||||
}
|
||||
@@ -133,7 +133,18 @@ describe("CertificateV3Service", () => {
|
||||
certificateProfileDAL: mockCertificateProfileDAL,
|
||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||
internalCaService: mockInternalCaService,
|
||||
permissionService: mockPermissionService
|
||||
permissionService: mockPermissionService,
|
||||
certificateSyncDAL: {
|
||||
findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]),
|
||||
addCertificates: vi.fn().mockResolvedValue([]),
|
||||
removeCertificates: vi.fn().mockResolvedValue(0)
|
||||
},
|
||||
pkiSyncDAL: {
|
||||
find: vi.fn().mockResolvedValue([])
|
||||
},
|
||||
pkiSyncQueue: {
|
||||
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ import {
|
||||
mapEnumsForValidation,
|
||||
normalizeDateForApi
|
||||
} 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 { replaceCertificateInSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
|
||||
import {
|
||||
TCertificateFromProfileResponse,
|
||||
TCertificateOrderResponse,
|
||||
@@ -72,6 +76,12 @@ type TCertificateV3ServiceFactoryDep = {
|
||||
>;
|
||||
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
"findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates"
|
||||
>;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
};
|
||||
|
||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||
@@ -328,7 +338,10 @@ export const certificateV3ServiceFactory = ({
|
||||
certificateProfileDAL,
|
||||
certificateTemplateV2Service,
|
||||
internalCaService,
|
||||
permissionService
|
||||
permissionService,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TCertificateV3ServiceFactoryDep) => {
|
||||
const issueCertificateFromProfile = async ({
|
||||
profileId,
|
||||
@@ -872,6 +885,8 @@ export const certificateV3ServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
await replaceCertificateInSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
@@ -883,6 +898,12 @@ export const certificateV3ServiceFactory = ({
|
||||
};
|
||||
});
|
||||
|
||||
await triggerAutoSyncForCertificate(renewalResult.newCert.id, {
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: renewalResult.certificate,
|
||||
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TCertificates } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
@@ -60,11 +62,13 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
.where(`${TableName.Project}.id`, projectId);
|
||||
|
||||
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) {
|
||||
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();
|
||||
@@ -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 ({
|
||||
limit,
|
||||
offset
|
||||
@@ -159,7 +266,7 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const findWithPrivateKeyInfo = async (
|
||||
filter: Partial<TCertificates>,
|
||||
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
|
||||
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
|
||||
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||
try {
|
||||
@@ -167,8 +274,18 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
.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(filter);
|
||||
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"));
|
||||
|
||||
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);
|
||||
@@ -197,10 +314,13 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
...certificateOrm,
|
||||
countCertificatesInProject,
|
||||
countActiveCertificatesForSync,
|
||||
countCertificatesForPkiSubscriber,
|
||||
findLatestActiveCertForSubscriber,
|
||||
findAllActiveCertsForSubscriber,
|
||||
findExpiredSyncedCertificates,
|
||||
findActiveCertificatesByIds,
|
||||
findActiveCertificatesForSync,
|
||||
findCertificatesEligibleForRenewal,
|
||||
findWithPrivateKeyInfo
|
||||
};
|
||||
|
||||
@@ -18,12 +18,13 @@ import { TCertificateAuthorityDALFactory } from "@app/services/certificate-autho
|
||||
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||
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 { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
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 { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
@@ -57,6 +58,7 @@ type TCertificateServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
};
|
||||
@@ -76,6 +78,7 @@ export const certificateServiceFactory = ({
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TCertificateServiceFactoryDep) => {
|
||||
@@ -166,10 +169,12 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||
|
||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
||||
if (cert.pkiSubscriberId) {
|
||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
||||
}
|
||||
// Trigger auto sync for PKI syncs connected to this certificate
|
||||
await triggerAutoSyncForCertificate(cert.id, {
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
return {
|
||||
deletedCert
|
||||
@@ -235,10 +240,12 @@ export const certificateServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
||||
if (cert.pkiSubscriberId) {
|
||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
||||
}
|
||||
// Trigger auto sync for PKI syncs connected to this certificate
|
||||
await triggerAutoSyncForCertificate(cert.id, {
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
// Note: External CA revocation handling would go here for supported CA types
|
||||
// Currently, only internal CAs and ACME CAs support revocation
|
||||
|
||||
@@ -4,6 +4,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
@@ -101,7 +102,8 @@ const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM
|
||||
|
||||
const validateCertificateIdentification = (
|
||||
certName: string,
|
||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }
|
||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string },
|
||||
alternativeCertNames?: string[]
|
||||
): boolean => {
|
||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
||||
return false;
|
||||
@@ -113,12 +115,15 @@ const validateCertificateIdentification = (
|
||||
return false;
|
||||
}
|
||||
|
||||
return certNameTag.Value === certName;
|
||||
};
|
||||
if (certNameTag.Value === certName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
if (alternativeCertNames && alternativeCertNames.includes(certNameTag.Value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateCertificateNameSchema = (schema: string): void => {
|
||||
@@ -174,6 +179,11 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
|
||||
return sanitizedCertificateName;
|
||||
};
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const getAwsAcmClient = async (
|
||||
connectionId: string,
|
||||
region: AWSRegion,
|
||||
@@ -392,48 +402,59 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
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 setCertificates: CertificateImportRequest[] = [];
|
||||
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean } | undefined;
|
||||
const preserveArn = syncOptions?.preserveArn ?? true;
|
||||
|
||||
Object.entries(certificateMap).forEach(([certName, certData]) => {
|
||||
const { cert, privateKey, certificateChain } = certData;
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
|
||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
||||
const { cert, privateKey, certificateChain, alternativeNames } = certData;
|
||||
|
||||
try {
|
||||
validateCertificateContent(cert, privateKey);
|
||||
} catch (validationError) {
|
||||
throw new PkiSyncError({
|
||||
message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
||||
shouldRetry: false,
|
||||
context: {
|
||||
certificateName,
|
||||
certName
|
||||
}
|
||||
const errorMessage = validationError instanceof Error ? validationError.message : String(validationError);
|
||||
validationErrors.push({
|
||||
name: certName,
|
||||
error: `Certificate validation failed: ${errorMessage}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldUpdateCert) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn: existingCert?.arn
|
||||
});
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
let existingArn: string | undefined;
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) => {
|
||||
return validateCertificateIdentification(certName, acmCert, alternativeNames);
|
||||
});
|
||||
|
||||
if (existingCert?.arn && preserveArn) {
|
||||
// When preserveArn is true, reuse the existing ARN
|
||||
existingArn = existingCert.arn;
|
||||
}
|
||||
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn
|
||||
});
|
||||
});
|
||||
|
||||
// Identify expired/removed certificates that need to be cleaned up from ACM
|
||||
const certificatesToRemove = Object.values(acmCertificates)
|
||||
.filter((acmCert) => {
|
||||
if (!acmCert.arn || !acmCert.Tags) {
|
||||
@@ -445,8 +466,22 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
const isActive = activeCertificateNames.includes(certNameTag.Value);
|
||||
return !isActive;
|
||||
const isActive = activeCertificateNames.some((activeCertName) => {
|
||||
const certData = certificateMap[activeCertName];
|
||||
if (!certData) return false;
|
||||
|
||||
return validateCertificateIdentification(activeCertName, acmCert, certData.alternativeNames);
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!preserveArn && isActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((acmCert) => acmCert.arn!)
|
||||
.filter((arn) => arn);
|
||||
@@ -457,14 +492,17 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
try {
|
||||
const importParams: AWS.ACM.ImportCertificateRequest = {
|
||||
Certificate: cert,
|
||||
PrivateKey: privateKey,
|
||||
Tags: [
|
||||
PrivateKey: privateKey
|
||||
};
|
||||
|
||||
if (!existingArn) {
|
||||
importParams.Tags = [
|
||||
{
|
||||
Key: INFISICAL_CERTIFICATE_TAG,
|
||||
Value: key
|
||||
}
|
||||
]
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
if (certificateChain && certificateChain.trim().length > 0) {
|
||||
importParams.CertificateChain = certificateChain;
|
||||
@@ -478,6 +516,39 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
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(), 100);
|
||||
});
|
||||
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { key, name, success: true, response };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
@@ -520,15 +591,21 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
const details: {
|
||||
failedUploads?: 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) {
|
||||
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";
|
||||
|
||||
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 {
|
||||
@@ -577,7 +654,14 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
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 certificateArnsToRemove: string[] = [];
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
|
||||
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
preserveArn: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -28,6 +29,9 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
|
||||
const testName = schema
|
||||
.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");
|
||||
|
||||
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
||||
@@ -43,7 +47,7 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||
},
|
||||
{
|
||||
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),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
||||
subscriberId: z.string().optional(),
|
||||
subscriberId: z.string().nullish(),
|
||||
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({
|
||||
@@ -71,7 +76,7 @@ export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||
isAutoSyncEnabled: z.boolean().optional(),
|
||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
||||
subscriberId: z.string().optional(),
|
||||
subscriberId: z.string().nullish(),
|
||||
connectionId: z.string().optional()
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface SyncCertificatesResult {
|
||||
details?: {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
validationErrors?: Array<{ name: string; error: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
|
||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
|
||||
subscriberId: z.string().optional(),
|
||||
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({
|
||||
|
||||
@@ -194,6 +194,7 @@ export const PkiSyncFns = {
|
||||
failedUploads?: Array<{ name: string; error: string }>;
|
||||
failedRemovals?: Array<{ name: string; error: string }>;
|
||||
skippedCertificates?: Array<{ name: string; reason: string }>;
|
||||
validationErrors?: Array<{ name: string; error: string }>;
|
||||
};
|
||||
}> => {
|
||||
switch (pkiSync.destination) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AxiosError } from "axios";
|
||||
import { Job } from "bullmq";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
import { TCertificates } from "@app/db/schemas";
|
||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
@@ -25,6 +26,7 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||
import { PkiSyncStatus } from "./pki-sync-enums";
|
||||
import { PkiSyncError } from "./pki-sync-errors";
|
||||
@@ -57,12 +59,21 @@ type TPkiSyncQueueFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
certificateDAL: Pick<
|
||||
TCertificateDALFactory,
|
||||
"findLatestActiveCertForSubscriber" | "findAllActiveCertsForSubscriber" | "create"
|
||||
| "findLatestActiveCertForSubscriber"
|
||||
| "findAllActiveCertsForSubscriber"
|
||||
| "findActiveCertificatesByIds"
|
||||
| "create"
|
||||
| "findById"
|
||||
| "find"
|
||||
>;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
"findCertificateIdsByPkiSyncId" | "updateSyncStatus" | "bulkUpdateSyncStatus"
|
||||
>;
|
||||
};
|
||||
|
||||
type PkiSyncActionJob = Job<
|
||||
@@ -93,7 +104,8 @@ export const pkiSyncQueueFactory = ({
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL
|
||||
certificateAuthorityCertDAL,
|
||||
certificateSyncDAL
|
||||
}: TPkiSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -153,25 +165,39 @@ export const pkiSyncQueueFactory = ({
|
||||
|
||||
const $getInfisicalCertificates = async (
|
||||
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
|
||||
): Promise<TCertificateMap> => {
|
||||
const { projectId, subscriberId } = pkiSync;
|
||||
|
||||
if (!subscriberId) {
|
||||
throw new PkiSyncError({
|
||||
message: "Invalid PKI Sync source configuration: subscriber no longer exists. Please update source subscriber.",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
): Promise<{ certificateMap: TCertificateMap; certificateMetadata: Map<string, { id: string; name: string }> }> => {
|
||||
const { projectId, subscriberId, id: pkiSyncId } = pkiSync;
|
||||
|
||||
const certificateMap: TCertificateMap = {};
|
||||
const certificateMetadata = new Map<string, { id: string; name: string }>();
|
||||
let certificates: Array<{ id: string; projectId: string; caCertId?: string | null }> = [];
|
||||
|
||||
try {
|
||||
// Get all active certificates for the subscriber (not just the latest)
|
||||
const certificates = await certificateDAL.findAllActiveCertsForSubscriber({
|
||||
subscriberId
|
||||
});
|
||||
if (subscriberId) {
|
||||
const subscriberCertificates = await certificateDAL.findAllActiveCertsForSubscriber({
|
||||
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) {
|
||||
const cert = certificate as TCertificates;
|
||||
try {
|
||||
// Get the certificate body and decrypt the certificate data
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
|
||||
@@ -246,19 +272,44 @@ export const pkiSyncQueueFactory = ({
|
||||
|
||||
if (certificateNameSchema) {
|
||||
const environment = "global";
|
||||
certificateName = handlebars.compile(certificateNameSchema)({
|
||||
const templateData = {
|
||||
certificateId: certificate.id.replace(/-/g, ""),
|
||||
profileId: cert.profileId?.replace(/-/g, "") || certificate.id.replace(/-/g, ""),
|
||||
commonName: cert.commonName || "",
|
||||
friendlyName: cert.friendlyName || "",
|
||||
environment
|
||||
});
|
||||
};
|
||||
certificateName = handlebars.compile(certificateNameSchema)(templateData);
|
||||
} 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] = {
|
||||
cert: certificatePem,
|
||||
privateKey: certPrivateKey || "",
|
||||
certificateChain
|
||||
certificateChain,
|
||||
alternativeNames
|
||||
};
|
||||
|
||||
certificateMetadata.set(certificateName, {
|
||||
id: certificate.id,
|
||||
name: certificateName
|
||||
});
|
||||
} else {
|
||||
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
|
||||
}
|
||||
@@ -281,7 +332,7 @@ export const pkiSyncQueueFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
return certificateMap;
|
||||
return { certificateMap, certificateMetadata };
|
||||
};
|
||||
|
||||
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
|
||||
@@ -348,12 +399,17 @@ export const pkiSyncQueueFactory = ({
|
||||
|
||||
try {
|
||||
const {
|
||||
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
|
||||
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
|
||||
} = pkiSync;
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
if (!appConnection) {
|
||||
throw new Error(`App connection not found: ${connectionId}`);
|
||||
}
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: appConnectionProjectId
|
||||
});
|
||||
@@ -366,7 +422,18 @@ export const pkiSyncQueueFactory = ({
|
||||
}
|
||||
} 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: "running",
|
||||
message: "Syncing certificate to destination"
|
||||
}));
|
||||
|
||||
if (statusUpdates.length > 0) {
|
||||
await certificateSyncDAL.bulkUpdateSyncStatus(statusUpdates);
|
||||
}
|
||||
|
||||
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
|
||||
appConnectionDAL,
|
||||
@@ -384,6 +451,60 @@ export const pkiSyncQueueFactory = ({
|
||||
"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: "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: "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: "failed",
|
||||
message: `Failed to sync certificate: ${failure.error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postSyncUpdates.length > 0) {
|
||||
await certificateSyncDAL.bulkUpdateSyncStatus(postSyncUpdates);
|
||||
}
|
||||
|
||||
isSynced = true;
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
@@ -550,17 +671,22 @@ export const pkiSyncQueueFactory = ({
|
||||
|
||||
try {
|
||||
const {
|
||||
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
|
||||
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
|
||||
} = pkiSync;
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
if (!appConnection) {
|
||||
throw new Error(`App connection not found: ${connectionId}`);
|
||||
}
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId,
|
||||
encryptedCredentials,
|
||||
encryptedCredentials: appConnection.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: appConnectionProjectId
|
||||
});
|
||||
|
||||
const certificateMap = await $getInfisicalCertificates(pkiSync);
|
||||
const { certificateMap } = await $getInfisicalCertificates(pkiSync);
|
||||
|
||||
await PkiSyncFns.removeCertificates(
|
||||
{
|
||||
|
||||
@@ -10,17 +10,23 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
|
||||
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||
import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
|
||||
import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
|
||||
import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
|
||||
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
||||
import {
|
||||
TAddCertificatesToPkiSyncDTO,
|
||||
TCreatePkiSyncDTO,
|
||||
TDeletePkiSyncDTO,
|
||||
TFindPkiSyncByIdDTO,
|
||||
TListPkiSyncCertificatesDTO,
|
||||
TListPkiSyncsByProjectId,
|
||||
TPkiSync,
|
||||
TPkiSyncCertificate,
|
||||
TRemoveCertificatesFromPkiSyncDTO,
|
||||
TTriggerPkiSyncImportCertificatesByIdDTO,
|
||||
TTriggerPkiSyncRemoveCertificatesByIdDTO,
|
||||
TTriggerPkiSyncSyncCertificatesByIdDTO,
|
||||
@@ -42,6 +48,16 @@ type TPkiSyncServiceFactoryDep = {
|
||||
TPkiSyncDALFactory,
|
||||
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
||||
>;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "findByPkiSyncId"
|
||||
| "findByCertificateId"
|
||||
| "findCertificateIdsByPkiSyncId"
|
||||
| "addCertificates"
|
||||
| "removeCertificates"
|
||||
| "removeAllCertificatesFromSync"
|
||||
| "findWithDetails"
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@@ -56,6 +72,7 @@ export type TPkiSyncServiceFactory = ReturnType<typeof pkiSyncServiceFactory>;
|
||||
|
||||
export const pkiSyncServiceFactory = ({
|
||||
pkiSyncDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
@@ -72,7 +89,8 @@ export const pkiSyncServiceFactory = ({
|
||||
syncOptions = {},
|
||||
subscriberId,
|
||||
connectionId,
|
||||
projectId
|
||||
projectId,
|
||||
certificateIds = []
|
||||
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<TPkiSync> => {
|
||||
@@ -128,6 +146,10 @@ export const pkiSyncServiceFactory = ({
|
||||
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
|
||||
});
|
||||
|
||||
if (certificateIds.length > 0) {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, certificateIds);
|
||||
}
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
|
||||
}
|
||||
@@ -152,7 +174,8 @@ export const pkiSyncServiceFactory = ({
|
||||
destinationConfig,
|
||||
syncOptions,
|
||||
subscriberId,
|
||||
connectionId
|
||||
connectionId,
|
||||
certificateIds
|
||||
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<TPkiSync> => {
|
||||
@@ -221,6 +244,13 @@ export const pkiSyncServiceFactory = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (certificateIds !== undefined) {
|
||||
await certificateSyncDAL.removeAllCertificatesFromSync(id);
|
||||
if (certificateIds.length > 0) {
|
||||
await certificateSyncDAL.addCertificates(id, certificateIds);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
|
||||
name,
|
||||
description,
|
||||
@@ -266,7 +296,7 @@ export const pkiSyncServiceFactory = ({
|
||||
};
|
||||
|
||||
const listPkiSyncsByProjectId = async (
|
||||
{ projectId }: TListPkiSyncsByProjectId,
|
||||
{ projectId, certificateId }: TListPkiSyncsByProjectId,
|
||||
actor: OrgServiceActor
|
||||
): Promise<TPkiSync[]> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
@@ -282,6 +312,29 @@ export const pkiSyncServiceFactory = ({
|
||||
|
||||
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[];
|
||||
};
|
||||
|
||||
@@ -433,6 +486,136 @@ export const pkiSyncServiceFactory = ({
|
||||
return listPkiSyncOptions();
|
||||
};
|
||||
|
||||
const addCertificatesToPkiSync = async (
|
||||
{ pkiSyncId, certificateIds }: Omit<TAddCertificatesToPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
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
|
||||
});
|
||||
|
||||
let subscriber;
|
||||
if (pkiSync.subscriberId) {
|
||||
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
subscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
);
|
||||
|
||||
const addedCertificates = await certificateSyncDAL.addCertificates(pkiSyncId, certificateIds);
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||
}
|
||||
|
||||
return addedCertificates;
|
||||
};
|
||||
|
||||
const removeCertificatesFromPkiSync = async (
|
||||
{ pkiSyncId, certificateIds }: Omit<TRemoveCertificatesFromPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
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
|
||||
});
|
||||
|
||||
let subscriber;
|
||||
if (pkiSync.subscriberId) {
|
||||
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
subscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
);
|
||||
|
||||
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||
}
|
||||
|
||||
return { removedCount };
|
||||
};
|
||||
|
||||
const listPkiSyncCertificates = async (
|
||||
{ pkiSyncId, offset = 0, limit = 20 }: Omit<TListPkiSyncCertificatesDTO, "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<{ certificates: TPkiSyncCertificate[]; totalCount: number }> => {
|
||||
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
|
||||
});
|
||||
|
||||
let subscriber;
|
||||
if (pkiSync.subscriberId) {
|
||||
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Read,
|
||||
subscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
|
||||
: 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,
|
||||
certificateStatus: detail.certificateStatus || undefined,
|
||||
certificateNotBefore: detail.certificateNotBefore || undefined,
|
||||
certificateNotAfter: detail.certificateNotAfter || undefined,
|
||||
pkiSyncName: detail.pkiSyncName || undefined,
|
||||
pkiSyncDestination: detail.pkiSyncDestination || undefined
|
||||
}));
|
||||
|
||||
return { certificates, totalCount };
|
||||
};
|
||||
|
||||
return {
|
||||
createPkiSync,
|
||||
updatePkiSync,
|
||||
@@ -442,6 +625,9 @@ export const pkiSyncServiceFactory = ({
|
||||
triggerPkiSyncSyncCertificatesById,
|
||||
triggerPkiSyncImportCertificatesById,
|
||||
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 { 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 { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||
@@ -70,7 +71,10 @@ export type TPkiSyncListItem = TPkiSync & {
|
||||
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[] }
|
||||
>;
|
||||
|
||||
export type TCreatePkiSyncDTO = {
|
||||
name: string;
|
||||
@@ -79,9 +83,10 @@ export type TCreatePkiSyncDTO = {
|
||||
isAutoSyncEnabled?: boolean;
|
||||
destinationConfig: Record<string, unknown>;
|
||||
syncOptions?: Record<string, unknown>;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
connectionId: string;
|
||||
projectId: string;
|
||||
certificateIds?: string[];
|
||||
auditLogInfo: AuditLogInfo;
|
||||
resourceMetadata?: ResourceMetadataDTO;
|
||||
};
|
||||
@@ -94,8 +99,9 @@ export type TUpdatePkiSyncDTO = {
|
||||
isAutoSyncEnabled?: boolean;
|
||||
destinationConfig?: Record<string, unknown>;
|
||||
syncOptions?: Record<string, unknown>;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
connectionId?: string;
|
||||
certificateIds?: string[];
|
||||
auditLogInfo: AuditLogInfo;
|
||||
resourceMetadata?: ResourceMetadataDTO;
|
||||
};
|
||||
@@ -108,6 +114,7 @@ export type TDeletePkiSyncDTO = {
|
||||
|
||||
export type TListPkiSyncsByProjectId = {
|
||||
projectId: string;
|
||||
certificateId?: string;
|
||||
};
|
||||
|
||||
export type TFindPkiSyncByIdDTO = {
|
||||
@@ -133,6 +140,45 @@ export type TTriggerPkiSyncRemoveCertificatesByIdDTO = {
|
||||
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;
|
||||
certificate?: {
|
||||
serialNumber: string;
|
||||
commonName: string;
|
||||
status: string;
|
||||
notBefore: Date;
|
||||
notAfter: Date;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
||||
|
||||
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
||||
|
||||
@@ -25,3 +28,67 @@ export const triggerAutoSyncForSubscriber = async (
|
||||
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
|
||||
});
|
||||
|
||||
const pkiSyncs = allPkiSyncs.filter((sync) => pkiSyncIds.includes(sync.id));
|
||||
|
||||
const syncPromises = pkiSyncs.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 replaceCertificateInSyncs = async (
|
||||
oldCertificateId: string,
|
||||
newCertificateId: string,
|
||||
dependencies: {
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
"findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates"
|
||||
>;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(oldCertificateId);
|
||||
|
||||
if (pkiSyncIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacementPromises = pkiSyncIds.map(async (pkiSyncId) => {
|
||||
await dependencies.certificateSyncDAL.removeCertificates(pkiSyncId, [oldCertificateId], tx);
|
||||
await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx);
|
||||
});
|
||||
|
||||
await Promise.all(replacementPromises);
|
||||
|
||||
logger.info(
|
||||
`Successfully replaced certificate ${oldCertificateId} with ${newCertificateId} in ${pkiSyncIds.length} PKI sync(s)`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to replace certificate ${oldCertificateId} with ${newCertificateId} in syncs:`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,7 +154,14 @@ type TProjectServiceFactoryDep = {
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject" | "findWithPrivateKeyInfo">;
|
||||
certificateDAL: Pick<
|
||||
TCertificateDALFactory,
|
||||
| "find"
|
||||
| "countCertificatesInProject"
|
||||
| "findWithPrivateKeyInfo"
|
||||
| "findActiveCertificatesForSync"
|
||||
| "countActiveCertificatesForSync"
|
||||
>;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||
@@ -915,6 +922,7 @@ export const projectServiceFactory = ({
|
||||
offset = 0,
|
||||
friendlyName,
|
||||
commonName,
|
||||
forPkiSync = false,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@@ -938,20 +946,35 @@ export const projectServiceFactory = ({
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const certificates = await certificateDAL.findWithPrivateKeyInfo(
|
||||
{
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["notAfter", "desc"]] }
|
||||
);
|
||||
const certificates = forPkiSync
|
||||
? await certificateDAL.findActiveCertificatesForSync(
|
||||
{
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit }
|
||||
)
|
||||
: await certificateDAL.findWithPrivateKeyInfo(
|
||||
{
|
||||
projectId,
|
||||
...(friendlyName && { friendlyName }),
|
||||
...(commonName && { commonName })
|
||||
},
|
||||
{ offset, limit, sort: [["notAfter", "desc"]] }
|
||||
);
|
||||
|
||||
const count = await certificateDAL.countCertificatesInProject({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
const count = forPkiSync
|
||||
? await certificateDAL.countActiveCertificatesForSync({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
})
|
||||
: await certificateDAL.countCertificatesInProject({
|
||||
projectId,
|
||||
friendlyName,
|
||||
commonName
|
||||
});
|
||||
|
||||
return {
|
||||
certificates,
|
||||
|
||||
@@ -142,6 +142,7 @@ export type TListProjectCertsDTO = {
|
||||
limit: number;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
forPkiSync?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListProjectAlertsDTO = TProjectPermission;
|
||||
|
||||
434
frontend/src/components/pki-syncs/CertificateManagementModal.tsx
Normal file
434
frontend/src/components/pki-syncs/CertificateManagementModal.tsx
Normal file
@@ -0,0 +1,434 @@
|
||||
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 {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
EmptyState,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
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>
|
||||
|
||||
{allCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates found">
|
||||
{searchTerm
|
||||
? "No certificates match your search criteria."
|
||||
: "No certificates available for sync."}
|
||||
</EmptyState>
|
||||
) : (
|
||||
<>
|
||||
<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">Common Name</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/6">Status</Th>
|
||||
<Th className="w-2/6">Expires</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);
|
||||
|
||||
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">
|
||||
<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">
|
||||
<div className="truncate" title={cert.commonName}>
|
||||
{cert.commonName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate font-mono text-xs text-bunker-300"
|
||||
title={cert.serialNumber}
|
||||
>
|
||||
{cert.serialNumber}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<Badge
|
||||
variant={
|
||||
cert.status === CertStatus.ACTIVE && !isExpired
|
||||
? "success"
|
||||
: "danger"
|
||||
}
|
||||
>
|
||||
{(() => {
|
||||
if (isRevoked) return "Revoked";
|
||||
if (isExpired) return "Expired";
|
||||
return cert.status === CertStatus.ACTIVE ? "Active" : cert.status;
|
||||
})()}
|
||||
</Badge>
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -13,11 +13,11 @@ import { PKI_SYNC_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { PkiSync, TPkiSync, useCreatePkiSync, usePkiSyncOption } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { PkiSyncFormSchema, TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
import { PkiSyncCertificatesFields } from "./PkiSyncCertificatesFields";
|
||||
import { PkiSyncDestinationFields } from "./PkiSyncDestinationFields";
|
||||
import { PkiSyncDetailsFields } from "./PkiSyncDetailsFields";
|
||||
import { PkiSyncOptionsFields } from "./PkiSyncOptionsFields";
|
||||
import { PkiSyncReviewFields } from "./PkiSyncReviewFields";
|
||||
import { PkiSyncSourceFields } from "./PkiSyncSourceFields";
|
||||
|
||||
type Props = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
@@ -26,10 +26,10 @@ type Props = {
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
|
||||
{ name: "Source", key: "source", fields: ["subscriberId"] },
|
||||
{ name: "Destination", key: "destination", fields: ["connection", "destinationConfig"] },
|
||||
{ name: "Sync Options", key: "options", fields: ["syncOptions"] },
|
||||
{ name: "Details", key: "details", fields: ["name", "description"] },
|
||||
{ name: "Certificates", key: "certificates", fields: ["certificateIds"] },
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
@@ -49,34 +49,46 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
||||
defaultValues: {
|
||||
destination,
|
||||
isAutoSyncEnabled: false,
|
||||
certificateIds: [],
|
||||
syncOptions: {
|
||||
canImportCertificates: false,
|
||||
canRemoveCertificates: false,
|
||||
preserveArn: true,
|
||||
certificateNameSchema: syncOption?.defaultCertificateNameSchema
|
||||
}
|
||||
} as Partial<TPkiSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
const onSubmit = async ({ connection, destinationConfig, ...formData }: TPkiSyncForm) => {
|
||||
const onSubmit = async ({
|
||||
connection,
|
||||
destinationConfig,
|
||||
certificateIds,
|
||||
...formData
|
||||
}: TPkiSyncForm) => {
|
||||
try {
|
||||
const pkiSync = await createPkiSync.mutateAsync({
|
||||
...formData,
|
||||
connectionId: connection.id,
|
||||
projectId: currentProject.id,
|
||||
destinationConfig
|
||||
destinationConfig,
|
||||
certificateIds: certificateIds || []
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully added ${destinationName} Certificate Sync`,
|
||||
text: `Successfully created ${destinationName} Certificate Sync${
|
||||
certificateIds && certificateIds.length > 0
|
||||
? ` with ${certificateIds.length} certificate(s)`
|
||||
: ""
|
||||
}`,
|
||||
type: "success"
|
||||
});
|
||||
onComplete(pkiSync);
|
||||
} catch (err: Error | unknown) {
|
||||
console.error(err);
|
||||
console.error("PKI sync creation failed:", err);
|
||||
setShowConfirmation(false);
|
||||
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",
|
||||
type: "error"
|
||||
});
|
||||
@@ -184,9 +196,6 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<PkiSyncSourceFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncDestinationFields />
|
||||
</Tab.Panel>
|
||||
@@ -200,8 +209,8 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
||||
<FormControl
|
||||
helperText={
|
||||
value
|
||||
? "Certificates will automatically be synced when changes occur in the source subscriber."
|
||||
: "Certificates will not automatically be synced when changes occur in the source subscriber. You can still trigger syncs manually."
|
||||
? "Certificates will automatically be synced when changes occur in the selected certificates."
|
||||
: "Certificates will not automatically be synced when changes occur. You can still trigger syncs manually."
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
@@ -223,6 +232,9 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
||||
<Tab.Panel>
|
||||
<PkiSyncDetailsFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncCertificatesFields />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<PkiSyncReviewFields />
|
||||
</Tab.Panel>
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
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>
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates selected" icon={faPlus} />
|
||||
) : (
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-2/5">Common Name</Th>
|
||||
<Th className="w-2/5">Serial Number</Th>
|
||||
<Th className="w-1/5">Remove</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{selectedCertificates.map((cert) => (
|
||||
<Tr key={cert.id}>
|
||||
<Td className="max-w-xs truncate">{cert.commonName}</Td>
|
||||
<Td className="font-mono text-xs text-bunker-300">
|
||||
{cert.serialNumber}
|
||||
</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>
|
||||
</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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -95,6 +95,49 @@ 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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.certificateNameSchema"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useFormContext } from "react-hook-form";
|
||||
import { Badge, GenericFieldLabel } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
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";
|
||||
|
||||
@@ -11,18 +11,24 @@ export const PkiSyncReviewFields = () => {
|
||||
const { watch } = useFormContext<TPkiSyncForm>();
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const { data: pkiSubscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
||||
const { data } = useListWorkspaceCertificates({
|
||||
projectId: currentProject?.id || "",
|
||||
offset: 0,
|
||||
limit: 100
|
||||
});
|
||||
|
||||
const getSubscriberName = (subscriberId?: string) => {
|
||||
const subscriber = pkiSubscribers.find((sub) => sub.id === subscriberId);
|
||||
return subscriber?.name || "Unknown";
|
||||
const certificates = data?.certificates || [];
|
||||
|
||||
const getSelectedCertificates = (certificateIds?: string[]) => {
|
||||
if (!certificateIds || certificateIds.length === 0) return [];
|
||||
return certificates.filter((cert) => certificateIds.includes(cert.id));
|
||||
};
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
connection,
|
||||
subscriberId,
|
||||
certificateIds,
|
||||
syncOptions,
|
||||
destination,
|
||||
destinationConfig,
|
||||
@@ -30,17 +36,28 @@ export const PkiSyncReviewFields = () => {
|
||||
} = watch();
|
||||
|
||||
const destinationName = PKI_SYNC_MAP[destination].name;
|
||||
const selectedCertificates = getSelectedCertificates(certificateIds);
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<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 className="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<GenericFieldLabel label="PKI Subscriber">
|
||||
{getSubscriberName(subscriberId)}
|
||||
</GenericFieldLabel>
|
||||
<div>
|
||||
{selectedCertificates.length === 0 ? (
|
||||
<span className="text-bunker-400">No certificates selected</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{selectedCertificates.map((cert) => (
|
||||
<div key={cert.id} className="text-sm">
|
||||
{cert.commonName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
const AwsCertificateManagerSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(false),
|
||||
preserveArn: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -53,7 +53,8 @@ export const BasePkiSyncSchema = <T extends AnyZodObject | undefined = undefined
|
||||
.max(255, "Name must be less than 255 characters"),
|
||||
description: z.string().optional(),
|
||||
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({
|
||||
id: z.string().uuid("Invalid connection ID format"),
|
||||
name: z.string().max(255, "Connection name must be less than 255 characters")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { CertStatus } from "./enums";
|
||||
export {
|
||||
useDeleteCert,
|
||||
useImportCertificate,
|
||||
|
||||
@@ -28,6 +28,7 @@ export * from "./organization";
|
||||
export * from "./pkiAlerts";
|
||||
export * from "./pkiCollections";
|
||||
export * from "./pkiSubscriber";
|
||||
export * from "./pkiSyncs";
|
||||
export * from "./projects";
|
||||
export * from "./projectUserAdditionalPrivilege";
|
||||
export * from "./rateLimit";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PamResourceType, PamSessionStatus } from "../enums";
|
||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||
|
||||
export * from "./postgres-resource";
|
||||
export * from "./mysql-resource";
|
||||
export * from "./postgres-resource";
|
||||
|
||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||
|
||||
|
||||
@@ -9,3 +9,10 @@ export enum PkiSyncStatus {
|
||||
Succeeded = "succeeded",
|
||||
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 { 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 = {
|
||||
all: ["pki-sync"] as const,
|
||||
options: () => [...pkiSyncKeys.all, "options"] 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) =>
|
||||
[...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 = (
|
||||
@@ -41,9 +52,14 @@ export const usePkiSyncOption = (destination: PkiSync) => {
|
||||
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", {
|
||||
params: { projectId }
|
||||
params
|
||||
});
|
||||
|
||||
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 = (
|
||||
{ syncId, projectId }: { syncId: string; projectId: string },
|
||||
options?: Omit<
|
||||
@@ -82,3 +119,33 @@ export const useGetPkiSync = (
|
||||
...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 { PkiSyncStatus } from "../enums";
|
||||
import { CertificateSyncStatus, PkiSyncStatus } from "../enums";
|
||||
|
||||
export type RootPkiSyncOptions = {
|
||||
canImportCertificates: boolean;
|
||||
@@ -43,4 +43,23 @@ export type TRootPkiSync = {
|
||||
} | null;
|
||||
appConnectionName?: 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;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
pkiSyncName?: string;
|
||||
pkiSyncDestination?: string;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ type TCreatePkiSyncDTOBase = {
|
||||
certificateNameSchema?: string;
|
||||
};
|
||||
isAutoSyncEnabled: boolean;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
certificateIds?: string[];
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -664,17 +664,26 @@ export const useListWorkspaceCas = ({
|
||||
export const useListWorkspaceCertificates = ({
|
||||
projectId,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
friendlyName,
|
||||
commonName,
|
||||
forPkiSync
|
||||
}: {
|
||||
projectId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
forPkiSync?: boolean;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: projectKeys.specificProjectCertificates({
|
||||
projectId,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
friendlyName,
|
||||
commonName,
|
||||
forPkiSync
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams({
|
||||
@@ -682,6 +691,16 @@ export const useListWorkspaceCertificates = ({
|
||||
limit: String(limit)
|
||||
});
|
||||
|
||||
if (friendlyName) {
|
||||
params.append("friendlyName", friendlyName);
|
||||
}
|
||||
if (commonName) {
|
||||
params.append("commonName", commonName);
|
||||
}
|
||||
if (forPkiSync) {
|
||||
params.append("forPkiSync", "true");
|
||||
}
|
||||
|
||||
const {
|
||||
data: { certificates, totalCount }
|
||||
} = await apiRequest.get<{ certificates: TCertificate[]; totalCount: number }>(
|
||||
@@ -693,7 +712,8 @@ export const useListWorkspaceCertificates = ({
|
||||
|
||||
return { certificates, totalCount };
|
||||
},
|
||||
enabled: Boolean(projectId)
|
||||
enabled: Boolean(projectId),
|
||||
placeholderData: (previousData) => previousData
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -39,12 +39,22 @@ export const projectKeys = {
|
||||
specificProjectCertificates: ({
|
||||
projectId,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
friendlyName,
|
||||
commonName,
|
||||
forPkiSync
|
||||
}: {
|
||||
projectId: string;
|
||||
offset: 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,
|
||||
getProjectPkiSubscribers: (projectId: string) =>
|
||||
[{ projectId }, "project-pki-subscribers"] as const,
|
||||
|
||||
@@ -51,25 +51,9 @@ export const PkiManagerLayout = () => {
|
||||
params={{
|
||||
projectId: currentProject.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Policies</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>
|
||||
<Tab value={isActive ? "selected" : ""}>Certificate Management</Tab>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
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 { useProject } from "@app/context";
|
||||
import {
|
||||
useAddCertificatesToPkiSync,
|
||||
useListPkiSyncsWithCertificate,
|
||||
useRemoveCertificatesFromPkiSync
|
||||
} from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
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 { 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);
|
||||
};
|
||||
|
||||
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="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}>
|
||||
Create a PKI sync first to manage certificate syncing.
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
|
||||
<EmptyState title="No PKI syncs found" icon={faSearch}>
|
||||
No PKI syncs match your search criteria. Try a different search term.
|
||||
</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={sync.destination.replace(/-/g, " ")}
|
||||
>
|
||||
{sync.destination.replace(/-/g, " ")}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { CertificateCertModal } from "./CertificateCertModal";
|
||||
import { CertificateImportModal } from "./CertificateImportModal";
|
||||
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||
import { CertificateManagePkiSyncsModal } from "./CertificateManagePkiSyncsModal";
|
||||
import { CertificateManageRenewalModal } from "./CertificateManageRenewalModal";
|
||||
import { CertificateModal } from "./CertificateModal";
|
||||
import { CertificateRenewalModal } from "./CertificateRenewalModal";
|
||||
@@ -37,7 +38,8 @@ export const CertificatesSection = () => {
|
||||
"deleteCertificate",
|
||||
"revokeCertificate",
|
||||
"manageRenewal",
|
||||
"renewCertificate"
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
] as const);
|
||||
|
||||
const onRemoveCertificateSubmit = async (serialNumber: string) => {
|
||||
@@ -105,6 +107,10 @@ export const CertificatesSection = () => {
|
||||
<CertificateManageRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRenewalModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateManagePkiSyncsModal
|
||||
popUp={popUp.managePkiSyncs}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificate.isOpen}
|
||||
title={`Are you sure you want to remove the certificate ${
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
faEllipsis,
|
||||
faEye,
|
||||
faFileExport,
|
||||
faLink,
|
||||
faQuestionCircle,
|
||||
faRedo,
|
||||
faTrash
|
||||
@@ -39,12 +40,13 @@ import {
|
||||
useProject,
|
||||
useSubscription
|
||||
} 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 { CaCapability, CaType } from "@app/hooks/api/ca/enums";
|
||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||
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 { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
|
||||
@@ -152,7 +154,8 @@ type Props = {
|
||||
"revokeCertificate",
|
||||
"certificateCert",
|
||||
"manageRenewal",
|
||||
"renewCertificate"
|
||||
"renewCertificate",
|
||||
"managePkiSyncs"
|
||||
]
|
||||
>,
|
||||
data?: {
|
||||
@@ -488,6 +491,31 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
{/* PKI Sync management - only for active certificates */}
|
||||
{certificate.status === CertStatus.ACTIVE && (
|
||||
<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} />}
|
||||
>
|
||||
PKI Syncs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
{/* Only show revoke button if CA supports revocation */}
|
||||
{(() => {
|
||||
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"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -59,9 +59,9 @@ export const PkiCollectionPage = () => {
|
||||
});
|
||||
handlePopUpClose("deletePkiCollection");
|
||||
navigate({
|
||||
to: "/projects/cert-management/$projectId/certificates",
|
||||
to: "/projects/cert-management/$projectId/policies",
|
||||
params: {
|
||||
projectId
|
||||
projectId: params.projectId
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
@@ -77,9 +77,9 @@ export const PkiCollectionPage = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/projects/cert-management/$projectId/certificates"
|
||||
to="/projects/cert-management/$projectId/policies"
|
||||
params={{
|
||||
projectId
|
||||
projectId: params.projectId
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
|
||||
@@ -5,13 +5,9 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
CaStatus,
|
||||
useAddItemToPkiCollection,
|
||||
useListWorkspaceCas,
|
||||
useListWorkspaceCertificates
|
||||
} from "@app/hooks/api";
|
||||
import { CaStatus, useAddItemToPkiCollection, useListWorkspaceCas } from "@app/hooks/api";
|
||||
import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants";
|
||||
import { useListWorkspaceCertificates } from "@app/hooks/api/projects";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Collections",
|
||||
link: linkOptions({
|
||||
to: "/projects/cert-management/$projectId/certificates",
|
||||
to: "/projects/cert-management/$projectId/policies",
|
||||
params: {
|
||||
projectId: params.projectId
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import {
|
||||
PkiSyncActionTriggers,
|
||||
PkiSyncAuditLogsSection,
|
||||
PkiSyncCertificatesSection,
|
||||
PkiSyncDestinationSection,
|
||||
PkiSyncDetailsSection,
|
||||
PkiSyncOptionsSection,
|
||||
PkiSyncSourceSection
|
||||
PkiSyncOptionsSection
|
||||
} from "./components";
|
||||
|
||||
const PageContent = () => {
|
||||
@@ -62,7 +62,6 @@ const PageContent = () => {
|
||||
const destinationDetails = PKI_SYNC_MAP[pkiSync.destination];
|
||||
|
||||
const handleEditDetails = () => handlePopUpOpen("editSync", PkiSyncEditFields.Details);
|
||||
const handleEditSource = () => handlePopUpOpen("editSync", PkiSyncEditFields.Source);
|
||||
const handleEditOptions = () => handlePopUpOpen("editSync", PkiSyncEditFields.Options);
|
||||
const handleEditDestination = () => handlePopUpOpen("editSync", PkiSyncEditFields.Destination);
|
||||
|
||||
@@ -103,7 +102,6 @@ const PageContent = () => {
|
||||
<div className="flex justify-center">
|
||||
<div className="mr-4 flex w-72 flex-col gap-4">
|
||||
<PkiSyncDetailsSection pkiSync={pkiSync} onEditDetails={handleEditDetails} />
|
||||
<PkiSyncSourceSection pkiSync={pkiSync} onEditSource={handleEditSource} />
|
||||
<PkiSyncOptionsSection pkiSync={pkiSync} onEditOptions={handleEditOptions} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
@@ -111,6 +109,7 @@ const PageContent = () => {
|
||||
pkiSync={pkiSync}
|
||||
onEditDestination={handleEditDestination}
|
||||
/>
|
||||
<PkiSyncCertificatesSection pkiSync={pkiSync} />
|
||||
<PkiSyncAuditLogsSection pkiSync={pkiSync} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faEdit, faPlus, 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 {
|
||||
Badge,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
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 "primary";
|
||||
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";
|
||||
};
|
||||
|
||||
export const PkiSyncCertificatesSection = ({ pkiSync }: Props) => {
|
||||
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
|
||||
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"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to remove certificate from sync",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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="font-medium text-mineshaft-100">Certificates ({totalCount})</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>
|
||||
{syncCertificates.length === 0 ? (
|
||||
<EmptyState title="No certificates" icon={faPlus}>
|
||||
No certificates are currently synced with this PKI destination.
|
||||
</EmptyState>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Common Name</Th>
|
||||
<Th className="w-1/3">Serial Number</Th>
|
||||
<Th className="w-1/9">Status</Th>
|
||||
<Th className="w-1/9">Expires</Th>
|
||||
<Th className="w-1/9">Actions</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{syncCertificates.map((syncCert) => {
|
||||
const isExpired = syncCert.certificateNotAfter
|
||||
? new Date(syncCert.certificateNotAfter) < new Date()
|
||||
: false;
|
||||
|
||||
return (
|
||||
<Tr key={syncCert.id}>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate"
|
||||
title={syncCert.certificateCommonName || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateCommonName || "Unknown"}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="max-w-0">
|
||||
<div
|
||||
className="truncate text-xs"
|
||||
title={syncCert.certificateSerialNumber || "Unknown"}
|
||||
>
|
||||
{syncCert.certificateSerialNumber || "Unknown"}
|
||||
</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">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSyncActions.Edit}
|
||||
a={permissionSubject}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="Remove certificate"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handleRemoveCertificate(syncCert.certificateId)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</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();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export { PkiSyncActionTriggers } from "./PkiSyncActionTriggers";
|
||||
export { PkiSyncAuditLogsSection } from "./PkiSyncAuditLogsSection";
|
||||
export { PkiSyncCertificatesSection } from "./PkiSyncCertificatesSection";
|
||||
export { PkiSyncDestinationSection } from "./PkiSyncDestinationSection";
|
||||
export { PkiSyncDetailsSection } from "./PkiSyncDetailsSection";
|
||||
export { PkiSyncOptionsSection } from "./PkiSyncOptionsSection";
|
||||
|
||||
@@ -2,17 +2,20 @@ import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
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 { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
||||
import { CertificatesTab } from "./components/CertificatesTab";
|
||||
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
||||
import { PkiCollectionsTab } from "./components/PkiCollectionsTab";
|
||||
|
||||
enum TabSections {
|
||||
CertificateProfiles = "profiles",
|
||||
CertificateTemplatesV2 = "templates-v2"
|
||||
CertificateTemplatesV2 = "templates-v2",
|
||||
Certificates = "certificates",
|
||||
PkiCollections = "pki-collections"
|
||||
}
|
||||
|
||||
export const PoliciesPage = () => {
|
||||
@@ -25,59 +28,54 @@ export const PoliciesPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.CertificateAuthorities}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
if (!isAllowed) {
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl text-center">
|
||||
<p>You don't have permission to access certificate policies.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Management" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Management"
|
||||
description="Manage certificate templates, profiles, certificates, and PKI collections for unified certificate issuance"
|
||||
/>
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Policies"
|
||||
description="Manage certificate templates and profiles for unified certificate issuance"
|
||||
/>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
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>
|
||||
<Tab variant="project" value={TabSections.Certificates}>
|
||||
Certificates
|
||||
</Tab>
|
||||
<Tab variant="project" value={TabSections.PkiCollections}>
|
||||
Certificate Collections
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
value={activeTab}
|
||||
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}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateProfiles}>
|
||||
<CertificateProfilesTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||
<CertificateTemplatesV2Tab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
<TabPanel value={TabSections.Certificates}>
|
||||
<CertificatesTab />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={TabSections.PkiCollections}>
|
||||
<PkiCollectionsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CertificatesSection } from "../../../CertificatesPage/components/CertificatesSection";
|
||||
|
||||
export const CertificatesTab = () => {
|
||||
return <CertificatesSection />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CertificatesTab } from "./CertificatesTab";
|
||||
@@ -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 { CertificatesTab } from "./CertificatesTab";
|
||||
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||
export { PkiCollectionsTab } from "./PkiCollectionsTab";
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
|
||||
type Props = {
|
||||
account?: TMySQLAccount;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, ModalClose } from "@app/components/v2";
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
} from "@app/hooks/api/pam";
|
||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
|
||||
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
|
||||
|
||||
type Props = {
|
||||
account?: TPostgresAccount;
|
||||
|
||||
@@ -114,7 +114,6 @@ import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/ro
|
||||
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
|
||||
import { Route as certManagerSettingsPageRouteImport } from './pages/cert-manager/SettingsPage/route'
|
||||
import { Route as certManagerPoliciesPageRouteImport } from './pages/cert-manager/PoliciesPage/route'
|
||||
import { Route as certManagerCertificatesPageRouteImport } from './pages/cert-manager/CertificatesPage/route'
|
||||
import { Route as certManagerCertificateAuthoritiesPageRouteImport } from './pages/cert-manager/CertificateAuthoritiesPage/route'
|
||||
import { Route as certManagerAlertingPageRouteImport } from './pages/cert-manager/AlertingPage/route'
|
||||
import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/route'
|
||||
@@ -1203,13 +1202,6 @@ const certManagerPoliciesPageRouteRoute =
|
||||
getParentRoute: () => certManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const certManagerCertificatesPageRouteRoute =
|
||||
certManagerCertificatesPageRouteImport.update({
|
||||
id: '/certificates',
|
||||
path: '/certificates',
|
||||
getParentRoute: () => certManagerLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const certManagerCertificateAuthoritiesPageRouteRoute =
|
||||
certManagerCertificateAuthoritiesPageRouteImport.update({
|
||||
id: '/certificate-authorities',
|
||||
@@ -2786,13 +2778,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof certManagerCertificateAuthoritiesPageRouteImport
|
||||
parentRoute: typeof certManagerLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates'
|
||||
path: '/certificates'
|
||||
fullPath: '/projects/cert-management/$projectId/certificates'
|
||||
preLoaderRoute: typeof certManagerCertificatesPageRouteImport
|
||||
parentRoute: typeof certManagerLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies'
|
||||
path: '/policies'
|
||||
@@ -4126,7 +4111,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsCertManagementProjectIdCertMa
|
||||
interface certManagerLayoutRouteChildren {
|
||||
certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute
|
||||
certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
|
||||
certManagerPoliciesPageRouteRoute: typeof certManagerPoliciesPageRouteRoute
|
||||
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
|
||||
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
|
||||
@@ -4147,7 +4131,6 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
|
||||
certManagerAlertingPageRouteRoute: certManagerAlertingPageRouteRoute,
|
||||
certManagerCertificateAuthoritiesPageRouteRoute:
|
||||
certManagerCertificateAuthoritiesPageRouteRoute,
|
||||
certManagerCertificatesPageRouteRoute: certManagerCertificatesPageRouteRoute,
|
||||
certManagerPoliciesPageRouteRoute: certManagerPoliciesPageRouteRoute,
|
||||
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
|
||||
projectAccessControlPageRouteCertManagerRoute:
|
||||
@@ -5061,7 +5044,6 @@ export interface FileRoutesByFullPath {
|
||||
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||
'/projects/cert-management/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
||||
'/projects/cert-management/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/policies': typeof certManagerPoliciesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
||||
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||
@@ -5294,7 +5276,6 @@ export interface FileRoutesByTo {
|
||||
'/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||
'/projects/cert-management/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
||||
'/projects/cert-management/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/policies': typeof certManagerPoliciesPageRouteRoute
|
||||
'/projects/cert-management/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
||||
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||
@@ -5538,7 +5519,6 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting': typeof certManagerAlertingPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates': typeof certManagerCertificatesPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies': typeof certManagerPoliciesPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
|
||||
|
||||
Reference in New Issue
Block a user