mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
PKI Syncs improvements
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier"))) {
|
||||
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||
t.text("externalIdentifier").nullable();
|
||||
t.index("externalIdentifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier")) {
|
||||
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||
t.dropIndex("externalIdentifier");
|
||||
t.dropColumn("externalIdentifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export const CertificateSyncsSchema = z.object({
|
||||
syncStatus: z.string().default("pending").nullable().optional(),
|
||||
lastSyncMessage: z.string().nullable().optional(),
|
||||
lastSyncedAt: z.date().nullable().optional(),
|
||||
externalIdentifier: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
@@ -426,6 +426,7 @@ export enum EventType {
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
GET_PKI_SYNCS = "get-pki-syncs",
|
||||
GET_PKI_SYNC = "get-pki-sync",
|
||||
GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates",
|
||||
CREATE_PKI_SYNC = "create-pki-sync",
|
||||
UPDATE_PKI_SYNC = "update-pki-sync",
|
||||
DELETE_PKI_SYNC = "delete-pki-sync",
|
||||
@@ -3161,6 +3162,16 @@ interface GetPkiSyncEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiSyncCertificatesEvent {
|
||||
type: EventType.GET_PKI_SYNC_CERTIFICATES;
|
||||
metadata: {
|
||||
syncId: string;
|
||||
count: number;
|
||||
certificateIds: string[];
|
||||
destination: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiSyncEvent {
|
||||
type: EventType.CREATE_PKI_SYNC;
|
||||
metadata: {
|
||||
@@ -4329,6 +4340,7 @@ export type Event =
|
||||
| SecretSyncRemoveSecretsEvent
|
||||
| GetPkiSyncsEvent
|
||||
| GetPkiSyncEvent
|
||||
| GetPkiSyncCertificatesEvent
|
||||
| CreatePkiSyncEvent
|
||||
| UpdatePkiSyncEvent
|
||||
| DeletePkiSyncEvent
|
||||
|
||||
@@ -2193,12 +2193,13 @@ export const registerRoutes = async (
|
||||
|
||||
const pkiSyncService = pkiSyncServiceFactory({
|
||||
pkiSyncDAL,
|
||||
certificateDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
licenseService,
|
||||
pkiSyncQueue,
|
||||
certificateSyncDAL
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const pkiTemplateService = pkiTemplatesServiceFactory({
|
||||
|
||||
@@ -93,8 +93,8 @@ const PkiSyncCertificateSchema = z.object({
|
||||
certificateStatus: z.string().optional(),
|
||||
certificateNotBefore: z.date().optional(),
|
||||
certificateNotAfter: z.date().optional(),
|
||||
certificateRenewBeforeDays: z.number().optional(),
|
||||
certificateRenewalError: z.string().optional(),
|
||||
certificateRenewBeforeDays: z.number().nullish(),
|
||||
certificateRenewalError: z.string().nullish(),
|
||||
pkiSyncName: z.string().optional(),
|
||||
pkiSyncDestination: z.string().optional()
|
||||
});
|
||||
@@ -233,26 +233,26 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { offset, limit } = req.query;
|
||||
|
||||
const result = await server.services.pkiSync.listPkiSyncCertificates(
|
||||
const { certificates, totalCount, pkiSyncInfo } = 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,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNC,
|
||||
type: EventType.GET_PKI_SYNC_CERTIFICATES,
|
||||
metadata: {
|
||||
syncId: pkiSyncId,
|
||||
destination: pkiSync.destination
|
||||
destination: pkiSyncInfo.destination,
|
||||
count: certificates.length,
|
||||
certificateIds: certificates.map((c) => c.certificateId)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return { certificates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -294,21 +294,19 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { certificateIds } = req.body;
|
||||
|
||||
const addedCertificates = await server.services.pkiSync.addCertificatesToPkiSync(
|
||||
const { addedCertificates, pkiSyncInfo } = 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,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSync.name
|
||||
name: pkiSyncInfo.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -344,26 +342,24 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { certificateIds } = req.body;
|
||||
|
||||
const result = await server.services.pkiSync.removeCertificatesFromPkiSync(
|
||||
const { removedCount, pkiSyncInfo } = 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,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSync.name
|
||||
name: pkiSyncInfo.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return { removedCount };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -192,7 +192,7 @@ export const castDbEntryToAzureAdCsCertificateAuthority = (
|
||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||
): TAzureAdCsCertificateAuthority & { credentials: unknown } => {
|
||||
if (!ca.externalCa?.id) {
|
||||
throw new BadRequestError({ message: "Malformed Azure AD Certificate Service certificate authority" });
|
||||
throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" });
|
||||
}
|
||||
|
||||
if (!ca.externalCa.dnsAppConnectionId) {
|
||||
@@ -776,7 +776,7 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||
if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) {
|
||||
throw new BadRequestError({ message: "CA is not an Azure AD Certificate Service CA" });
|
||||
throw new BadRequestError({ message: "CA is not an Active Directory Certificate Service CA" });
|
||||
}
|
||||
|
||||
const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { CaCapability, CaType } from "./certificate-authority-enums";
|
||||
|
||||
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
||||
[CaType.INTERNAL]: "Internal",
|
||||
[CaType.ACME]: "ACME",
|
||||
[CaType.AZURE_AD_CS]: "Azure AD Certificate Service"
|
||||
[CaType.ACME]: "ACME-compatible CA",
|
||||
[CaType.AZURE_AD_CS]: "Active Directory Certificate Service"
|
||||
};
|
||||
|
||||
export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||
|
||||
@@ -72,14 +72,15 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||
|
||||
const addCertificates = async (
|
||||
pkiSyncId: string,
|
||||
certificateIds: string[],
|
||||
certificateData: Array<{ certificateId: string; externalIdentifier?: string }>,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateSyncs[]> => {
|
||||
try {
|
||||
const insertData = certificateIds.map((certificateId) => ({
|
||||
const insertData = certificateData.map(({ certificateId, externalIdentifier }) => ({
|
||||
pkiSyncId,
|
||||
certificateId,
|
||||
syncStatus: CertificateSyncStatus.Pending
|
||||
syncStatus: CertificateSyncStatus.Pending,
|
||||
externalIdentifier
|
||||
}));
|
||||
|
||||
const docs = await (tx || db)(TableName.CertificateSync).insert(insertData).returning("*");
|
||||
@@ -184,7 +185,7 @@ export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
certificateRenewBeforeDays?: number;
|
||||
certificateRenewBeforeDays?: number | null;
|
||||
certificateRenewedByCertificateId?: string;
|
||||
certificateRenewalError?: string;
|
||||
pkiSyncName?: string;
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("CertificateV3Service", () => {
|
||||
certificateSyncDAL: {
|
||||
findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]),
|
||||
addCertificates: vi.fn().mockResolvedValue([]),
|
||||
removeCertificates: vi.fn().mockResolvedValue(0)
|
||||
findByPkiSyncAndCertificate: vi.fn().mockResolvedValue(null)
|
||||
},
|
||||
pkiSyncDAL: {
|
||||
find: vi.fn().mockResolvedValue([])
|
||||
|
||||
@@ -78,7 +78,7 @@ type TCertificateV3ServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
"findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates"
|
||||
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||
>;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
|
||||
@@ -92,43 +92,6 @@ const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): b
|
||||
return isAwsIssuedCertificate(certificate);
|
||||
};
|
||||
|
||||
const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => {
|
||||
if (!tags || !Array.isArray(tags)) {
|
||||
return undefined;
|
||||
}
|
||||
return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value);
|
||||
};
|
||||
|
||||
const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => {
|
||||
return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG);
|
||||
};
|
||||
|
||||
const validateCertificateIdentification = (
|
||||
certName: string,
|
||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string },
|
||||
alternativeCertNames?: string[]
|
||||
): boolean => {
|
||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const certNameTag = findInfisicalCertificateTag(existingCert.Tags);
|
||||
|
||||
if (!certNameTag || !certNameTag.Value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (certNameTag.Value === certName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alternativeCertNames && alternativeCertNames.includes(certNameTag.Value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateCertificateNameSchema = (schema: string): void => {
|
||||
if (!schema.includes("{{certificateId}}")) {
|
||||
throw new Error(
|
||||
@@ -185,7 +148,15 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "removeCertificates">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "removeCertificates"
|
||||
| "addCertificates"
|
||||
| "findByPkiSyncAndCertificate"
|
||||
| "updateSyncStatus"
|
||||
| "updateById"
|
||||
| "findByPkiSyncId"
|
||||
>;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||
};
|
||||
|
||||
@@ -418,16 +389,38 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
>;
|
||||
} = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
|
||||
const acmCertificatesByArn = new Map<string, (typeof acmCertificates)[string]>();
|
||||
Object.values(acmCertificates).forEach((acmCert) => {
|
||||
if (acmCert.arn) {
|
||||
acmCertificatesByArn.set(acmCert.arn, acmCert);
|
||||
}
|
||||
});
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
type SyncRecord = (typeof existingSyncRecords)[0];
|
||||
const syncRecordsByCertId = new Map<string, SyncRecord>();
|
||||
const syncRecordsByExternalId = new Map<string, SyncRecord>();
|
||||
|
||||
existingSyncRecords.forEach((record: SyncRecord) => {
|
||||
if (record.certificateId) {
|
||||
syncRecordsByCertId.set(record.certificateId, record);
|
||||
}
|
||||
if (record.externalIdentifier) {
|
||||
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||
}
|
||||
});
|
||||
|
||||
const setCertificates: CertificateImportRequest[] = [];
|
||||
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean; canRemoveCertificates?: boolean } | undefined;
|
||||
const preserveArn = syncOptions?.preserveArn ?? true;
|
||||
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||
|
||||
const activeExternalIdentifiers = new Set<string>();
|
||||
|
||||
for (const [certName, certData] of Object.entries(certificateMap)) {
|
||||
const { cert, privateKey, certificateChain, alternativeNames, certificateId } = certData;
|
||||
const { cert, privateKey, certificateChain, certificateId } = certData;
|
||||
|
||||
try {
|
||||
validateCertificateContent(cert, privateKey);
|
||||
@@ -441,7 +434,7 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preserveArn && certificateId) {
|
||||
if (preserveArn && certificateId && typeof certificateId === "string") {
|
||||
const certificate = await certificateDAL.findById(certificateId);
|
||||
if (certificate?.renewedByCertificateId) {
|
||||
// eslint-disable-next-line no-continue
|
||||
@@ -451,60 +444,120 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
let existingArn: string | undefined;
|
||||
let targetArn: string | undefined;
|
||||
let shouldCreateNew = false;
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) => {
|
||||
return validateCertificateIdentification(certName, acmCert, alternativeNames);
|
||||
});
|
||||
if (!certificateId || typeof certificateId !== "string") {
|
||||
shouldCreateNew = true;
|
||||
} else {
|
||||
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||
const isRenewal = !!currentCertificate?.renewedFromCertificateId;
|
||||
|
||||
if (existingCert?.arn && preserveArn) {
|
||||
// When preserveArn is true, reuse the existing ARN
|
||||
existingArn = existingCert.arn;
|
||||
if (isRenewal) {
|
||||
const currentSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||
const oldCertificateId = currentCertificate.renewedFromCertificateId;
|
||||
const oldSyncRecord = oldCertificateId ? syncRecordsByCertId.get(oldCertificateId) : undefined;
|
||||
|
||||
if (currentSyncRecord?.externalIdentifier) {
|
||||
const existingAcmCert = acmCertificatesByArn.get(currentSyncRecord.externalIdentifier);
|
||||
|
||||
if (existingAcmCert) {
|
||||
if (!preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||
shouldCreateNew = true;
|
||||
} else if (preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||
targetArn = currentSyncRecord.externalIdentifier;
|
||||
shouldCreateNew = true;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
|
||||
if (oldCertificateId && oldSyncRecord) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||
}
|
||||
} else {
|
||||
targetArn = currentSyncRecord.externalIdentifier;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
shouldCreateNew = false;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else if (preserveArn && oldSyncRecord?.externalIdentifier) {
|
||||
const existingAcmCert = acmCertificatesByArn.get(oldSyncRecord.externalIdentifier);
|
||||
|
||||
if (existingAcmCert) {
|
||||
targetArn = oldSyncRecord.externalIdentifier;
|
||||
shouldCreateNew = true;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
if (oldCertificateId) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||
if (existingSyncRecord?.externalIdentifier) {
|
||||
const existingAcmCert = acmCertificatesByArn.get(existingSyncRecord.externalIdentifier);
|
||||
if (existingAcmCert) {
|
||||
targetArn = existingSyncRecord.externalIdentifier;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
shouldCreateNew = false;
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn,
|
||||
certificateId
|
||||
});
|
||||
if (shouldCreateNew) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn: targetArn,
|
||||
certificateId: certificateId as string
|
||||
});
|
||||
}
|
||||
|
||||
if (targetArn) {
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
}
|
||||
}
|
||||
|
||||
const certificatesToRemove = canRemoveCertificates
|
||||
? Object.values(acmCertificates)
|
||||
.filter((acmCert) => {
|
||||
if (!acmCert.arn || !acmCert.Tags) {
|
||||
return false;
|
||||
const certificatesToRemove: string[] = [];
|
||||
|
||||
if (canRemoveCertificates) {
|
||||
existingSyncRecords.forEach((syncRecord) => {
|
||||
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||
const acmCert = acmCertificatesByArn.get(syncRecord.externalIdentifier);
|
||||
if (acmCert?.arn) {
|
||||
certificatesToRemove.push(acmCert.arn);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(acmCertificates).forEach((acmCert) => {
|
||||
if (acmCert.arn && acmCert.Tags) {
|
||||
const hasInfisicalTag = acmCert.Tags.some((tag) => tag.Key === INFISICAL_CERTIFICATE_TAG && tag.Value);
|
||||
|
||||
if (hasInfisicalTag) {
|
||||
const isTrackedInSyncRecords = existingSyncRecords.some(
|
||||
(record) => record.externalIdentifier === acmCert.arn
|
||||
);
|
||||
const isInActiveSet = activeExternalIdentifiers.has(acmCert.arn);
|
||||
if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(acmCert.arn)) {
|
||||
certificatesToRemove.push(acmCert.arn);
|
||||
}
|
||||
|
||||
const certNameTag = findInfisicalCertificateTag(acmCert.Tags);
|
||||
if (!certNameTag || !certNameTag.Value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const uploadResults = await executeWithConcurrencyLimit(
|
||||
setCertificates,
|
||||
@@ -569,10 +622,21 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (existingArn && preserveArn && certificateId) {
|
||||
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||
if (currentCertificate?.renewedFromCertificateId) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]);
|
||||
if (response.CertificateArn && certificateId) {
|
||||
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||
if (existingCertSync) {
|
||||
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||
externalIdentifier: response.CertificateArn,
|
||||
syncStatus: CertificateSyncStatus.Succeeded,
|
||||
lastSyncedAt: new Date()
|
||||
});
|
||||
} else {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||
{
|
||||
certificateId,
|
||||
externalIdentifier: response.CertificateArn
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,29 +746,33 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
const {
|
||||
acmCertificates
|
||||
}: {
|
||||
acmCertificates: Record<
|
||||
string,
|
||||
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||
>;
|
||||
} = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
const certificateArnsToRemove: string[] = [];
|
||||
|
||||
const certificateIdToArnMap = new Map<string, string>();
|
||||
for (const certName of certificateNames) {
|
||||
const matchingCerts = Object.values(acmCertificates).filter((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
const certificateData = deps?.certificateMap?.[certName];
|
||||
if (certificateData?.certificateId) {
|
||||
const { certificateId } = certificateData;
|
||||
|
||||
for (const acmCert of matchingCerts) {
|
||||
if (acmCert.arn) {
|
||||
certificateArnsToRemove.push(acmCert.arn);
|
||||
if (typeof certificateId === "string") {
|
||||
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId);
|
||||
|
||||
if (syncRecord?.externalIdentifier) {
|
||||
certificateArnsToRemove.push(syncRecord.externalIdentifier);
|
||||
certificateIdToArnMap.set(certificateId, syncRecord.externalIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (certificateArnsToRemove.length === 0) {
|
||||
return {
|
||||
removed: 0,
|
||||
failed: 0,
|
||||
skipped: certificateNames.length
|
||||
};
|
||||
}
|
||||
|
||||
const results = await executeWithConcurrencyLimit(
|
||||
certificateArnsToRemove,
|
||||
async (certificateArn) =>
|
||||
@@ -714,43 +782,38 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
|
||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||
|
||||
if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) {
|
||||
const certificateNameToArnMap = new Map<string, string>();
|
||||
for (const certName of certificateNames) {
|
||||
const matchingCerts = Object.values(acmCertificates).filter((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
for (const acmCert of matchingCerts) {
|
||||
if (acmCert.arn) {
|
||||
certificateNameToArnMap.set(acmCert.arn, certName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||
for (const failure of failedRemovals) {
|
||||
if (failure.status === "rejected") {
|
||||
const failedArn = certificateArnsToRemove[results.indexOf(failure)];
|
||||
const certificateName = certificateNameToArnMap.get(failedArn);
|
||||
if (certificateName && deps.certificateMap[certificateName]?.certificateId) {
|
||||
const { certificateId } = deps.certificateMap[certificateName];
|
||||
if (certificateId) {
|
||||
const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||
try {
|
||||
await deps.certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
`Failed to remove from AWS: ${errorMessage}`
|
||||
);
|
||||
} catch (updateError) {
|
||||
logger.warn(`Failed to update sync status for certificate ${certificateId}:`, String(updateError));
|
||||
}
|
||||
}
|
||||
const certificateId = Array.from(certificateIdToArnMap.entries()).find(([, arn]) => arn === failedArn)?.[0];
|
||||
|
||||
if (certificateId) {
|
||||
const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||
await deps.certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
`Failed to remove from AWS: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||
if (successfulRemovals.length > 0) {
|
||||
const successfulArns = new Set(successfulRemovals.map((_, index) => certificateArnsToRemove[index]));
|
||||
|
||||
const certificateIdsToRemove = Array.from(certificateIdToArnMap.entries())
|
||||
.filter(([, arn]) => successfulArns.has(arn))
|
||||
.map(([certificateId]) => certificateId);
|
||||
|
||||
if (certificateIdsToRemove.length > 0) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedRemovals.length > 0) {
|
||||
const failedReasons = failedRemovals.map((failure) => {
|
||||
if (failure.status === "rejected") {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||
@@ -50,6 +51,16 @@ const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyn
|
||||
type TAzureKeyVaultPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "removeCertificates"
|
||||
| "addCertificates"
|
||||
| "findByPkiSyncAndCertificate"
|
||||
| "updateById"
|
||||
| "findByPkiSyncId"
|
||||
| "updateSyncStatus"
|
||||
>;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||
};
|
||||
|
||||
const parseCertificateX509Props = (certPem: string) => {
|
||||
@@ -192,7 +203,12 @@ const parseCertificateKeyProps = (certPem: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
||||
export const azureKeyVaultPkiSyncFactory = ({
|
||||
kmsService,
|
||||
appConnectionDAL,
|
||||
certificateSyncDAL,
|
||||
certificateDAL
|
||||
}: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
||||
const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
|
||||
const paginateAzureKeyVaultCertificates = async () => {
|
||||
let result: GetAzureKeyVaultCertificate[] = [];
|
||||
@@ -329,6 +345,20 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
pkiSync.id
|
||||
);
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
type SyncRecord = (typeof existingSyncRecords)[0];
|
||||
const syncRecordsByCertId = new Map<string, SyncRecord>();
|
||||
const syncRecordsByExternalId = new Map<string, SyncRecord>();
|
||||
|
||||
existingSyncRecords.forEach((record: SyncRecord) => {
|
||||
if (record.certificateId) {
|
||||
syncRecordsByCertId.set(record.certificateId, record);
|
||||
}
|
||||
if (record.externalIdentifier) {
|
||||
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||
}
|
||||
});
|
||||
|
||||
const setCertificates: {
|
||||
key: string;
|
||||
cert: string;
|
||||
@@ -338,48 +368,104 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
}[] = [];
|
||||
|
||||
const syncOptions = pkiSync.syncOptions as
|
||||
| { certificateNameSchema?: string; canRemoveCertificates?: boolean }
|
||||
| { certificateNameSchema?: string; canRemoveCertificates?: boolean; preserveVersion?: boolean }
|
||||
| undefined;
|
||||
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||
const preserveVersion = syncOptions?.preserveVersion ?? true;
|
||||
|
||||
// Track which certificates should exist in Azure Key Vault
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
const activeExternalIdentifiers = new Set<string>();
|
||||
|
||||
// Iterate through certificates to sync to Azure Key Vault
|
||||
Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain, certificateId }]) => {
|
||||
for (const [certName, { cert, privateKey, certificateChain, certificateId }] of Object.entries(certificateMap)) {
|
||||
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
|
||||
return;
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingCert = vaultCertificates[certName];
|
||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
||||
if (preserveVersion && typeof certificateId === "string") {
|
||||
const certificate = await certificateDAL.findById(certificateId);
|
||||
if (certificate?.renewedByCertificateId) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdateCert) {
|
||||
let targetCertName = certName;
|
||||
let shouldCreateNew = false;
|
||||
|
||||
if (typeof certificateId === "string") {
|
||||
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||
|
||||
if (existingSyncRecord?.externalIdentifier) {
|
||||
const existingAzureCert = vaultCertificates[existingSyncRecord.externalIdentifier];
|
||||
|
||||
if (existingAzureCert && preserveVersion) {
|
||||
targetCertName = existingSyncRecord.externalIdentifier;
|
||||
activeExternalIdentifiers.add(targetCertName);
|
||||
|
||||
const shouldUpdateCert = existingAzureCert.cert !== cert;
|
||||
if (shouldUpdateCert) {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else if (!existingAzureCert) {
|
||||
shouldCreateNew = true;
|
||||
} else if (!preserveVersion) {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
|
||||
if (shouldCreateNew || !vaultCertificates[targetCertName] || vaultCertificates[targetCertName].cert !== cert) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
key: targetCertName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
certificateId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify expired/removed certificates that need to be cleaned up from Azure Key Vault
|
||||
// Only remove certificates that were managed by Infisical (match naming schema)
|
||||
const certificatesToRemove = canRemoveCertificates
|
||||
? Object.keys(vaultCertificates).filter(
|
||||
(vaultCertName) =>
|
||||
isInfisicalManagedCertificate(vaultCertName, pkiSync) &&
|
||||
!activeCertificateNames.includes(vaultCertName) &&
|
||||
!disabledAzureKeyVaultCertificateKeys.includes(vaultCertName)
|
||||
)
|
||||
: [];
|
||||
if (targetCertName) {
|
||||
activeExternalIdentifiers.add(targetCertName);
|
||||
}
|
||||
}
|
||||
|
||||
const certificatesToRemove: string[] = [];
|
||||
|
||||
if (canRemoveCertificates) {
|
||||
existingSyncRecords.forEach((syncRecord) => {
|
||||
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||
if (vaultCertificates[syncRecord.externalIdentifier]) {
|
||||
certificatesToRemove.push(syncRecord.externalIdentifier);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(vaultCertificates).forEach((certificateName) => {
|
||||
const isInfisicalManaged = isInfisicalManagedCertificate(certificateName, pkiSync);
|
||||
|
||||
if (isInfisicalManaged) {
|
||||
const isTrackedInSyncRecords = existingSyncRecords.some(
|
||||
(record) => record.externalIdentifier === certificateName
|
||||
);
|
||||
|
||||
const isInActiveSet = activeExternalIdentifiers.has(certificateName);
|
||||
|
||||
if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(certificateName)) {
|
||||
certificatesToRemove.push(certificateName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Upload certificates to Azure Key Vault with rate limiting
|
||||
const uploadResults = await executeWithConcurrencyLimit(
|
||||
setCertificates,
|
||||
async ({ key, cert, privateKey, certificateChain }) => {
|
||||
async ({ key, cert, privateKey, certificateChain, certificateId }) => {
|
||||
try {
|
||||
// Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
|
||||
let combinedPem = "";
|
||||
@@ -441,6 +527,31 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
}
|
||||
);
|
||||
|
||||
if (certificateId) {
|
||||
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||
if (existingCertSync) {
|
||||
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||
externalIdentifier: key,
|
||||
syncStatus: CertificateSyncStatus.Succeeded,
|
||||
lastSyncedAt: new Date()
|
||||
});
|
||||
} else {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||
{
|
||||
certificateId,
|
||||
externalIdentifier: key
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
if (preserveVersion) {
|
||||
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||
if (currentCertificate?.renewedFromCertificateId) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { key, success: true, response: response.data as unknown };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
@@ -622,13 +733,33 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
// Cast destination config to Azure Key Vault config
|
||||
const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
|
||||
|
||||
// Only remove certificates that are managed by Infisical (match naming schema)
|
||||
const infisicalManagedCertNames = certificateNames.filter((certName) =>
|
||||
isInfisicalManagedCertificate(certName, pkiSync)
|
||||
);
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
const certificateNamesToRemove: string[] = [];
|
||||
const certificateIdToNameMap = new Map<string, string>();
|
||||
|
||||
for (const certName of certificateNames) {
|
||||
if (deps?.certificateMap?.[certName]?.certificateId) {
|
||||
const { certificateId } = deps.certificateMap[certName];
|
||||
|
||||
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId);
|
||||
|
||||
if (syncRecord?.externalIdentifier && typeof certificateId === "string") {
|
||||
certificateNamesToRemove.push(syncRecord.externalIdentifier);
|
||||
certificateIdToNameMap.set(certificateId, syncRecord.externalIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (certificateNamesToRemove.length === 0) {
|
||||
return {
|
||||
removed: 0,
|
||||
failed: 0,
|
||||
skipped: certificateNames.length
|
||||
};
|
||||
}
|
||||
|
||||
const results = await executeWithConcurrencyLimit(
|
||||
infisicalManagedCertNames,
|
||||
certificateNamesToRemove,
|
||||
async (certName) => {
|
||||
try {
|
||||
const response = await request.delete(
|
||||
@@ -663,29 +794,44 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
},
|
||||
{ operation: "remove-specific-certificates", syncId: pkiSync.id }
|
||||
);
|
||||
|
||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||
|
||||
if (failedRemovals.length > 0 && deps?.certificateSyncDAL && deps?.certificateMap) {
|
||||
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||
for (const failure of failedRemovals) {
|
||||
if (failure.status === "rejected") {
|
||||
const failedIndex = results.indexOf(failure);
|
||||
const failedCertName = infisicalManagedCertNames[failedIndex];
|
||||
if (failedCertName && deps.certificateMap[failedCertName]?.certificateId) {
|
||||
const { certificateId } = deps.certificateMap[failedCertName];
|
||||
if (certificateId) {
|
||||
const errorMessage = (failure.reason as Error)?.message || "Unknown error";
|
||||
await deps.certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
`Failed to remove from Azure: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
const failedCertName = certificateNamesToRemove[results.indexOf(failure)];
|
||||
|
||||
const certificateId = Array.from(certificateIdToNameMap.entries()).find(
|
||||
([, name]) => name === failedCertName
|
||||
)?.[0];
|
||||
|
||||
if (certificateId) {
|
||||
const errorMessage = (failure.reason as Error)?.message || "Unknown error";
|
||||
await deps.certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
`Failed to remove from Azure: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||
if (successfulRemovals.length > 0) {
|
||||
const successfulCertNames = new Set(successfulRemovals.map((_, index) => certificateNamesToRemove[index]));
|
||||
|
||||
const certificateIdsToRemove = Array.from(certificateIdToNameMap.entries())
|
||||
.filter(([, name]) => successfulCertNames.has(name))
|
||||
.map(([certificateId]) => certificateId);
|
||||
|
||||
if (certificateIdsToRemove.length > 0) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedRemovals.length > 0) {
|
||||
const failedReasons = failedRemovals.map((failure) => {
|
||||
if (failure.status === "rejected") {
|
||||
@@ -698,16 +844,16 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`,
|
||||
context: {
|
||||
failedReasons,
|
||||
totalCertificates: infisicalManagedCertNames.length,
|
||||
totalCertificates: certificateNamesToRemove.length,
|
||||
failedCount: failedRemovals.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
removed: infisicalManagedCertNames.length - failedRemovals.length,
|
||||
removed: certificateNamesToRemove.length - failedRemovals.length,
|
||||
failed: failedRemovals.length,
|
||||
skipped: certificateNames.length - infisicalManagedCertNames.length
|
||||
skipped: certificateNames.length - certificateNamesToRemove.length
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const AzureKeyVaultPkiSyncConfigSchema = z.object({
|
||||
const AzureKeyVaultPkiSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
preserveVersion: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -204,7 +204,12 @@ export const PkiSyncFns = {
|
||||
switch (pkiSync.destination) {
|
||||
case PkiSync.AzureKeyVault: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
|
||||
appConnectionDAL: dependencies.appConnectionDAL,
|
||||
kmsService: dependencies.kmsService,
|
||||
certificateDAL: dependencies.certificateDAL,
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||
});
|
||||
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||
}
|
||||
case PkiSync.AwsCertificateManager: {
|
||||
@@ -236,7 +241,12 @@ export const PkiSyncFns = {
|
||||
switch (pkiSync.destination) {
|
||||
case PkiSync.AzureKeyVault: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
|
||||
appConnectionDAL: dependencies.appConnectionDAL,
|
||||
kmsService: dependencies.kmsService,
|
||||
certificateDAL: dependencies.certificateDAL,
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||
});
|
||||
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||
certificateMap: dependencies.certificateMap
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
@@ -10,6 +10,7 @@ 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 { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||
@@ -48,6 +49,7 @@ type TPkiSyncServiceFactoryDep = {
|
||||
TPkiSyncDALFactory,
|
||||
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
||||
>;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findActiveCertificatesByIds">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "findByPkiSyncId"
|
||||
@@ -72,6 +74,7 @@ export type TPkiSyncServiceFactory = ReturnType<typeof pkiSyncServiceFactory>;
|
||||
|
||||
export const pkiSyncServiceFactory = ({
|
||||
pkiSyncDAL,
|
||||
certificateDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
@@ -79,6 +82,26 @@ export const pkiSyncServiceFactory = ({
|
||||
licenseService,
|
||||
pkiSyncQueue
|
||||
}: TPkiSyncServiceFactoryDep) => {
|
||||
const validateCertificatesProjectOwnership = async (certificateIds: string[], expectedProjectId: string) => {
|
||||
if (certificateIds.length === 0) return;
|
||||
|
||||
const certificates = await certificateDAL.findActiveCertificatesByIds(certificateIds);
|
||||
|
||||
if (certificates.length !== certificateIds.length) {
|
||||
const foundIds = certificates.map((cert) => cert.id);
|
||||
const missingIds = certificateIds.filter((id) => !foundIds.includes(id));
|
||||
throw new NotFoundError({
|
||||
message: `Certificates not found or not active: ${missingIds.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
const invalidCertificates = certificates.filter((cert) => cert.projectId !== expectedProjectId);
|
||||
if (invalidCertificates.length > 0) {
|
||||
throw new BadRequestError({
|
||||
message: `Certificates do not belong to the same project: ${invalidCertificates.map((cert) => cert.id).join(", ")}`
|
||||
});
|
||||
}
|
||||
};
|
||||
const createPkiSync = async (
|
||||
{
|
||||
name,
|
||||
@@ -132,6 +155,10 @@ export const pkiSyncServiceFactory = ({
|
||||
...syncOptions
|
||||
};
|
||||
|
||||
if (certificateIds.length > 0) {
|
||||
await validateCertificatesProjectOwnership(certificateIds, projectId);
|
||||
}
|
||||
|
||||
try {
|
||||
const pkiSync = await pkiSyncDAL.create({
|
||||
name,
|
||||
@@ -147,7 +174,10 @@ export const pkiSyncServiceFactory = ({
|
||||
});
|
||||
|
||||
if (certificateIds.length > 0) {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, certificateIds);
|
||||
await certificateSyncDAL.addCertificates(
|
||||
pkiSync.id,
|
||||
certificateIds.map((id) => ({ certificateId: id }))
|
||||
);
|
||||
}
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
@@ -245,9 +275,16 @@ export const pkiSyncServiceFactory = ({
|
||||
}
|
||||
|
||||
if (certificateIds !== undefined) {
|
||||
if (certificateIds.length > 0) {
|
||||
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||
}
|
||||
|
||||
await certificateSyncDAL.removeAllCertificatesFromSync(id);
|
||||
if (certificateIds.length > 0) {
|
||||
await certificateSyncDAL.addCertificates(id, certificateIds);
|
||||
await certificateSyncDAL.addCertificates(
|
||||
id,
|
||||
certificateIds.map((certId) => ({ certificateId: certId }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +526,10 @@ export const pkiSyncServiceFactory = ({
|
||||
const addCertificatesToPkiSync = async (
|
||||
{ pkiSyncId, certificateIds }: Omit<TAddCertificatesToPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
): Promise<{
|
||||
addedCertificates: TCertificateSyncs[];
|
||||
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||
}> => {
|
||||
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||
|
||||
@@ -502,31 +542,33 @@ export const pkiSyncServiceFactory = ({
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
let subscriber;
|
||||
if (pkiSync.subscriberId) {
|
||||
subscriber = await pkiSubscriberDAL.findById(pkiSync.subscriberId);
|
||||
}
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
subscriber
|
||||
? subject(ProjectPermissionSub.PkiSyncs, { subscriberName: subscriber.name })
|
||||
: ProjectPermissionSub.PkiSyncs
|
||||
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||
|
||||
const addedCertificates = await certificateSyncDAL.addCertificates(
|
||||
pkiSyncId,
|
||||
certificateIds.map((id) => ({ certificateId: id }))
|
||||
);
|
||||
|
||||
const addedCertificates = await certificateSyncDAL.addCertificates(pkiSyncId, certificateIds);
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||
}
|
||||
|
||||
return addedCertificates;
|
||||
return {
|
||||
addedCertificates,
|
||||
pkiSyncInfo: {
|
||||
projectId: pkiSync.projectId,
|
||||
destination: pkiSync.destination,
|
||||
name: pkiSync.name
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const removeCertificatesFromPkiSync = async (
|
||||
{ pkiSyncId, certificateIds }: Omit<TRemoveCertificatesFromPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
): Promise<{ removedCount: number; pkiSyncInfo: { projectId: string; destination: string; name: string } }> => {
|
||||
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||
|
||||
@@ -539,17 +581,7 @@ export const pkiSyncServiceFactory = ({
|
||||
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
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
|
||||
|
||||
@@ -557,13 +589,24 @@ export const pkiSyncServiceFactory = ({
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||
}
|
||||
|
||||
return { removedCount };
|
||||
return {
|
||||
removedCount,
|
||||
pkiSyncInfo: {
|
||||
projectId: pkiSync.projectId,
|
||||
destination: pkiSync.destination,
|
||||
name: pkiSync.name
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const listPkiSyncCertificates = async (
|
||||
{ pkiSyncId, offset = 0, limit = 20 }: Omit<TListPkiSyncCertificatesDTO, "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<{ certificates: TPkiSyncCertificate[]; totalCount: number }> => {
|
||||
): Promise<{
|
||||
certificates: TPkiSyncCertificate[];
|
||||
totalCount: number;
|
||||
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||
}> => {
|
||||
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||
|
||||
@@ -576,17 +619,7 @@ export const pkiSyncServiceFactory = ({
|
||||
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
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
const result = await certificateSyncDAL.findWithDetails({
|
||||
pkiSyncId,
|
||||
@@ -611,14 +644,22 @@ export const pkiSyncServiceFactory = ({
|
||||
certificateNotBefore: detail.certificateNotBefore || undefined,
|
||||
certificateNotAfter: detail.certificateNotAfter || undefined,
|
||||
certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId
|
||||
? detail.certificateRenewBeforeDays
|
||||
? detail.certificateRenewBeforeDays || undefined
|
||||
: undefined,
|
||||
certificateRenewalError: detail.certificateRenewalError || undefined,
|
||||
pkiSyncName: detail.pkiSyncName || undefined,
|
||||
pkiSyncDestination: detail.pkiSyncDestination || undefined
|
||||
}));
|
||||
|
||||
return { certificates, totalCount };
|
||||
return {
|
||||
certificates,
|
||||
totalCount,
|
||||
pkiSyncInfo: {
|
||||
projectId: pkiSync.projectId,
|
||||
destination: pkiSync.destination,
|
||||
name: pkiSync.name
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -45,12 +45,13 @@ export const triggerAutoSyncForCertificate = async (
|
||||
}
|
||||
|
||||
const allPkiSyncs = await dependencies.pkiSyncDAL.find({
|
||||
isAutoSyncEnabled: true
|
||||
isAutoSyncEnabled: true,
|
||||
$in: {
|
||||
id: pkiSyncIds
|
||||
}
|
||||
});
|
||||
|
||||
const pkiSyncs = allPkiSyncs.filter((sync) => pkiSyncIds.includes(sync.id));
|
||||
|
||||
const syncPromises = pkiSyncs.map((pkiSync) =>
|
||||
const syncPromises = allPkiSyncs.map((pkiSync) =>
|
||||
dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id })
|
||||
);
|
||||
await Promise.all(syncPromises);
|
||||
@@ -65,7 +66,7 @@ export const addRenewedCertificateToSyncs = async (
|
||||
dependencies: {
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
"findPkiSyncIdsByCertificateId" | "removeCertificates" | "addCertificates"
|
||||
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||
>;
|
||||
},
|
||||
tx?: Knex
|
||||
@@ -78,12 +79,25 @@ export const addRenewedCertificateToSyncs = async (
|
||||
}
|
||||
|
||||
const addPromises = pkiSyncIds.map(async (pkiSyncId) => {
|
||||
await dependencies.certificateSyncDAL.addCertificates(pkiSyncId, [newCertificateId], tx);
|
||||
});
|
||||
const oldCertificateRecord = await dependencies.certificateSyncDAL.findByPkiSyncAndCertificate(
|
||||
pkiSyncId,
|
||||
oldCertificateId
|
||||
);
|
||||
|
||||
await dependencies.certificateSyncDAL.addCertificates(
|
||||
pkiSyncId,
|
||||
[
|
||||
{
|
||||
certificateId: newCertificateId,
|
||||
externalIdentifier: oldCertificateRecord?.externalIdentifier || undefined
|
||||
}
|
||||
],
|
||||
tx
|
||||
);
|
||||
});
|
||||
await Promise.all(addPromises);
|
||||
|
||||
logger.info(`Successfully added renewed certificate ${newCertificateId} to ${pkiSyncIds.length} PKI sync(s)`);
|
||||
logger.info(`Successfully added renewed certificate ${newCertificateId} to PKI sync(s)`);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to add renewed certificate ${newCertificateId} to syncs:`);
|
||||
throw error;
|
||||
|
||||
@@ -31,7 +31,7 @@ This section walks you through the complete end-to-end process of setting up Azu
|
||||
|
||||
<Step title="Create New Azure ADCS Certificate Service CA">
|
||||
Click **Create CA** and configure:
|
||||
- **Type**: Choose **Azure AD Certificate Service**
|
||||
- **Type**: Choose **Active Directory Certificate Services (AD CS)**
|
||||
- **Name**: Friendly name for this CA (e.g., "Production ADCS CA")
|
||||
- **App Connection**: Choose your ADCS connection from the dropdown
|
||||
|
||||
|
||||
@@ -11,21 +11,24 @@ type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
selectSync?: PkiSync | null;
|
||||
initialData?: any;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
selectedSync: PkiSync | null;
|
||||
setSelectedSync: (selectedSync: PkiSync | null) => void;
|
||||
initialData?: any;
|
||||
};
|
||||
|
||||
const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) => {
|
||||
const Content = ({ onComplete, setSelectedSync, selectedSync, initialData }: ContentProps) => {
|
||||
if (selectedSync) {
|
||||
return (
|
||||
<CreatePkiSyncForm
|
||||
onComplete={onComplete}
|
||||
onCancel={() => setSelectedSync(null)}
|
||||
destination={selectedSync}
|
||||
initialData={initialData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +36,12 @@ const Content = ({ onComplete, setSelectedSync, selectedSync }: ContentProps) =>
|
||||
return <PkiSyncSelect onSelect={setSelectedSync} />;
|
||||
};
|
||||
|
||||
export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }: Props) => {
|
||||
export const CreatePkiSyncModal = ({
|
||||
onOpenChange,
|
||||
selectSync = null,
|
||||
initialData,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [selectedSync, setSelectedSync] = useState<PkiSync | null>(selectSync);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,6 +77,7 @@ export const CreatePkiSyncModal = ({ onOpenChange, selectSync = null, ...props }
|
||||
}}
|
||||
selectedSync={selectedSync}
|
||||
setSelectedSync={setSelectedSync}
|
||||
initialData={initialData}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -23,6 +23,7 @@ type Props = {
|
||||
onComplete: (pkiSync: TPkiSync) => void;
|
||||
destination: PkiSync;
|
||||
onCancel: () => void;
|
||||
initialData?: any;
|
||||
};
|
||||
|
||||
const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[] = [
|
||||
@@ -33,7 +34,7 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TPkiSyncForm)[] }[]
|
||||
{ name: "Review", key: "review", fields: [] }
|
||||
];
|
||||
|
||||
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props) => {
|
||||
export const CreatePkiSyncForm = ({ destination, onComplete, onCancel, initialData }: Props) => {
|
||||
const createPkiSync = useCreatePkiSync();
|
||||
const { currentProject } = useProject();
|
||||
const { name: destinationName } = PKI_SYNC_MAP[destination];
|
||||
@@ -55,7 +56,8 @@ export const CreatePkiSyncForm = ({ destination, onComplete, onCancel }: Props)
|
||||
canRemoveCertificates: false,
|
||||
preserveArn: true,
|
||||
certificateNameSchema: syncOption?.defaultCertificateNameSchema
|
||||
}
|
||||
},
|
||||
...initialData
|
||||
} as Partial<TPkiSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
@@ -27,12 +27,16 @@ export const EditPkiSyncForm = ({ pkiSync, fields, onComplete }: Props) => {
|
||||
const formMethods = useForm<TUpdatePkiSyncForm>({
|
||||
resolver: zodResolver(UpdatePkiSyncFormSchema),
|
||||
defaultValues: {
|
||||
...pkiSync,
|
||||
name: pkiSync.name,
|
||||
destination: pkiSync.destination,
|
||||
description: pkiSync.description ?? "",
|
||||
connection: {
|
||||
id: pkiSync.connectionId,
|
||||
name: pkiSync.appConnectionName
|
||||
}
|
||||
},
|
||||
syncOptions: pkiSync.syncOptions,
|
||||
destinationConfig: pkiSync.destinationConfig,
|
||||
isAutoSyncEnabled: pkiSync.isAutoSyncEnabled
|
||||
} as Partial<TUpdatePkiSyncForm>,
|
||||
reValidateMode: "onChange"
|
||||
});
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { SingleValue } from "react-select";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { useRouterState } from "@tanstack/react-router";
|
||||
|
||||
import { AppConnectionOption } from "@app/components/app-connections";
|
||||
import { FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { ProjectPermissionSub, useProject, useProjectPermission } from "@app/context";
|
||||
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { PKI_SYNC_CONNECTION_MAP } from "@app/helpers/pkiSyncs";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||
import { AddAppConnectionModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
|
||||
|
||||
import { TPkiSyncForm } from "./schemas/pki-sync-schema";
|
||||
|
||||
@@ -18,12 +22,30 @@ type Props = {
|
||||
|
||||
export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
||||
const { permission } = useProjectPermission();
|
||||
const { control, watch } = useFormContext<TPkiSyncForm>();
|
||||
const { currentProject } = useProject();
|
||||
const { control, watch, setValue } = useFormContext<TPkiSyncForm>();
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["addConnection"] as const);
|
||||
|
||||
const destination = watch("destination");
|
||||
const app = PKI_SYNC_CONNECTION_MAP[destination];
|
||||
|
||||
const { currentProject } = useProject();
|
||||
|
||||
const {
|
||||
location: { pathname }
|
||||
} = useRouterState();
|
||||
|
||||
const getPkiSyncReturnUrl = () => {
|
||||
if (pathname.includes("selectedTab=secret-syncs")) {
|
||||
return pathname.replace("selectedTab=secret-syncs", "selectedTab=pki-syncs");
|
||||
}
|
||||
if (!pathname.includes("selectedTab=")) {
|
||||
const separator = pathname.includes("?") ? "&" : "?";
|
||||
return `${pathname}${separator}selectedTab=pki-syncs`;
|
||||
}
|
||||
return pathname;
|
||||
};
|
||||
|
||||
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
||||
app,
|
||||
currentProject.id
|
||||
@@ -47,6 +69,7 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText="App Connections can be created from the Organization Settings page."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label={`${connectionName} Connection`}
|
||||
@@ -54,36 +77,54 @@ export const PkiSyncConnectionField = ({ onChange: callback }: Props) => {
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
if ((newValue as SingleValue<{ id: string; name: string }>)?.id === "_create") {
|
||||
handlePopUpOpen("addConnection");
|
||||
onChange(null);
|
||||
const formData = { ...watch(), returnUrl: getPkiSyncReturnUrl() };
|
||||
localStorage.setItem("pkiSyncFormData", JSON.stringify(formData));
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
if (callback) callback();
|
||||
}}
|
||||
isLoading={isPending}
|
||||
options={availableConnections}
|
||||
options={[
|
||||
...(canCreateConnection ? [{ id: "_create", name: "Create Connection" }] : []),
|
||||
...(availableConnections ?? [])
|
||||
]}
|
||||
placeholder="Select connection..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
components={{ Option: AppConnectionOption }}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="connection"
|
||||
/>
|
||||
{availableConnections?.length === 0 && (
|
||||
{!isPending && !availableConnections?.length && !canCreateConnection && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
|
||||
{canCreateConnection ? (
|
||||
<>
|
||||
You do not have access to any {appName} Connections. Create one from the{" "}
|
||||
<Link to="/organization/app-connections" className="underline">
|
||||
App Connections
|
||||
</Link>{" "}
|
||||
page.
|
||||
</>
|
||||
) : (
|
||||
`You do not have access to any ${appName} Connections. Contact an admin to create one.`
|
||||
)}
|
||||
You do not have access to any {appName} Connections. Contact an admin to create one.
|
||||
</p>
|
||||
)}
|
||||
<AddAppConnectionModal
|
||||
isOpen={popUp.addConnection.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
handlePopUpToggle("addConnection", isOpen);
|
||||
}}
|
||||
projectType={currentProject.type}
|
||||
projectId={currentProject.id}
|
||||
app={app}
|
||||
onComplete={(connection) => {
|
||||
if (connection) {
|
||||
setValue("connection", connection);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Enable Inactive Certificate Removal{" "}
|
||||
Enable Removal of Active/Revoked Certificates{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
@@ -138,6 +138,51 @@ export const PkiSyncOptionsFields = ({ destination }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentDestination === PkiSync.AzureKeyVault && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.preserveVersion"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="preserve-version"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
>
|
||||
<p>
|
||||
Preserve Version on Renewal{" "}
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
When enabled, Infisical will create a new version of the existing
|
||||
certificate in Azure Key Vault during certificate renewal syncs,
|
||||
preserving the certificate name.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
This allows consuming services to continue using the same certificate name
|
||||
while automatically using the latest version without requiring manual
|
||||
updates.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
When disabled, new certificates will be created with new names, and old
|
||||
certificates will be removed.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="syncOptions.certificateNameSchema"
|
||||
|
||||
@@ -4,7 +4,46 @@ import { PkiSync } from "@app/hooks/api/pkiSyncs";
|
||||
|
||||
import { BasePkiSyncSchema } from "./base-pki-sync-schema";
|
||||
|
||||
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema().merge(
|
||||
const AzureKeyVaultSyncOptionsSchema = z.object({
|
||||
canImportCertificates: z.boolean().default(false),
|
||||
canRemoveCertificates: z.boolean().default(true),
|
||||
preserveVersion: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
const allowedOptionalPlaceholders = ["{{environment}}"];
|
||||
|
||||
const allowedPlaceholdersRegexPart = ["{{certificateId}}", ...allowedOptionalPlaceholders]
|
||||
.map((p) => p.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"))
|
||||
.join("|");
|
||||
|
||||
const allowedContentRegex = new RegExp(
|
||||
`^([a-zA-Z0-9_\\-/]|${allowedPlaceholdersRegexPart})*$`
|
||||
);
|
||||
const contentIsValid = allowedContentRegex.test(val);
|
||||
|
||||
if (val.trim()) {
|
||||
const certificateIdRegex = /\{\{certificateId\}\}/;
|
||||
const certificateIdIsPresent = certificateIdRegex.test(val);
|
||||
return contentIsValid && certificateIdIsPresent;
|
||||
}
|
||||
|
||||
return contentIsValid;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Certificate name schema must include exactly one {{certificateId}} placeholder. It can also include {{environment}} placeholders. Only alphanumeric characters (a-z, A-Z, 0-9), dashes (-), underscores (_), and slashes (/) are allowed besides the placeholders."
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
export const AzureKeyVaultPkiSyncDestinationSchema = BasePkiSyncSchema(
|
||||
AzureKeyVaultSyncOptionsSchema
|
||||
).merge(
|
||||
z.object({
|
||||
destination: z.literal(PkiSync.AzureKeyVault),
|
||||
destinationConfig: z.object({
|
||||
|
||||
@@ -124,7 +124,7 @@ type Props = {
|
||||
|
||||
const caTypes = [
|
||||
{ label: "ACME", value: CaType.ACME },
|
||||
{ label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS }
|
||||
{ label: "Active Directory Certificate Services (AD CS)", value: CaType.AZURE_AD_CS }
|
||||
];
|
||||
|
||||
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
@@ -18,12 +19,15 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useProject } from "@app/context";
|
||||
import {
|
||||
PkiSync,
|
||||
useAddCertificatesToPkiSync,
|
||||
useListPkiSyncsWithCertificate,
|
||||
useRemoveCertificatesFromPkiSync
|
||||
} from "@app/hooks/api/pkiSyncs";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
type Props = {
|
||||
popUp: {
|
||||
@@ -46,6 +50,7 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const { currentProject } = useProject();
|
||||
const navigate = useNavigate();
|
||||
const { certificateId, commonName } = popUp.data || {};
|
||||
|
||||
const { data: pkiSyncs = [], isPending } = useListPkiSyncsWithCertificate(
|
||||
@@ -81,6 +86,32 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleNavigateToPkiSyncs = () => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
navigate({
|
||||
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||
params: {
|
||||
projectId: currentProject.id
|
||||
},
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.PkiSyncs
|
||||
}
|
||||
});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const getDestinationDisplayName = (destination: string) => {
|
||||
switch (destination) {
|
||||
case PkiSync.AzureKeyVault:
|
||||
return "Azure Key Vault";
|
||||
case PkiSync.AwsCertificateManager:
|
||||
return "AWS Certificate Manager";
|
||||
default:
|
||||
return destination;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!certificateId || !pkiSyncs || pkiSyncs.length === 0) return;
|
||||
|
||||
@@ -161,7 +192,7 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro
|
||||
placeholder="Search PKI syncs by name..."
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<div className="mt-4 max-h-96 overflow-y-auto">
|
||||
{isPending && (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<div className="text-bunker-300">Loading PKI syncs...</div>
|
||||
@@ -169,12 +200,24 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro
|
||||
)}
|
||||
{!isPending && pkiSyncs.length === 0 && (
|
||||
<EmptyState title="No PKI syncs available" icon={faPlus}>
|
||||
Create a PKI sync first to manage certificate syncing.
|
||||
<div className="mt-1">
|
||||
Create a{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNavigateToPkiSyncs}
|
||||
className="cursor-pointer underline hover:text-mineshaft-300"
|
||||
>
|
||||
PKI sync
|
||||
</button>{" "}
|
||||
first to manage certificate syncing.
|
||||
</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && pkiSyncs.length > 0 && filteredSyncs.length === 0 && searchTerm && (
|
||||
<EmptyState title="No PKI syncs found" icon={faSearch}>
|
||||
No PKI syncs match your search criteria. Try a different search term.
|
||||
<div className="mt-1">
|
||||
No PKI syncs match your search criteria. Try a different search term.
|
||||
</div>
|
||||
</EmptyState>
|
||||
)}
|
||||
{!isPending && filteredSyncs.length > 0 && (
|
||||
@@ -209,9 +252,9 @@ export const CertificateManagePkiSyncsModal = ({ popUp, handlePopUpToggle }: Pro
|
||||
<Td className="w-1/2 max-w-0">
|
||||
<div
|
||||
className="truncate capitalize"
|
||||
title={sync.destination.replace(/-/g, " ")}
|
||||
title={getDestinationDisplayName(sync.destination)}
|
||||
>
|
||||
{sync.destination.replace(/-/g, " ")}
|
||||
{getDestinationDisplayName(sync.destination)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -478,7 +478,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faLink} />}
|
||||
>
|
||||
PKI Syncs
|
||||
Manage PKI Syncs
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -12,13 +12,14 @@ import { ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { ProjectPermissionPkiSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useListPkiSyncs } from "@app/hooks/api/pkiSyncs";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
import { PkiSyncsTable } from "./PkiSyncTable";
|
||||
|
||||
export const PkiSyncsTab = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addSync"] as const);
|
||||
|
||||
const { addSync, ...search } = useSearch({
|
||||
const { addSync, connectionId, connectionName, ...search } = useSearch({
|
||||
from: ROUTE_PATHS.CertManager.IntegrationsListPage.id
|
||||
});
|
||||
|
||||
@@ -45,6 +46,42 @@ export const PkiSyncsTab = () => {
|
||||
navigateToBase();
|
||||
}, [addSync, handlePopUpOpen, navigateToBase]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedFormData = localStorage.getItem("pkiSyncFormData");
|
||||
if (storedFormData && !popUp.addSync.isOpen) {
|
||||
try {
|
||||
const parsedData = JSON.parse(storedFormData);
|
||||
if (connectionId && connectionName) {
|
||||
const initialData = {
|
||||
...parsedData,
|
||||
connection: { id: connectionId, name: connectionName }
|
||||
};
|
||||
handlePopUpOpen("addSync", { destination: parsedData.destination, initialData });
|
||||
navigate({
|
||||
to: ROUTE_PATHS.CertManager.IntegrationsListPage.path,
|
||||
params: { projectId: currentProject?.id },
|
||||
search: { selectedTab: IntegrationsListPageTabs.PkiSyncs },
|
||||
replace: true
|
||||
});
|
||||
} else {
|
||||
handlePopUpOpen("addSync", { destination: parsedData.destination });
|
||||
}
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
} catch (error) {
|
||||
console.error("Failed to parse stored PKI sync form data:", error);
|
||||
localStorage.removeItem("pkiSyncFormData");
|
||||
handlePopUpOpen("addSync");
|
||||
}
|
||||
}
|
||||
}, [
|
||||
handlePopUpOpen,
|
||||
popUp.addSync.isOpen,
|
||||
connectionId,
|
||||
connectionName,
|
||||
navigate,
|
||||
currentProject?.id
|
||||
]);
|
||||
|
||||
const { data: pkiSyncs = [], isPending: isPkiSyncsPending } = useListPkiSyncs(
|
||||
currentProject?.id || "",
|
||||
{
|
||||
@@ -94,7 +131,8 @@ export const PkiSyncsTab = () => {
|
||||
<PkiSyncsTable pkiSyncs={pkiSyncs} />
|
||||
</div>
|
||||
<CreatePkiSyncModal
|
||||
selectSync={popUp.addSync.data}
|
||||
selectSync={popUp.addSync.data?.destination || popUp.addSync.data}
|
||||
initialData={popUp.addSync.data?.initialData}
|
||||
isOpen={popUp.addSync.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSync", isOpen)}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,9 @@ import { IntegrationsListPage } from "./IntegrationsListPage";
|
||||
|
||||
const IntegrationsListPageQuerySchema = z.object({
|
||||
selectedTab: z.nativeEnum(IntegrationsListPageTabs).optional(),
|
||||
addSync: z.nativeEnum(PkiSync).optional()
|
||||
addSync: z.nativeEnum(PkiSync).optional(),
|
||||
connectionId: z.string().optional(),
|
||||
connectionName: z.string().optional()
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
|
||||
@@ -418,7 +418,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
||||
name="enrollmentType"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Enrollment Type"
|
||||
label="Enrollment Method"
|
||||
isRequired
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
|
||||
@@ -633,7 +633,9 @@ export const OAuthCallbackPage = () => {
|
||||
connectionName: data.connection.name,
|
||||
...(data.returnUrl.includes("integrations")
|
||||
? {
|
||||
selectedTab: IntegrationsListPageTabs.SecretSyncs
|
||||
selectedTab: localStorage.getItem("pkiSyncFormData")
|
||||
? IntegrationsListPageTabs.PkiSyncs
|
||||
: IntegrationsListPageTabs.SecretSyncs
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user