Merge branch 'main' into fix/remove-duplicate-error-notifications
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier"))) {
|
||||
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||
t.text("externalIdentifier").nullable();
|
||||
t.index("externalIdentifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier")) {
|
||||
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||
t.dropIndex("externalIdentifier");
|
||||
t.dropColumn("externalIdentifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificateSyncsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
pkiSyncId: z.string().uuid(),
|
||||
certificateId: z.string().uuid(),
|
||||
syncStatus: z.string().default("pending").nullable().optional(),
|
||||
lastSyncMessage: z.string().nullable().optional(),
|
||||
lastSyncedAt: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
externalIdentifier: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateSyncs = z.infer<typeof CertificateSyncsSchema>;
|
||||
export type TCertificateSyncsInsert = Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>;
|
||||
export type TCertificateSyncsUpdate = Partial<Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>>;
|
||||
@@ -17,6 +17,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||
@@ -61,8 +61,7 @@ export const gatewayV2ServiceFactory = ({
|
||||
relayDAL,
|
||||
permissionService,
|
||||
orgDAL,
|
||||
notificationService,
|
||||
smtpService
|
||||
notificationService
|
||||
}: TGatewayV2ServiceFactoryDep) => {
|
||||
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
@@ -931,15 +930,17 @@ export const gatewayV2ServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
subjectLine: "Gateway Health Alert",
|
||||
substitutions: {
|
||||
type: "gateway",
|
||||
names: gatewayNames
|
||||
},
|
||||
template: SmtpTemplates.HealthAlert
|
||||
});
|
||||
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||
//
|
||||
// await smtpService.sendMail({
|
||||
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
// subjectLine: "Gateway Health Alert",
|
||||
// substitutions: {
|
||||
// type: "gateway",
|
||||
// names: gatewayNames
|
||||
// },
|
||||
// template: SmtpTemplates.HealthAlert
|
||||
// });
|
||||
|
||||
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import knex from "knex";
|
||||
import mysql, { Connection } from "mysql2/promise";
|
||||
import * as pg from "pg";
|
||||
import tls, { PeerCertificate } from "tls";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
@@ -97,7 +96,7 @@ const makeSqlConnection = (
|
||||
try {
|
||||
await client.raw(SIMPLE_QUERY);
|
||||
} catch (error) {
|
||||
if (error instanceof pg.DatabaseError) {
|
||||
if (error instanceof Error) {
|
||||
// Hacky way to know if we successfully hit the database.
|
||||
// TODO: potentially two approaches to solve the problem.
|
||||
// 1. change the work flow, add account first then resource
|
||||
|
||||
@@ -1268,15 +1268,17 @@ export const relayServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
subjectLine: "Relay Health Alert",
|
||||
substitutions: {
|
||||
type: "relay",
|
||||
names: relayNames
|
||||
},
|
||||
template: SmtpTemplates.HealthAlert
|
||||
});
|
||||
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||
//
|
||||
// await smtpService.sendMail({
|
||||
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
// subjectLine: "Relay Health Alert",
|
||||
// substitutions: {
|
||||
// type: "relay",
|
||||
// names: relayNames
|
||||
// },
|
||||
// template: SmtpTemplates.HealthAlert
|
||||
// });
|
||||
}
|
||||
|
||||
await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() })));
|
||||
|
||||
@@ -2379,6 +2379,12 @@ export const AppConnections = {
|
||||
},
|
||||
LARAVEL_FORGE: {
|
||||
apiToken: "The API token used to authenticate with Laravel Forge."
|
||||
},
|
||||
CHEF: {
|
||||
serverUrl: "The URL of the Chef server to connect to.",
|
||||
orgName: "The short name of the Chef organization to connect to.",
|
||||
userName: "The username used to access Chef.",
|
||||
privateKey: "The private key used to access Chef."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2624,6 +2630,10 @@ export const SecretSyncs = {
|
||||
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||
context: "The Netlify context to sync secrets to."
|
||||
},
|
||||
CHEF: {
|
||||
dataBagName: "The name of the Chef data bag to sync secrets to.",
|
||||
dataBagItemName: "The name of the Chef data bag item to sync secrets to."
|
||||
},
|
||||
NORTHFLANK: {
|
||||
projectId: "The ID of the Northflank project to sync secrets to.",
|
||||
projectName: "The name of the Northflank project to sync secrets to.",
|
||||
|
||||
@@ -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";
|
||||
@@ -1064,6 +1065,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);
|
||||
@@ -2027,7 +2029,8 @@ export const registerRoutes = async (
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL
|
||||
certificateAuthorityCertDAL,
|
||||
certificateSyncDAL
|
||||
});
|
||||
|
||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||
@@ -2138,6 +2141,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
@@ -2149,7 +2153,10 @@ export const registerRoutes = async (
|
||||
certificateProfileDAL,
|
||||
certificateTemplateV2Service,
|
||||
internalCaService: internalCertificateAuthorityService,
|
||||
permissionService
|
||||
permissionService,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||
@@ -2191,6 +2198,8 @@ export const registerRoutes = async (
|
||||
|
||||
const pkiSyncService = pkiSyncServiceFactory({
|
||||
pkiSyncDAL,
|
||||
certificateDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
ChecklyConnectionListItemSchema,
|
||||
SanitizedChecklyConnectionSchema
|
||||
} from "@app/services/app-connection/checkly";
|
||||
import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef";
|
||||
import {
|
||||
CloudflareConnectionListItemSchema,
|
||||
SanitizedCloudflareConnectionSchema
|
||||
@@ -168,7 +169,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -212,7 +214,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } 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 {
|
||||
CreateChefConnectionSchema,
|
||||
SanitizedChefConnectionSchema,
|
||||
UpdateChefConnectionSchema
|
||||
} from "@app/services/app-connection/chef";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerChefConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Chef,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedChefConnectionSchema,
|
||||
createSchema: CreateChefConnectionSchema,
|
||||
updateSchema: UpdateChefConnectionSchema
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/data-bags`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const dataBags = await server.services.appConnection.chef.listDataBags(connectionId, req.permission);
|
||||
|
||||
return dataBags;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/data-bag-items`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
dataBagName: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { dataBagName } = req.query;
|
||||
const dataBagItems = await server.services.appConnection.chef.listDataBagItems(
|
||||
connectionId,
|
||||
dataBagName,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return dataBagItems;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect
|
||||
import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
|
||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { registerChefConnectionRouter } from "./chef-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||
@@ -86,5 +87,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter,
|
||||
[AppConnection.Chef]: registerChefConnectionRouter
|
||||
};
|
||||
|
||||
@@ -121,9 +121,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
search: z.string().optional(),
|
||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||
caId: z.string().uuid().optional(),
|
||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||
caId: z.string().uuid().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||
@@ -232,16 +226,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
autoRenew: z.boolean(),
|
||||
renewBeforeDays: z.number().optional()
|
||||
})
|
||||
.optional(),
|
||||
metrics: z
|
||||
.object({
|
||||
profileId: z.string(),
|
||||
totalCertificates: z.number(),
|
||||
activeCertificates: z.number(),
|
||||
expiredCertificates: z.number(),
|
||||
expiringCertificates: z.number(),
|
||||
revokedCertificates: z.number()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
profileId: req.params.id
|
||||
});
|
||||
|
||||
let result = certificateProfile;
|
||||
|
||||
if (req.query.includeMetrics) {
|
||||
const metrics = await server.services.certificateProfile.getProfileMetrics({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.params.id,
|
||||
expiringDays: req.query.expiringDays
|
||||
});
|
||||
result = { ...certificateProfile, metrics };
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateProfile.projectId,
|
||||
@@ -283,7 +253,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
}
|
||||
});
|
||||
|
||||
return { certificateProfile: result };
|
||||
return { certificateProfile };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
|
||||
syncOptions?: Record<string, unknown>;
|
||||
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,27 @@ 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(),
|
||||
certificateAltNames: z.string().optional(),
|
||||
certificateStatus: z.string().optional(),
|
||||
certificateNotBefore: z.date().optional(),
|
||||
certificateNotAfter: z.date().optional(),
|
||||
certificateRenewBeforeDays: z.number().nullish(),
|
||||
certificateRenewalError: z.string().nullish(),
|
||||
pkiSyncName: z.string().optional(),
|
||||
pkiSyncDestination: z.string().optional()
|
||||
});
|
||||
|
||||
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@@ -111,7 +134,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 +144,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 +203,163 @@ 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 { certificates, totalCount, pkiSyncInfo } = await server.services.pkiSync.listPkiSyncCertificates(
|
||||
{ pkiSyncId, offset, limit },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNC_CERTIFICATES,
|
||||
metadata: {
|
||||
syncId: pkiSyncId,
|
||||
destination: pkiSyncInfo.destination,
|
||||
count: certificates.length,
|
||||
certificateIds: certificates.map((c) => c.certificateId)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { certificates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:pkiSyncId/certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "Add certificates to a PKI Sync.",
|
||||
params: z.object({
|
||||
pkiSyncId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
addedCertificates: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
pkiSyncId: z.string().uuid(),
|
||||
certificateId: z.string().uuid(),
|
||||
syncStatus: z.string().default("pending").optional().nullable(),
|
||||
lastSyncMessage: z.string().optional().nullable(),
|
||||
lastSyncedAt: z.date().optional().nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { certificateIds } = req.body;
|
||||
|
||||
const { addedCertificates, pkiSyncInfo } = await server.services.pkiSync.addCertificatesToPkiSync(
|
||||
{ pkiSyncId, certificateIds },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSyncInfo.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { addedCertificates };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:pkiSyncId/certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "Remove certificates from a PKI Sync.",
|
||||
params: z.object({
|
||||
pkiSyncId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
removedCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { certificateIds } = req.body;
|
||||
|
||||
const { removedCount, pkiSyncInfo } = await server.services.pkiSync.removeCertificatesFromPkiSync(
|
||||
{ pkiSyncId, certificateIds },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSyncInfo.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { removedCount };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1195,8 +1195,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
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({
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerChefSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Chef,
|
||||
server,
|
||||
responseSchema: ChefSyncSchema,
|
||||
createSchema: CreateChefSyncSchema,
|
||||
updateSchema: UpdateChefSyncSchema
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
||||
import { registerBitbucketSyncRouter } from "./bitbucket-sync-router";
|
||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
||||
import { registerChefSyncRouter } from "./chef-sync-router";
|
||||
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
||||
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
@@ -67,5 +68,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter
|
||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
|
||||
[SecretSync.Chef]: registerChefSyncRouter
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/s
|
||||
import { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket";
|
||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
||||
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
@@ -88,7 +89,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
NetlifySyncSchema,
|
||||
NorthflankSyncSchema,
|
||||
BitbucketSyncSchema,
|
||||
LaravelForgeSyncSchema
|
||||
LaravelForgeSyncSchema,
|
||||
ChefSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -123,7 +125,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
NetlifySyncListItemSchema,
|
||||
NorthflankSyncListItemSchema,
|
||||
BitbucketSyncListItemSchema,
|
||||
LaravelForgeSyncListItemSchema
|
||||
LaravelForgeSyncListItemSchema,
|
||||
ChefSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum AppConnection {
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef",
|
||||
Northflank = "northflank"
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
} from "./bitbucket";
|
||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
||||
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
@@ -210,7 +211,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getNetlifyConnectionListItem(),
|
||||
getNorthflankConnectionListItem(),
|
||||
getOktaConnectionListItem(),
|
||||
getRedisConnectionListItem()
|
||||
getRedisConnectionListItem(),
|
||||
getChefConnectionListItem()
|
||||
]
|
||||
.filter((option) => {
|
||||
switch (projectType) {
|
||||
@@ -341,6 +343,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
@@ -409,6 +412,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return "API Key";
|
||||
case ChefConnectionMethod.UserKey:
|
||||
return "User Key";
|
||||
case SupabaseConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
default:
|
||||
@@ -483,7 +488,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta",
|
||||
[AppConnection.Redis]: "Redis",
|
||||
[AppConnection.Chef]: "Chef",
|
||||
[AppConnection.Northflank]: "Northflank"
|
||||
};
|
||||
|
||||
@@ -85,5 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Chef]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -67,6 +67,8 @@ import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
||||
import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateChefConnectionCredentialsSchema } from "./chef";
|
||||
import { chefConnectionService } from "./chef/chef-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
@@ -174,7 +176,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -881,6 +884,7 @@ export const appConnectionServiceFactory = ({
|
||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||
northflank: northflankConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById),
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||
chef: chefConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,6 +82,12 @@ import {
|
||||
TChecklyConnectionInput,
|
||||
TValidateChecklyConnectionCredentialsSchema
|
||||
} from "./checkly";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefConnectionInput,
|
||||
TValidateChefConnectionCredentialsSchema
|
||||
} from "./chef";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
@@ -282,6 +288,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -330,6 +337,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TNorthflankConnectionInput
|
||||
| TOktaConnectionInput
|
||||
| TRedisConnectionInput
|
||||
| TChefConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -395,7 +403,8 @@ export type TAppConnectionConfig =
|
||||
| TNetlifyConnectionConfig
|
||||
| TNorthflankConnectionConfig
|
||||
| TOktaConnectionConfig
|
||||
| TRedisConnectionConfig;
|
||||
| TRedisConnectionConfig
|
||||
| TChefConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -438,7 +447,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateNetlifyConnectionCredentialsSchema
|
||||
| TValidateNorthflankConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema
|
||||
| TValidateRedisConnectionCredentialsSchema;
|
||||
| TValidateRedisConnectionCredentialsSchema
|
||||
| TValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum ChefConnectionMethod {
|
||||
UserKey = "user-key"
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { AxiosError } from "axios";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { ChefConnectionMethod } from "./chef-connection-enums";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefDataBag,
|
||||
TChefDataBagItem,
|
||||
TGetChefDataBagItem,
|
||||
TUpdateChefDataBagItem
|
||||
} from "./chef-connection-types";
|
||||
|
||||
export const getChefServerUrl = async (serverUrl?: string) => {
|
||||
const chefServerUrl = serverUrl ? removeTrailingSlash(serverUrl) : IntegrationUrls.CHEF_API_URL;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(chefServerUrl);
|
||||
|
||||
return chefServerUrl;
|
||||
};
|
||||
|
||||
// Helper to ensure private key is in proper PEM format
|
||||
const formatPrivateKey = (key: string): string => {
|
||||
let formattedKey = key.trim();
|
||||
|
||||
// Ensure proper line breaks in PEM format (handle escaped newlines)
|
||||
formattedKey = formattedKey.replace(/\\n/g, "\n");
|
||||
|
||||
// Remove any extra whitespace between lines
|
||||
formattedKey = formattedKey.replace(/\n\s+/g, "\n");
|
||||
|
||||
// If key doesn't have headers, add PKCS#1 RSA headers
|
||||
if (!formattedKey.includes("BEGIN")) {
|
||||
formattedKey = `-----BEGIN RSA PRIVATE KEY-----\n${formattedKey}\n-----END RSA PRIVATE KEY-----`;
|
||||
}
|
||||
|
||||
// Ensure the key has proper line breaks after headers and before footers
|
||||
formattedKey = formattedKey.replace(/(-----BEGIN[^-]+-----)\s*/g, "$1\n").replace(/\s*(-----END[^-]+-----)/g, "\n$1");
|
||||
|
||||
// Remove any duplicate newlines
|
||||
formattedKey = formattedKey.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
return formattedKey;
|
||||
};
|
||||
|
||||
const getChefAuthHeaders = (
|
||||
method: string,
|
||||
path: string,
|
||||
body: string,
|
||||
userId: string,
|
||||
privateKey: string,
|
||||
apiVersion: "1.0" | "1.3" = "1.3"
|
||||
) => {
|
||||
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); // Remove milliseconds from timestamp
|
||||
|
||||
// Calculate content hash based on version
|
||||
let contentHash: string;
|
||||
if (apiVersion === "1.3") {
|
||||
contentHash = crypto.createHash("sha256").update(body).digest("base64");
|
||||
} else {
|
||||
contentHash = crypto.createHash("sha1").update(body).digest("base64");
|
||||
}
|
||||
|
||||
// Build canonical request based on version
|
||||
let canonicalRequest: string;
|
||||
if (apiVersion === "1.3") {
|
||||
canonicalRequest = [
|
||||
`Method:${method}`,
|
||||
`Path:${path}`,
|
||||
`X-Ops-Content-Hash:${contentHash}`,
|
||||
"X-Ops-Sign:version=1.3",
|
||||
`X-Ops-Timestamp:${timestamp}`,
|
||||
`X-Ops-UserId:${userId}`,
|
||||
"X-Ops-Server-API-Version:1"
|
||||
].join("\n");
|
||||
} else {
|
||||
const hashedPath = crypto.createHash("sha1").update(path).digest("base64");
|
||||
canonicalRequest = [
|
||||
`Method:${method}`,
|
||||
`Hashed Path:${hashedPath}`,
|
||||
`X-Ops-Content-Hash:${contentHash}`,
|
||||
`X-Ops-Timestamp:${timestamp}`,
|
||||
`X-Ops-UserId:${userId}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// Format the private key properly
|
||||
const formattedKey = formatPrivateKey(privateKey);
|
||||
|
||||
// Sign the canonical request
|
||||
const sign = crypto.createSign(apiVersion === "1.3" ? "RSA-SHA256" : "RSA-SHA1");
|
||||
sign.update(canonicalRequest);
|
||||
const signature = sign.sign(formattedKey, "base64");
|
||||
|
||||
// Split signature into 60-character chunks
|
||||
const authHeaders: Record<string, string> = {};
|
||||
const signatureLines = signature.match(/.{1,60}/g) || [];
|
||||
signatureLines.forEach((line, index) => {
|
||||
authHeaders[`X-Ops-Authorization-${index + 1}`] = line;
|
||||
});
|
||||
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Chef-Version": "14.0.0",
|
||||
"X-Ops-Timestamp": timestamp,
|
||||
"X-Ops-UserId": userId,
|
||||
"X-Ops-Sign": apiVersion === "1.3" ? "version=1.3" : "algorithm=sha1;version=1.0",
|
||||
"X-Ops-Content-Hash": contentHash,
|
||||
...(apiVersion === "1.3" && { "X-Ops-Server-API-Version": "1" }),
|
||||
...authHeaders
|
||||
};
|
||||
};
|
||||
|
||||
export const getChefConnectionListItem = () => {
|
||||
return {
|
||||
name: "Chef" as const,
|
||||
app: AppConnection.Chef as const,
|
||||
methods: Object.values(ChefConnectionMethod) as [ChefConnectionMethod.UserKey]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateChefConnectionCredentials = async (config: TChefConnectionConfig) => {
|
||||
const { credentials: inputCredentials } = config;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${inputCredentials.orgName}/users/${inputCredentials.userName}`;
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(inputCredentials.serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey);
|
||||
|
||||
await request.get(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate Chef credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate Chef connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listChefDataBags = async (appConnection: TChefConnection): Promise<TChefDataBag[]> => {
|
||||
const {
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return Object.keys(res.data).map((name) => ({
|
||||
name
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Chef data bags: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Chef data bags"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listChefDataBagItems = async (
|
||||
appConnection: TChefConnection,
|
||||
dataBagName: string
|
||||
): Promise<TChefDataBagItem[]> => {
|
||||
const {
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return Object.keys(res.data).map((name) => ({
|
||||
name
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Chef data bag items: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Chef data bag items"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
}: TGetChefDataBagItem): Promise<TChefDataBagItemContent> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<TChefDataBagItemContent>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to get Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName,
|
||||
data
|
||||
}: TUpdateChefDataBagItem): Promise<void> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("PUT", path, body, userName, privateKey);
|
||||
|
||||
await request.put(`${hostServerUrl}${path}`, data, {
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to update Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { ChefConnectionMethod } from "./chef-connection-enums";
|
||||
|
||||
export const ChefConnectionUserKeyCredentialsSchema = z.object({
|
||||
serverUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Valid Chef Server URL required")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.serverUrl),
|
||||
orgName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Organization name required")
|
||||
.max(256, "Organization name cannot exceed 256 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.orgName),
|
||||
userName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "User name required")
|
||||
.max(256, "User name cannot exceed 256 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.userName),
|
||||
privateKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Private key required")
|
||||
.max(16384, "Private key cannot exceed 16384 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.privateKey)
|
||||
});
|
||||
|
||||
const BaseChefConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Chef) });
|
||||
|
||||
export const ChefConnectionSchema = BaseChefConnectionSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedChefConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseChefConnectionSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true })
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(ChefConnectionMethod.UserKey).describe(AppConnections.CREATE(AppConnection.Chef).method),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Chef).credentials)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateChefConnectionSchema = ValidateChefConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Chef)
|
||||
);
|
||||
|
||||
export const UpdateChefConnectionSchema = z
|
||||
.object({
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Chef).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef));
|
||||
|
||||
export const ChefConnectionListItemSchema = z.object({
|
||||
name: z.literal("Chef"),
|
||||
app: z.literal(AppConnection.Chef),
|
||||
methods: z.nativeEnum(ChefConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns";
|
||||
import { TChefConnection } from "./chef-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TChefConnection>;
|
||||
|
||||
export const chefConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listDataBags = async (appConnectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new ForbiddenRequestError({ message: "App connection not found" });
|
||||
}
|
||||
|
||||
return listChefDataBags(appConnection);
|
||||
};
|
||||
|
||||
const listDataBagItems = async (appConnectionId: string, dataBagName: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new ForbiddenRequestError({ message: "App connection not found" });
|
||||
}
|
||||
|
||||
return listChefDataBagItems(appConnection, dataBagName);
|
||||
};
|
||||
|
||||
return {
|
||||
listDataBags,
|
||||
listDataBagItems
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { TChefDataBagItemContent } from "@app/services/secret-sync/chef";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
ChefConnectionSchema,
|
||||
CreateChefConnectionSchema,
|
||||
ValidateChefConnectionCredentialsSchema
|
||||
} from "./chef-connection-schemas";
|
||||
|
||||
export type TChefConnection = z.infer<typeof ChefConnectionSchema>;
|
||||
|
||||
export type TChefConnectionInput = z.infer<typeof CreateChefConnectionSchema> & {
|
||||
app: AppConnection.Chef;
|
||||
};
|
||||
|
||||
export type TValidateChefConnectionCredentialsSchema = typeof ValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TChefConnectionConfig = DiscriminativePick<TChefConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
export type TChefDataBag = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TChefDataBagItem = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TGetChefDataBagItem = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
};
|
||||
|
||||
export type TUpdateChefDataBagItem = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
data: TChefDataBagItemContent;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-connection-enums";
|
||||
export * from "./chef-connection-fns";
|
||||
export * from "./chef-connection-schemas";
|
||||
export * from "./chef-connection-types";
|
||||
@@ -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[]> = {
|
||||
|
||||
@@ -10,10 +10,8 @@ import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileCertificate,
|
||||
TCertificateProfileInsert,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileUpdate,
|
||||
TCertificateProfileWithConfigs,
|
||||
TCertificateProfileWithRawMetrics
|
||||
TCertificateProfileWithConfigs
|
||||
} from "./certificate-profile-types";
|
||||
|
||||
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
||||
@@ -203,21 +201,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
includeMetrics?: boolean;
|
||||
expiringDays?: number;
|
||||
} = {},
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
|
||||
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||
try {
|
||||
const {
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId,
|
||||
includeMetrics = false,
|
||||
expiringDays = 7
|
||||
} = options;
|
||||
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
||||
|
||||
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||
`${TableName.PkiCertificateProfile}.projectId`,
|
||||
@@ -242,7 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||
}
|
||||
|
||||
let query = baseQuery
|
||||
const query = baseQuery
|
||||
.leftJoin(
|
||||
TableName.PkiEstEnrollmentConfig,
|
||||
`${TableName.PkiCertificateProfile}.estConfigId`,
|
||||
@@ -267,52 +255,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
||||
);
|
||||
|
||||
if (includeMetrics) {
|
||||
query = query.leftJoin(
|
||||
TableName.Certificate,
|
||||
`${TableName.PkiCertificateProfile}.id`,
|
||||
`${TableName.Certificate}.profileId`
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const expiringDate = new Date();
|
||||
expiringDate.setDate(now.getDate() + expiringDays);
|
||||
|
||||
query = query
|
||||
.select(
|
||||
selectAllTableCols(TableName.PkiCertificateProfile),
|
||||
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
|
||||
db
|
||||
.ref("disableBootstrapCaValidation")
|
||||
.withSchema(TableName.PkiEstEnrollmentConfig)
|
||||
.as("estDisableBootstrapCaValidation"),
|
||||
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
|
||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
|
||||
db.raw("COUNT(certificates.id) as total_certificates"),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
||||
[expiringDate]
|
||||
),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
|
||||
[now]
|
||||
),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
|
||||
[now, expiringDate]
|
||||
),
|
||||
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
||||
)
|
||||
.groupBy(
|
||||
`${TableName.PkiCertificateProfile}.id`,
|
||||
`${TableName.PkiEstEnrollmentConfig}.id`,
|
||||
`${TableName.PkiApiEnrollmentConfig}.id`
|
||||
);
|
||||
}
|
||||
|
||||
const results = (await query
|
||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
||||
.offset(offset)
|
||||
@@ -353,17 +295,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
apiConfig
|
||||
};
|
||||
|
||||
if (includeMetrics) {
|
||||
return {
|
||||
...baseProfile,
|
||||
total_certificates: result.total_certificates,
|
||||
active_certificates: result.active_certificates,
|
||||
expired_certificates: result.expired_certificates,
|
||||
expiring_certificates: result.expiring_certificates,
|
||||
revoked_certificates: result.revoked_certificates
|
||||
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
|
||||
}
|
||||
|
||||
return baseProfile as TCertificateProfileWithConfigs;
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -485,45 +416,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getProfileMetrics = async (
|
||||
profileId: string,
|
||||
expiringDays: number = 7,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateProfileMetrics> => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const expiringDate = new Date();
|
||||
expiringDate.setDate(now.getDate() + expiringDays);
|
||||
|
||||
const metrics = await (tx || db)(TableName.Certificate)
|
||||
.where("profileId", profileId)
|
||||
.select(
|
||||
db.raw("COUNT(*) as total_certificates"),
|
||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
|
||||
expiringDate
|
||||
]),
|
||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
|
||||
db.raw(
|
||||
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
|
||||
[now, expiringDate]
|
||||
),
|
||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
||||
)
|
||||
.first();
|
||||
|
||||
return {
|
||||
profileId,
|
||||
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
|
||||
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
|
||||
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
|
||||
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
|
||||
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
|
||||
}
|
||||
};
|
||||
|
||||
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
||||
@@ -546,7 +438,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
countByProjectId,
|
||||
findByNameAndProjectId,
|
||||
getCertificatesByProfile,
|
||||
getProfileMetrics,
|
||||
isProfileInUse
|
||||
};
|
||||
};
|
||||
|
||||
@@ -127,8 +127,3 @@ export const listCertificatesByProfileSchema = z.object({
|
||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
||||
search: z.string().optional()
|
||||
});
|
||||
|
||||
export const getCertificateProfileMetricsSchema = z.object({
|
||||
profileId: z.string().uuid(),
|
||||
expiringDays: z.coerce.number().min(1).max(365).default(30)
|
||||
});
|
||||
|
||||
@@ -47,7 +47,6 @@ describe("CertificateProfileService", () => {
|
||||
findByNameAndProjectId: vi.fn(),
|
||||
findByIdWithConfigs: vi.fn(),
|
||||
getCertificatesByProfile: vi.fn(),
|
||||
getProfileMetrics: vi.fn(),
|
||||
isProfileInUse: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
find: vi.fn(),
|
||||
@@ -493,9 +492,7 @@ describe("CertificateProfileService", () => {
|
||||
limit: 20,
|
||||
search: undefined,
|
||||
enrollmentType: undefined,
|
||||
caId: undefined,
|
||||
includeMetrics: false,
|
||||
expiringDays: 30
|
||||
caId: undefined
|
||||
});
|
||||
});
|
||||
|
||||
@@ -515,51 +512,7 @@ describe("CertificateProfileService", () => {
|
||||
limit: 5,
|
||||
search: "test",
|
||||
enrollmentType: EnrollmentType.API,
|
||||
caId: "ca-123",
|
||||
includeMetrics: false,
|
||||
expiringDays: 30
|
||||
});
|
||||
});
|
||||
|
||||
it("should list profiles with metrics when includeMetrics is true", async () => {
|
||||
const mockProfilesWithMetrics = [
|
||||
{
|
||||
...sampleProfile,
|
||||
total_certificates: 10,
|
||||
active_certificates: 8,
|
||||
expired_certificates: 1,
|
||||
expiring_certificates: 1,
|
||||
revoked_certificates: 0
|
||||
}
|
||||
];
|
||||
(mockCertificateProfileDAL.findByProjectId as any).mockResolvedValue(mockProfilesWithMetrics);
|
||||
|
||||
const result = await service.listProfiles({
|
||||
...mockActor,
|
||||
projectId: "project-123",
|
||||
includeMetrics: true,
|
||||
expiringDays: 15
|
||||
});
|
||||
|
||||
expect(result.profiles).toHaveLength(1);
|
||||
expect(result.profiles[0]).toHaveProperty("metrics");
|
||||
expect(result.profiles[0].metrics).toEqual({
|
||||
profileId: sampleProfile.id,
|
||||
totalCertificates: 10,
|
||||
activeCertificates: 8,
|
||||
expiredCertificates: 1,
|
||||
expiringCertificates: 1,
|
||||
revokedCertificates: 0
|
||||
});
|
||||
|
||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
search: undefined,
|
||||
enrollmentType: undefined,
|
||||
caId: undefined,
|
||||
includeMetrics: true,
|
||||
expiringDays: 15
|
||||
caId: "ca-123"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -659,54 +612,6 @@ describe("CertificateProfileService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProfileMetrics", () => {
|
||||
const mockMetrics = {
|
||||
profileId: "profile-123",
|
||||
totalCertificates: 10,
|
||||
activeCertificates: 8,
|
||||
expiredCertificates: 1,
|
||||
expiringCertificates: 2,
|
||||
revokedCertificates: 1
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(mockMetrics);
|
||||
});
|
||||
|
||||
it("should get profile metrics successfully", async () => {
|
||||
const result = await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockMetrics);
|
||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 30);
|
||||
});
|
||||
|
||||
it("should get profile metrics with custom expiring days", async () => {
|
||||
await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123",
|
||||
expiringDays: 60
|
||||
});
|
||||
|
||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 60);
|
||||
});
|
||||
|
||||
it("should throw NotFoundError when profile not found", async () => {
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
})
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("comprehensive certificate profile scenarios", () => {
|
||||
describe("profile configuration validation", () => {
|
||||
it("should validate EST enrollment configuration", async () => {
|
||||
@@ -929,53 +834,6 @@ describe("CertificateProfileService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("metrics and monitoring", () => {
|
||||
it("should calculate profile metrics correctly", async () => {
|
||||
const detailedMetrics = {
|
||||
profileId: "profile-123",
|
||||
totalCertificates: 50,
|
||||
activeCertificates: 40,
|
||||
expiredCertificates: 5,
|
||||
expiringCertificates: 3,
|
||||
revokedCertificates: 2
|
||||
};
|
||||
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(detailedMetrics);
|
||||
|
||||
const result = await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123",
|
||||
expiringDays: 14
|
||||
});
|
||||
|
||||
expect(result).toEqual(detailedMetrics);
|
||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 14);
|
||||
});
|
||||
|
||||
it("should handle zero certificate metrics", async () => {
|
||||
const emptyMetrics = {
|
||||
profileId: "profile-123",
|
||||
totalCertificates: 0,
|
||||
activeCertificates: 0,
|
||||
expiredCertificates: 0,
|
||||
expiringCertificates: 0,
|
||||
revokedCertificates: 0
|
||||
};
|
||||
|
||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(emptyMetrics);
|
||||
|
||||
const result = await service.getProfileMetrics({
|
||||
...mockActor,
|
||||
profileId: "profile-123"
|
||||
});
|
||||
|
||||
expect(result.totalCertificates).toBe(0);
|
||||
expect(result.activeCertificates).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error scenarios", () => {
|
||||
it("should handle database connection errors gracefully", async () => {
|
||||
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
|
||||
|
||||
@@ -27,10 +27,8 @@ import {
|
||||
TCertificateProfile,
|
||||
TCertificateProfileCertificate,
|
||||
TCertificateProfileInsert,
|
||||
TCertificateProfileMetrics,
|
||||
TCertificateProfileUpdate,
|
||||
TCertificateProfileWithConfigs,
|
||||
TCertificateProfileWithRawMetrics
|
||||
TCertificateProfileWithConfigs
|
||||
} from "./certificate-profile-types";
|
||||
|
||||
const validateAndEncryptPemCaChain = async (
|
||||
@@ -361,18 +359,14 @@ export const certificateProfileServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
includeMetrics = false,
|
||||
expiringDays = 30
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
includeMetrics?: boolean;
|
||||
expiringDays?: number;
|
||||
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
|
||||
}): Promise<TCertificateProfile> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
@@ -393,14 +387,6 @@ export const certificateProfileServiceFactory = ({
|
||||
|
||||
const converted = convertDalToService(profile);
|
||||
|
||||
if (includeMetrics) {
|
||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
||||
return {
|
||||
...converted,
|
||||
metrics
|
||||
};
|
||||
}
|
||||
|
||||
return converted;
|
||||
};
|
||||
|
||||
@@ -506,9 +492,7 @@ export const certificateProfileServiceFactory = ({
|
||||
limit = 20,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId,
|
||||
includeMetrics = false,
|
||||
expiringDays = 30
|
||||
caId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
@@ -520,10 +504,8 @@ export const certificateProfileServiceFactory = ({
|
||||
search?: string;
|
||||
enrollmentType?: EnrollmentType;
|
||||
caId?: string;
|
||||
includeMetrics?: boolean;
|
||||
expiringDays?: number;
|
||||
}): Promise<{
|
||||
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
|
||||
profiles: TCertificateProfileWithConfigs[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
@@ -544,9 +526,7 @@ export const certificateProfileServiceFactory = ({
|
||||
limit,
|
||||
search,
|
||||
enrollmentType,
|
||||
caId,
|
||||
includeMetrics,
|
||||
expiringDays
|
||||
caId
|
||||
});
|
||||
|
||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||
@@ -591,27 +571,12 @@ export const certificateProfileServiceFactory = ({
|
||||
}
|
||||
|
||||
const converted = convertDalToService(profileWithConfigs);
|
||||
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
|
||||
const result: TCertificateProfileWithConfigs = {
|
||||
...converted,
|
||||
estConfig: decryptedEstConfig,
|
||||
apiConfig: profileWithConfigs.apiConfig
|
||||
};
|
||||
|
||||
if (includeMetrics) {
|
||||
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
|
||||
result = {
|
||||
...result,
|
||||
metrics: {
|
||||
profileId: converted.id,
|
||||
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
|
||||
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
|
||||
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
|
||||
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
|
||||
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
@@ -709,43 +674,6 @@ export const certificateProfileServiceFactory = ({
|
||||
return certificates;
|
||||
};
|
||||
|
||||
const getProfileMetrics = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
expiringDays = 30
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
expiringDays?: number;
|
||||
}): Promise<TCertificateProfileMetrics> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: profile.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
const getEstConfigurationByProfile = async (
|
||||
params:
|
||||
| {
|
||||
@@ -818,7 +746,6 @@ export const certificateProfileServiceFactory = ({
|
||||
listProfiles,
|
||||
deleteProfile,
|
||||
getProfileCertificates,
|
||||
getProfileMetrics,
|
||||
getEstConfigurationByProfile
|
||||
};
|
||||
};
|
||||
|
||||
@@ -54,18 +54,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||
autoRenew: boolean;
|
||||
renewBeforeDays?: number;
|
||||
};
|
||||
metrics?: TCertificateProfileMetrics;
|
||||
};
|
||||
|
||||
export interface TCertificateProfileMetrics {
|
||||
profileId: string;
|
||||
totalCertificates: number;
|
||||
activeCertificates: number;
|
||||
expiredCertificates: number;
|
||||
expiringCertificates: number;
|
||||
revokedCertificates: number;
|
||||
}
|
||||
|
||||
export interface TCertificateProfileCertificate {
|
||||
id: string;
|
||||
serialNumber: string;
|
||||
@@ -76,11 +66,3 @@ export interface TCertificateProfileCertificate {
|
||||
revokedAt: Date | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
|
||||
total_certificates?: string;
|
||||
active_certificates?: string;
|
||||
expired_certificates?: string;
|
||||
expiring_certificates?: string;
|
||||
revoked_certificates?: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TCertificateSyncs } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
import { CertificateSyncStatus } from "./certificate-sync-enums";
|
||||
|
||||
export type TCertificateSyncDALFactory = ReturnType<typeof certificateSyncDALFactory>;
|
||||
|
||||
type CertificateSyncFindFilter = Parameters<typeof buildFindFilter<TCertificateSyncs>>[0];
|
||||
|
||||
export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||
const certificateSyncOrm = ormify(db, TableName.CertificateSync);
|
||||
|
||||
const findByPkiSyncId = async (pkiSyncId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||
.where({ pkiSyncId })
|
||||
.select(selectAllTableCols(TableName.CertificateSync));
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByPkiSyncId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByCertificateId = async (certificateId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||
.where({ certificateId })
|
||||
.select(selectAllTableCols(TableName.CertificateSync));
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCertificateId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByPkiSyncAndCertificate = async (pkiSyncId: string, certificateId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||
.where({ pkiSyncId, certificateId })
|
||||
.select(selectAllTableCols(TableName.CertificateSync))
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByPkiSyncAndCertificate" });
|
||||
}
|
||||
};
|
||||
|
||||
const findCertificateIdsByPkiSyncId = async (pkiSyncId: string, tx?: Knex): Promise<string[]> => {
|
||||
try {
|
||||
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||
.where({ pkiSyncId })
|
||||
.select("certificateId")) as Array<{ certificateId: string }>;
|
||||
return docs.map((doc) => doc.certificateId);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindCertificateIdsByPkiSyncId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findPkiSyncIdsByCertificateId = async (certificateId: string, tx?: Knex): Promise<string[]> => {
|
||||
try {
|
||||
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||
.where({ certificateId })
|
||||
.select("pkiSyncId")) as Array<{ pkiSyncId: string }>;
|
||||
return docs.map((doc) => doc.pkiSyncId);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindPkiSyncIdsByCertificateId" });
|
||||
}
|
||||
};
|
||||
|
||||
const addCertificates = async (
|
||||
pkiSyncId: string,
|
||||
certificateData: Array<{ certificateId: string; externalIdentifier?: string }>,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateSyncs[]> => {
|
||||
try {
|
||||
const insertData = certificateData.map(({ certificateId, externalIdentifier }) => ({
|
||||
pkiSyncId,
|
||||
certificateId,
|
||||
syncStatus: CertificateSyncStatus.Pending,
|
||||
externalIdentifier
|
||||
}));
|
||||
|
||||
const docs = await (tx || db)(TableName.CertificateSync).insert(insertData).returning("*");
|
||||
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "AddCertificates" });
|
||||
}
|
||||
};
|
||||
|
||||
const removeCertificates = async (pkiSyncId: string, certificateIds: string[], tx?: Knex): Promise<number> => {
|
||||
try {
|
||||
const deletedCount = await (tx || db)(TableName.CertificateSync)
|
||||
.where({ pkiSyncId })
|
||||
.whereIn("certificateId", certificateIds)
|
||||
.del();
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "RemoveCertificates" });
|
||||
}
|
||||
};
|
||||
|
||||
const removeAllCertificatesFromSync = async (pkiSyncId: string, tx?: Knex): Promise<number> => {
|
||||
try {
|
||||
const deletedCount = await (tx || db)(TableName.CertificateSync).where({ pkiSyncId }).del();
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "RemoveAllCertificatesFromSync" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateSyncStatus = async (
|
||||
pkiSyncId: string,
|
||||
certificateId: string,
|
||||
status: string,
|
||||
message?: string,
|
||||
tx?: Knex
|
||||
): Promise<TCertificateSyncs | undefined> => {
|
||||
try {
|
||||
const updateData: Partial<TCertificateSyncs> = {
|
||||
syncStatus: status,
|
||||
lastSyncedAt: new Date()
|
||||
};
|
||||
|
||||
if (message !== undefined) {
|
||||
updateData.lastSyncMessage = message;
|
||||
}
|
||||
|
||||
const docs = await (tx || db)(TableName.CertificateSync)
|
||||
.where({ pkiSyncId, certificateId })
|
||||
.update(updateData)
|
||||
.returning("*");
|
||||
|
||||
return docs[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "UpdateSyncStatus" });
|
||||
}
|
||||
};
|
||||
|
||||
const bulkUpdateSyncStatus = async (
|
||||
updates: Array<{
|
||||
pkiSyncId: string;
|
||||
certificateId: string;
|
||||
status: string;
|
||||
message?: string;
|
||||
}>,
|
||||
tx?: Knex
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (tx) {
|
||||
for (const update of updates) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, tx);
|
||||
}
|
||||
} else {
|
||||
await certificateSyncOrm.transaction(async (trx) => {
|
||||
for (const update of updates) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, trx);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "BulkUpdateSyncStatus" });
|
||||
}
|
||||
};
|
||||
|
||||
const findWithDetails = async (
|
||||
options: {
|
||||
filter?: CertificateSyncFindFilter;
|
||||
pkiSyncId?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
},
|
||||
tx?: Knex
|
||||
): Promise<{
|
||||
certificateDetails: (TCertificateSyncs & {
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
certificateRenewBeforeDays?: number | null;
|
||||
certificateRenewedByCertificateId?: string;
|
||||
certificateRenewalError?: string;
|
||||
pkiSyncName?: string;
|
||||
pkiSyncDestination?: string;
|
||||
})[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
try {
|
||||
const { filter, pkiSyncId, offset, limit } = options;
|
||||
|
||||
const baseQuery = (tx || db.replicaNode())(TableName.CertificateSync)
|
||||
.leftJoin(TableName.Certificate, `${TableName.CertificateSync}.certificateId`, `${TableName.Certificate}.id`)
|
||||
.leftJoin(TableName.PkiSync, `${TableName.CertificateSync}.pkiSyncId`, `${TableName.PkiSync}.id`);
|
||||
|
||||
if (filter) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
void baseQuery.where(buildFindFilter(filter));
|
||||
}
|
||||
if (pkiSyncId) {
|
||||
void baseQuery.where(`${TableName.CertificateSync}.pkiSyncId`, pkiSyncId);
|
||||
}
|
||||
|
||||
const countResult = await baseQuery.clone().count("* as count");
|
||||
const totalCount = Number((countResult[0] as unknown as { count: string | number }).count);
|
||||
|
||||
const query = baseQuery
|
||||
.select(selectAllTableCols(TableName.CertificateSync))
|
||||
.select(
|
||||
db.ref("serialNumber").withSchema(TableName.Certificate).as("certificateSerialNumber"),
|
||||
db.ref("commonName").withSchema(TableName.Certificate).as("certificateCommonName"),
|
||||
db.ref("altNames").withSchema(TableName.Certificate).as("certificateAltNames"),
|
||||
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
|
||||
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
|
||||
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
|
||||
db.ref("renewBeforeDays").withSchema(TableName.Certificate).as("certificateRenewBeforeDays"),
|
||||
db.ref("renewedByCertificateId").withSchema(TableName.Certificate).as("certificateRenewedByCertificateId"),
|
||||
db.ref("renewalError").withSchema(TableName.Certificate).as("certificateRenewalError"),
|
||||
db.ref("name").withSchema(TableName.PkiSync).as("pkiSyncName"),
|
||||
db.ref("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination")
|
||||
)
|
||||
.orderBy(`${TableName.CertificateSync}.createdAt`, "desc");
|
||||
|
||||
if (offset !== undefined) {
|
||||
void query.offset(offset);
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
void query.limit(limit);
|
||||
}
|
||||
|
||||
const certificateDetails = (await query) as (TCertificateSyncs & {
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
certificateRenewBeforeDays?: number;
|
||||
certificateRenewedByCertificateId?: string;
|
||||
certificateRenewalError?: string;
|
||||
pkiSyncName?: string;
|
||||
pkiSyncDestination?: string;
|
||||
})[];
|
||||
|
||||
return { certificateDetails, totalCount };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindWithDetails" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...certificateSyncOrm,
|
||||
findByPkiSyncId,
|
||||
findByCertificateId,
|
||||
findByPkiSyncAndCertificate,
|
||||
findCertificateIdsByPkiSyncId,
|
||||
findPkiSyncIdsByCertificateId,
|
||||
addCertificates,
|
||||
removeCertificates,
|
||||
removeAllCertificatesFromSync,
|
||||
updateSyncStatus,
|
||||
bulkUpdateSyncStatus,
|
||||
findWithDetails
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum CertificateSyncStatus {
|
||||
Pending = "pending",
|
||||
Syncing = "syncing",
|
||||
Succeeded = "succeeded",
|
||||
Failed = "failed",
|
||||
Running = "running"
|
||||
}
|
||||
@@ -133,7 +133,18 @@ describe("CertificateV3Service", () => {
|
||||
certificateProfileDAL: mockCertificateProfileDAL,
|
||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||
internalCaService: mockInternalCaService,
|
||||
permissionService: mockPermissionService
|
||||
permissionService: mockPermissionService,
|
||||
certificateSyncDAL: {
|
||||
findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]),
|
||||
addCertificates: vi.fn().mockResolvedValue([]),
|
||||
findByPkiSyncAndCertificate: vi.fn().mockResolvedValue(null)
|
||||
},
|
||||
pkiSyncDAL: {
|
||||
find: vi.fn().mockResolvedValue([])
|
||||
},
|
||||
pkiSyncQueue: {
|
||||
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ import {
|
||||
mapEnumsForValidation,
|
||||
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 { addRenewedCertificateToSyncs, 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" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||
>;
|
||||
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 addRenewedCertificateToSyncs(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
|
||||
|
||||
@@ -104,7 +104,8 @@ export enum IntegrationUrls {
|
||||
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations"
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
||||
CHEF_API_URL = "https://api.chef.io"
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async () => {
|
||||
|
||||
@@ -3,7 +3,9 @@ import * as AWS from "aws-sdk";
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TCertificateSyncs } from "@app/db/schemas";
|
||||
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";
|
||||
@@ -14,6 +16,9 @@ import {
|
||||
AwsConnectionAssumeRoleCredentialsSchema
|
||||
} from "@app/services/app-connection/aws/aws-connection-schemas";
|
||||
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
|
||||
@@ -88,39 +93,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 }
|
||||
): boolean => {
|
||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const certNameTag = findInfisicalCertificateTag(existingCert.Tags);
|
||||
|
||||
if (!certNameTag || !certNameTag.Value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return certNameTag.Value === certName;
|
||||
};
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const validateCertificateNameSchema = (schema: string): void => {
|
||||
if (!schema.includes("{{certificateId}}")) {
|
||||
throw new Error(
|
||||
@@ -174,6 +146,21 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
|
||||
return sanitizedCertificateName;
|
||||
};
|
||||
|
||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "removeCertificates"
|
||||
| "addCertificates"
|
||||
| "findByPkiSyncAndCertificate"
|
||||
| "updateSyncStatus"
|
||||
| "updateById"
|
||||
| "findByPkiSyncId"
|
||||
>;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||
};
|
||||
|
||||
const getAwsAcmClient = async (
|
||||
connectionId: string,
|
||||
region: AWSRegion,
|
||||
@@ -230,7 +217,9 @@ const getAwsAcmClient = async (
|
||||
|
||||
export const awsCertificateManagerPkiSyncFactory = ({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
appConnectionDAL,
|
||||
certificateSyncDAL,
|
||||
certificateDAL
|
||||
}: TAwsCertificateManagerPkiSyncFactoryDeps) => {
|
||||
const deleteCertificateFromAcm = async (
|
||||
acm: AWS.ACM,
|
||||
@@ -392,79 +381,201 @@ 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 acmCertificatesByArn = new Map<string, (typeof acmCertificates)[string]>();
|
||||
Object.values(acmCertificates).forEach((acmCert) => {
|
||||
if (acmCert.arn) {
|
||||
acmCertificatesByArn.set(acmCert.arn, acmCert);
|
||||
}
|
||||
});
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||
|
||||
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||
if (record.certificateId) {
|
||||
syncRecordsByCertId.set(record.certificateId, record);
|
||||
}
|
||||
if (record.externalIdentifier) {
|
||||
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||
}
|
||||
});
|
||||
|
||||
const setCertificates: CertificateImportRequest[] = [];
|
||||
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean; canRemoveCertificates?: boolean } | undefined;
|
||||
const preserveArn = syncOptions?.preserveArn ?? true;
|
||||
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||
|
||||
Object.entries(certificateMap).forEach(([certName, certData]) => {
|
||||
const { cert, privateKey, certificateChain } = certData;
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
const activeExternalIdentifiers = new Set<string>();
|
||||
|
||||
const existingCert = Object.values(acmCertificates).find((acmCert) =>
|
||||
validateCertificateIdentification(certName, acmCert)
|
||||
);
|
||||
|
||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
||||
for (const [certName, certData] of Object.entries(certificateMap)) {
|
||||
const { cert, privateKey, certificateChain, certificateId } = 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}`
|
||||
});
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldUpdateCert) {
|
||||
if (preserveArn && certificateId && typeof certificateId === "string") {
|
||||
const certificate = await certificateDAL.findById(certificateId);
|
||||
if (certificate?.renewedByCertificateId) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const certificateName = generateCertificateName(certName, pkiSync);
|
||||
|
||||
let targetArn: string | undefined;
|
||||
let shouldCreateNew = false;
|
||||
|
||||
if (!certificateId || typeof certificateId !== "string") {
|
||||
shouldCreateNew = true;
|
||||
} else {
|
||||
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||
const isRenewal = !!currentCertificate?.renewedFromCertificateId;
|
||||
|
||||
if (isRenewal) {
|
||||
const currentSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||
const oldCertificateId = currentCertificate.renewedFromCertificateId;
|
||||
const oldSyncRecord = oldCertificateId ? syncRecordsByCertId.get(oldCertificateId) : undefined;
|
||||
|
||||
if (currentSyncRecord?.externalIdentifier) {
|
||||
const existingAcmCert = acmCertificatesByArn.get(currentSyncRecord.externalIdentifier);
|
||||
|
||||
if (existingAcmCert) {
|
||||
if (!preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||
shouldCreateNew = true;
|
||||
} else if (preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||
targetArn = currentSyncRecord.externalIdentifier;
|
||||
shouldCreateNew = true;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
|
||||
if (oldCertificateId && oldSyncRecord) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||
}
|
||||
} else {
|
||||
targetArn = currentSyncRecord.externalIdentifier;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
shouldCreateNew = false;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else if (preserveArn && oldSyncRecord?.externalIdentifier) {
|
||||
const existingAcmCert = acmCertificatesByArn.get(oldSyncRecord.externalIdentifier);
|
||||
|
||||
if (existingAcmCert) {
|
||||
targetArn = oldSyncRecord.externalIdentifier;
|
||||
shouldCreateNew = true;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
if (oldCertificateId) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||
if (existingSyncRecord?.externalIdentifier) {
|
||||
const existingAcmCert = acmCertificatesByArn.get(existingSyncRecord.externalIdentifier);
|
||||
if (existingAcmCert) {
|
||||
targetArn = existingSyncRecord.externalIdentifier;
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
shouldCreateNew = false;
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCreateNew) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
name: certificateName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain,
|
||||
existingArn: existingCert?.arn
|
||||
existingArn: targetArn,
|
||||
certificateId: certificateId as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify expired/removed certificates that need to be cleaned up from ACM
|
||||
const certificatesToRemove = Object.values(acmCertificates)
|
||||
.filter((acmCert) => {
|
||||
if (!acmCert.arn || !acmCert.Tags) {
|
||||
return false;
|
||||
if (targetArn) {
|
||||
activeExternalIdentifiers.add(targetArn);
|
||||
}
|
||||
}
|
||||
|
||||
const certificatesToRemove: string[] = [];
|
||||
|
||||
if (canRemoveCertificates) {
|
||||
existingSyncRecords.forEach((syncRecord) => {
|
||||
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||
const acmCert = acmCertificatesByArn.get(syncRecord.externalIdentifier);
|
||||
if (acmCert?.arn) {
|
||||
certificatesToRemove.push(acmCert.arn);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const certNameTag = findInfisicalCertificateTag(acmCert.Tags);
|
||||
if (!certNameTag || !certNameTag.Value) {
|
||||
return false;
|
||||
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 isActive = activeCertificateNames.includes(certNameTag.Value);
|
||||
return !isActive;
|
||||
})
|
||||
.map((acmCert) => acmCert.arn!)
|
||||
.filter((arn) => arn);
|
||||
});
|
||||
}
|
||||
|
||||
const uploadResults = await executeWithConcurrencyLimit(
|
||||
setCertificates,
|
||||
async ({ key, name, cert, privateKey, certificateChain, existingArn }) => {
|
||||
async ({ key, name, cert, privateKey, certificateChain, existingArn, certificateId }) => {
|
||||
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 +589,57 @@ 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(), 500);
|
||||
});
|
||||
|
||||
await withRateLimitRetry(
|
||||
() =>
|
||||
acm
|
||||
.addTagsToCertificate({
|
||||
CertificateArn: response.CertificateArn!,
|
||||
Tags: [
|
||||
{
|
||||
Key: INFISICAL_CERTIFICATE_TAG,
|
||||
Value: key
|
||||
}
|
||||
]
|
||||
})
|
||||
.promise(),
|
||||
{
|
||||
operation: "add-tags-to-certificate",
|
||||
syncId: pkiSync.id
|
||||
}
|
||||
);
|
||||
} catch (tagError) {
|
||||
const errorMessage = tagError instanceof Error ? tagError.message : "Unknown tagging error";
|
||||
logger.warn(
|
||||
`Failed to add tags to certificate ${key} (ARN: ${response.CertificateArn}): ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.CertificateArn && certificateId) {
|
||||
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||
if (existingCertSync) {
|
||||
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||
externalIdentifier: response.CertificateArn,
|
||||
syncStatus: CertificateSyncStatus.Succeeded,
|
||||
lastSyncedAt: new Date()
|
||||
});
|
||||
} else {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||
{
|
||||
certificateId,
|
||||
externalIdentifier: response.CertificateArn
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return { key, name, success: true, response };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
@@ -520,15 +682,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 {
|
||||
@@ -567,7 +735,8 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
|
||||
const removeCertificates = async (
|
||||
pkiSync: TPkiSyncWithCredentials,
|
||||
certificateNames: string[]
|
||||
certificateNames: string[],
|
||||
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||
): Promise<RemoveCertificatesResult> => {
|
||||
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
||||
const acm = await getAwsAcmClient(
|
||||
@@ -577,22 +746,33 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
const { acmCertificates } = 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) =>
|
||||
@@ -602,6 +782,38 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
||||
|
||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||
|
||||
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||
for (const failure of failedRemovals) {
|
||||
if (failure.status === "rejected") {
|
||||
const failedArn = certificateArnsToRemove[results.indexOf(failure)];
|
||||
const certificateId = Array.from(certificateIdToArnMap.entries()).find(([, arn]) => arn === failedArn)?.[0];
|
||||
|
||||
if (certificateId) {
|
||||
const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||
await deps.certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
`Failed to remove from AWS: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||
if (successfulRemovals.length > 0) {
|
||||
const successfulArns = new Set(successfulRemovals.map((_, index) => certificateArnsToRemove[index]));
|
||||
|
||||
const certificateIdsToRemove = Array.from(certificateIdToArnMap.entries())
|
||||
.filter(([, arn]) => successfulArns.has(arn))
|
||||
.map(([certificateId]) => certificateId);
|
||||
|
||||
if (certificateIdsToRemove.length > 0) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedRemovals.length > 0) {
|
||||
const failedReasons = failedRemovals.map((failure) => {
|
||||
if (failure.status === "rejected") {
|
||||
|
||||
@@ -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 }>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,4 +56,5 @@ export interface CertificateImportRequest {
|
||||
privateKey: string;
|
||||
certificateChain?: string;
|
||||
existingArn?: string;
|
||||
certificateId?: string;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
import { AxiosError } from "axios";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
import { TCertificateSyncs } from "@app/db/schemas";
|
||||
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";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
|
||||
@@ -32,7 +36,9 @@ const extractCertificateNameFromId = (certificateId: string): string => {
|
||||
};
|
||||
|
||||
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
|
||||
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
|
||||
const syncOptions = pkiSync.syncOptions as
|
||||
| { certificateNameSchema?: string; canRemoveCertificates?: boolean }
|
||||
| undefined;
|
||||
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
||||
|
||||
if (certificateNameSchema) {
|
||||
@@ -46,6 +52,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) => {
|
||||
@@ -188,7 +204,12 @@ const parseCertificateKeyProps = (certPem: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
||||
export const azureKeyVaultPkiSyncFactory = ({
|
||||
kmsService,
|
||||
appConnectionDAL,
|
||||
certificateSyncDAL,
|
||||
certificateDAL
|
||||
}: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
||||
const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
|
||||
const paginateAzureKeyVaultCertificates = async () => {
|
||||
let result: GetAzureKeyVaultCertificate[] = [];
|
||||
@@ -325,48 +346,126 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
pkiSync.id
|
||||
);
|
||||
|
||||
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||
|
||||
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||
if (record.certificateId) {
|
||||
syncRecordsByCertId.set(record.certificateId, record);
|
||||
}
|
||||
if (record.externalIdentifier) {
|
||||
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||
}
|
||||
});
|
||||
|
||||
const setCertificates: {
|
||||
key: string;
|
||||
cert: string;
|
||||
privateKey: string;
|
||||
certificateChain?: string;
|
||||
certificateId?: string;
|
||||
}[] = [];
|
||||
|
||||
// Track which certificates should exist in Azure Key Vault
|
||||
const activeCertificateNames = Object.keys(certificateMap);
|
||||
const syncOptions = pkiSync.syncOptions as
|
||||
| { certificateNameSchema?: string; canRemoveCertificates?: boolean; enableVersioning?: boolean }
|
||||
| undefined;
|
||||
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||
const enableVersioning = syncOptions?.enableVersioning ?? true;
|
||||
|
||||
const activeExternalIdentifiers = new Set<string>();
|
||||
|
||||
// Iterate through certificates to sync to Azure Key Vault
|
||||
Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => {
|
||||
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 (enableVersioning && 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 && enableVersioning) {
|
||||
targetCertName = existingSyncRecord.externalIdentifier;
|
||||
activeExternalIdentifiers.add(targetCertName);
|
||||
|
||||
const shouldUpdateCert = existingAzureCert.cert !== cert;
|
||||
if (shouldUpdateCert) {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else if (!existingAzureCert) {
|
||||
shouldCreateNew = true;
|
||||
} else if (!enableVersioning) {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
} else {
|
||||
shouldCreateNew = true;
|
||||
}
|
||||
|
||||
if (shouldCreateNew || !vaultCertificates[targetCertName] || vaultCertificates[targetCertName].cert !== cert) {
|
||||
setCertificates.push({
|
||||
key: certName,
|
||||
key: targetCertName,
|
||||
cert,
|
||||
privateKey,
|
||||
certificateChain
|
||||
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 = 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 = "";
|
||||
@@ -428,6 +527,31 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
}
|
||||
);
|
||||
|
||||
if (certificateId) {
|
||||
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||
if (existingCertSync) {
|
||||
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||
externalIdentifier: key,
|
||||
syncStatus: CertificateSyncStatus.Succeeded,
|
||||
lastSyncedAt: new Date()
|
||||
});
|
||||
} else {
|
||||
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||
{
|
||||
certificateId,
|
||||
externalIdentifier: key
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
if (enableVersioning) {
|
||||
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||
if (currentCertificate?.renewedFromCertificateId) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { key, success: true, response: response.data as unknown };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
@@ -599,19 +723,43 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
};
|
||||
};
|
||||
|
||||
const removeCertificates = async (pkiSync: TPkiSyncWithCredentials, certificateNames: string[]) => {
|
||||
const removeCertificates = async (
|
||||
pkiSync: TPkiSyncWithCredentials,
|
||||
certificateNames: string[],
|
||||
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||
) => {
|
||||
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
|
||||
|
||||
// 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(
|
||||
@@ -646,8 +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) {
|
||||
for (const failure of failedRemovals) {
|
||||
if (failure.status === "rejected") {
|
||||
const failedCertName = certificateNamesToRemove[results.indexOf(failure)];
|
||||
|
||||
const certificateId = Array.from(certificateIdToNameMap.entries()).find(
|
||||
([, name]) => name === failedCertName
|
||||
)?.[0];
|
||||
|
||||
if (certificateId) {
|
||||
const errorMessage = (failure.reason as Error)?.message || "Unknown error";
|
||||
await deps.certificateSyncDAL.updateSyncStatus(
|
||||
pkiSync.id,
|
||||
certificateId,
|
||||
CertificateSyncStatus.Failed,
|
||||
`Failed to remove from Azure: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||
if (successfulRemovals.length > 0) {
|
||||
const successfulCertNames = new Set(successfulRemovals.map((_, index) => certificateNamesToRemove[index]));
|
||||
|
||||
const certificateIdsToRemove = Array.from(certificateIdToNameMap.entries())
|
||||
.filter(([, name]) => successfulCertNames.has(name))
|
||||
.map(([certificateId]) => certificateId);
|
||||
|
||||
if (certificateIdsToRemove.length > 0) {
|
||||
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedRemovals.length > 0) {
|
||||
const failedReasons = failedRemovals.map((failure) => {
|
||||
if (failure.status === "rejected") {
|
||||
@@ -660,16 +844,16 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
||||
message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`,
|
||||
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),
|
||||
enableVersioning: z.boolean().default(true),
|
||||
certificateNameSchema: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -50,9 +51,10 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
|
||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.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 UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
||||
@@ -61,7 +63,7 @@ export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
||||
isAutoSyncEnabled: z.boolean().optional(),
|
||||
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
|
||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
|
||||
subscriberId: z.string().optional(),
|
||||
subscriberId: z.string().nullish(),
|
||||
connectionId: z.string().optional()
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { z, ZodSchema } from "zod";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
|
||||
@@ -184,6 +186,8 @@ export const PkiSyncFns = {
|
||||
dependencies: {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||
}
|
||||
): Promise<{
|
||||
uploaded: number;
|
||||
@@ -194,17 +198,28 @@ 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) {
|
||||
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: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
|
||||
appConnectionDAL: dependencies.appConnectionDAL,
|
||||
kmsService: dependencies.kmsService,
|
||||
certificateDAL: dependencies.certificateDAL,
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||
});
|
||||
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||
}
|
||||
default:
|
||||
@@ -218,19 +233,38 @@ export const PkiSyncFns = {
|
||||
dependencies: {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateMap: TCertificateMap;
|
||||
}
|
||||
): Promise<void> => {
|
||||
switch (pkiSync.destination) {
|
||||
case PkiSync.AzureKeyVault: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
||||
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames);
|
||||
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
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PkiSync.AwsCertificateManager: {
|
||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
||||
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames);
|
||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
|
||||
appConnectionDAL: dependencies.appConnectionDAL,
|
||||
kmsService: dependencies.kmsService,
|
||||
certificateDAL: dependencies.certificateDAL,
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||
});
|
||||
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||
certificateMap: dependencies.certificateMap
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -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,8 @@ 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 { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||
import { PkiSyncStatus } from "./pki-sync-enums";
|
||||
import { PkiSyncError } from "./pki-sync-errors";
|
||||
@@ -55,14 +58,12 @@ type TPkiSyncQueueFactoryDep = {
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
projectDAL: TProjectDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
certificateDAL: Pick<
|
||||
TCertificateDALFactory,
|
||||
"findLatestActiveCertForSubscriber" | "findAllActiveCertsForSubscriber" | "create"
|
||||
>;
|
||||
certificateDAL: TCertificateDALFactory;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||
};
|
||||
|
||||
type PkiSyncActionJob = Job<
|
||||
@@ -93,7 +94,8 @@ export const pkiSyncQueueFactory = ({
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL
|
||||
certificateAuthorityCertDAL,
|
||||
certificateSyncDAL
|
||||
}: TPkiSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -153,25 +155,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 +262,45 @@ 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,
|
||||
certificateId: certificate.id
|
||||
};
|
||||
|
||||
certificateMetadata.set(certificateName, {
|
||||
id: certificate.id,
|
||||
name: certificateName
|
||||
});
|
||||
} else {
|
||||
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
|
||||
}
|
||||
@@ -281,7 +323,7 @@ export const pkiSyncQueueFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
return certificateMap;
|
||||
return { certificateMap, certificateMetadata };
|
||||
};
|
||||
|
||||
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
|
||||
@@ -348,12 +390,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,11 +413,24 @@ 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: CertificateSyncStatus.Running,
|
||||
message: "Syncing certificate to destination"
|
||||
}));
|
||||
|
||||
if (statusUpdates.length > 0) {
|
||||
await certificateSyncDAL.bulkUpdateSyncStatus(statusUpdates);
|
||||
}
|
||||
|
||||
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
certificateDAL,
|
||||
certificateSyncDAL
|
||||
});
|
||||
|
||||
logger.info(
|
||||
@@ -384,6 +444,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: CertificateSyncStatus.Succeeded,
|
||||
message: "Certificate successfully synced to destination"
|
||||
});
|
||||
}
|
||||
|
||||
if (syncResult.details?.validationErrors) {
|
||||
for (const validationError of syncResult.details.validationErrors) {
|
||||
const metadata = certificateMetadata.get(validationError.name);
|
||||
if (metadata) {
|
||||
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
|
||||
if (updateIndex >= 0) {
|
||||
postSyncUpdates[updateIndex] = {
|
||||
pkiSyncId: pkiSync.id,
|
||||
certificateId: metadata.id,
|
||||
status: CertificateSyncStatus.Failed,
|
||||
message: `${validationError.error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (syncResult.details?.failedUploads) {
|
||||
for (const failure of syncResult.details.failedUploads) {
|
||||
const metadata = certificateMetadata.get(failure.name);
|
||||
if (metadata) {
|
||||
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
|
||||
if (updateIndex >= 0) {
|
||||
postSyncUpdates[updateIndex] = {
|
||||
pkiSyncId: pkiSync.id,
|
||||
certificateId: metadata.id,
|
||||
status: CertificateSyncStatus.Failed,
|
||||
message: `Failed to sync certificate: ${failure.error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postSyncUpdates.length > 0) {
|
||||
await certificateSyncDAL.bulkUpdateSyncStatus(postSyncUpdates);
|
||||
}
|
||||
|
||||
isSynced = true;
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
@@ -550,17 +664,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(
|
||||
{
|
||||
@@ -573,7 +692,10 @@ export const pkiSyncQueueFactory = ({
|
||||
Object.keys(certificateMap),
|
||||
{
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
certificateSyncDAL,
|
||||
certificateDAL,
|
||||
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,17 +10,24 @@ 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";
|
||||
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 +49,17 @@ type TPkiSyncServiceFactoryDep = {
|
||||
TPkiSyncDALFactory,
|
||||
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
||||
>;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "findActiveCertificatesByIds">;
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
| "findByPkiSyncId"
|
||||
| "findByCertificateId"
|
||||
| "findCertificateIdsByPkiSyncId"
|
||||
| "addCertificates"
|
||||
| "removeCertificates"
|
||||
| "removeAllCertificatesFromSync"
|
||||
| "findWithDetails"
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@@ -56,12 +74,41 @@ export type TPkiSyncServiceFactory = ReturnType<typeof pkiSyncServiceFactory>;
|
||||
|
||||
export const pkiSyncServiceFactory = ({
|
||||
pkiSyncDAL,
|
||||
certificateDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
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 invalidProjectCertificates = certificates.filter((cert) => cert.projectId !== expectedProjectId);
|
||||
if (invalidProjectCertificates.length > 0) {
|
||||
throw new BadRequestError({
|
||||
message: `Certificates do not belong to the same project: ${invalidProjectCertificates.map((cert) => cert.id).join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
const invalidRenewedCertificates = certificates.filter((cert) => cert.renewedByCertificateId);
|
||||
if (invalidRenewedCertificates.length > 0) {
|
||||
throw new BadRequestError({
|
||||
message: `Cannot add renewed certificates to PKI sync: ${invalidRenewedCertificates.map((cert) => cert.id).join(", ")}`
|
||||
});
|
||||
}
|
||||
};
|
||||
const createPkiSync = async (
|
||||
{
|
||||
name,
|
||||
@@ -72,7 +119,8 @@ export const pkiSyncServiceFactory = ({
|
||||
syncOptions = {},
|
||||
subscriberId,
|
||||
connectionId,
|
||||
projectId
|
||||
projectId,
|
||||
certificateIds = []
|
||||
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<TPkiSync> => {
|
||||
@@ -114,6 +162,10 @@ export const pkiSyncServiceFactory = ({
|
||||
...syncOptions
|
||||
};
|
||||
|
||||
if (certificateIds.length > 0) {
|
||||
await validateCertificatesProjectOwnership(certificateIds, projectId);
|
||||
}
|
||||
|
||||
try {
|
||||
const pkiSync = await pkiSyncDAL.create({
|
||||
name,
|
||||
@@ -128,6 +180,13 @@ export const pkiSyncServiceFactory = ({
|
||||
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
|
||||
});
|
||||
|
||||
if (certificateIds.length > 0) {
|
||||
await certificateSyncDAL.addCertificates(
|
||||
pkiSync.id,
|
||||
certificateIds.map((id) => ({ certificateId: id }))
|
||||
);
|
||||
}
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
|
||||
}
|
||||
@@ -152,7 +211,8 @@ export const pkiSyncServiceFactory = ({
|
||||
destinationConfig,
|
||||
syncOptions,
|
||||
subscriberId,
|
||||
connectionId
|
||||
connectionId,
|
||||
certificateIds
|
||||
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<TPkiSync> => {
|
||||
@@ -221,6 +281,20 @@ export const pkiSyncServiceFactory = ({
|
||||
};
|
||||
}
|
||||
|
||||
if (certificateIds !== undefined) {
|
||||
if (certificateIds.length > 0) {
|
||||
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||
}
|
||||
|
||||
await certificateSyncDAL.removeAllCertificatesFromSync(id);
|
||||
if (certificateIds.length > 0) {
|
||||
await certificateSyncDAL.addCertificates(
|
||||
id,
|
||||
certificateIds.map((certId) => ({ certificateId: certId }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
|
||||
name,
|
||||
description,
|
||||
@@ -266,7 +340,7 @@ export const pkiSyncServiceFactory = ({
|
||||
};
|
||||
|
||||
const listPkiSyncsByProjectId = async (
|
||||
{ projectId }: TListPkiSyncsByProjectId,
|
||||
{ projectId, certificateId }: TListPkiSyncsByProjectId,
|
||||
actor: OrgServiceActor
|
||||
): Promise<TPkiSync[]> => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
@@ -282,6 +356,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 +530,145 @@ export const pkiSyncServiceFactory = ({
|
||||
return listPkiSyncOptions();
|
||||
};
|
||||
|
||||
const addCertificatesToPkiSync = async (
|
||||
{ pkiSyncId, certificateIds }: Omit<TAddCertificatesToPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<{
|
||||
addedCertificates: TCertificateSyncs[];
|
||||
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||
}> => {
|
||||
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager,
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||
|
||||
const addedCertificates = await certificateSyncDAL.addCertificates(
|
||||
pkiSyncId,
|
||||
certificateIds.map((id) => ({ certificateId: id }))
|
||||
);
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||
}
|
||||
|
||||
return {
|
||||
addedCertificates,
|
||||
pkiSyncInfo: {
|
||||
projectId: pkiSync.projectId,
|
||||
destination: pkiSync.destination,
|
||||
name: pkiSync.name
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const removeCertificatesFromPkiSync = async (
|
||||
{ pkiSyncId, certificateIds }: Omit<TRemoveCertificatesFromPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<{ removedCount: number; pkiSyncInfo: { projectId: string; destination: string; name: string } }> => {
|
||||
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager,
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
|
||||
|
||||
if (pkiSync.isAutoSyncEnabled) {
|
||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||
}
|
||||
|
||||
return {
|
||||
removedCount,
|
||||
pkiSyncInfo: {
|
||||
projectId: pkiSync.projectId,
|
||||
destination: pkiSync.destination,
|
||||
name: pkiSync.name
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const listPkiSyncCertificates = async (
|
||||
{ pkiSyncId, offset = 0, limit = 20 }: Omit<TListPkiSyncCertificatesDTO, "projectId">,
|
||||
actor: OrgServiceActor
|
||||
): Promise<{
|
||||
certificates: TPkiSyncCertificate[];
|
||||
totalCount: number;
|
||||
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||
}> => {
|
||||
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager,
|
||||
projectId: pkiSync.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||
|
||||
const result = await certificateSyncDAL.findWithDetails({
|
||||
pkiSyncId,
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
const { certificateDetails, totalCount } = result;
|
||||
|
||||
const certificates = certificateDetails.map((detail) => ({
|
||||
id: detail.id,
|
||||
pkiSyncId: detail.pkiSyncId,
|
||||
certificateId: detail.certificateId,
|
||||
syncStatus: (detail.syncStatus as CertificateSyncStatus) || CertificateSyncStatus.Pending,
|
||||
lastSyncMessage: detail.lastSyncMessage || undefined,
|
||||
lastSyncedAt: detail.lastSyncedAt || undefined,
|
||||
createdAt: detail.createdAt,
|
||||
updatedAt: detail.updatedAt,
|
||||
certificateSerialNumber: detail.certificateSerialNumber || undefined,
|
||||
certificateCommonName: detail.certificateCommonName || undefined,
|
||||
certificateAltNames: detail.certificateAltNames || undefined,
|
||||
certificateStatus: detail.certificateStatus || undefined,
|
||||
certificateNotBefore: detail.certificateNotBefore || undefined,
|
||||
certificateNotAfter: detail.certificateNotAfter || undefined,
|
||||
certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId
|
||||
? detail.certificateRenewBeforeDays || undefined
|
||||
: undefined,
|
||||
certificateRenewalError: detail.certificateRenewalError || undefined,
|
||||
pkiSyncName: detail.pkiSyncName || undefined,
|
||||
pkiSyncDestination: detail.pkiSyncDestination || undefined
|
||||
}));
|
||||
|
||||
return {
|
||||
certificates,
|
||||
totalCount,
|
||||
pkiSyncInfo: {
|
||||
projectId: pkiSync.projectId,
|
||||
destination: pkiSync.destination,
|
||||
name: pkiSync.name
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createPkiSync,
|
||||
updatePkiSync,
|
||||
@@ -442,6 +678,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[]; certificateId?: 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,48 @@ 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;
|
||||
certificateSerialNumber?: string;
|
||||
certificateCommonName?: string;
|
||||
certificateAltNames?: string;
|
||||
certificateStatus?: string;
|
||||
certificateNotBefore?: Date;
|
||||
certificateNotAfter?: Date;
|
||||
certificateRenewBeforeDays?: number;
|
||||
certificateRenewalError?: string;
|
||||
pkiSyncName?: string;
|
||||
pkiSyncDestination?: string;
|
||||
};
|
||||
|
||||
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
||||
|
||||
export type 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,78 @@ 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,
|
||||
$in: {
|
||||
id: pkiSyncIds
|
||||
}
|
||||
});
|
||||
|
||||
const syncPromises = allPkiSyncs.map((pkiSync) =>
|
||||
dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id })
|
||||
);
|
||||
await Promise.all(syncPromises);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to trigger auto sync for certificate ${certificateId}:`);
|
||||
}
|
||||
};
|
||||
|
||||
export const addRenewedCertificateToSyncs = async (
|
||||
oldCertificateId: string,
|
||||
newCertificateId: string,
|
||||
dependencies: {
|
||||
certificateSyncDAL: Pick<
|
||||
TCertificateSyncDALFactory,
|
||||
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||
>;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(oldCertificateId);
|
||||
|
||||
if (pkiSyncIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addPromises = pkiSyncIds.map(async (pkiSyncId) => {
|
||||
const oldCertificateRecord = await dependencies.certificateSyncDAL.findByPkiSyncAndCertificate(
|
||||
pkiSyncId,
|
||||
oldCertificateId
|
||||
);
|
||||
|
||||
await dependencies.certificateSyncDAL.addCertificates(
|
||||
pkiSyncId,
|
||||
[
|
||||
{
|
||||
certificateId: newCertificateId,
|
||||
externalIdentifier: oldCertificateRecord?.externalIdentifier || undefined
|
||||
}
|
||||
],
|
||||
tx
|
||||
);
|
||||
});
|
||||
await Promise.all(addPromises);
|
||||
|
||||
logger.info(`Successfully added renewed certificate ${newCertificateId} to PKI sync(s)`);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to add renewed certificate ${newCertificateId} to syncs:`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -156,7 +156,14 @@ type TProjectServiceFactoryDep = {
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||
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">;
|
||||
@@ -929,6 +936,7 @@ export const projectServiceFactory = ({
|
||||
offset = 0,
|
||||
friendlyName,
|
||||
commonName,
|
||||
forPkiSync = false,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
@@ -952,20 +960,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;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const CHEF_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Chef",
|
||||
destination: SecretSync.Chef,
|
||||
connection: AppConnection.Chef,
|
||||
canImportSecrets: true
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { getChefDataBagItem, updateChefDataBagItem } from "@app/services/app-connection/chef";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import {
|
||||
ChefSecret,
|
||||
TChefDataBagItemContent,
|
||||
TChefSecret,
|
||||
TChefSecrets,
|
||||
TChefSyncWithCredentials,
|
||||
TGetChefSecrets
|
||||
} from "./chef-sync-types";
|
||||
|
||||
const getChefSecretsRaw = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
}: TGetChefSecrets): Promise<TChefDataBagItemContent> => {
|
||||
const dataBagItem = await getChefDataBagItem({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
});
|
||||
|
||||
// Ensure the data bag item has an id field
|
||||
if (!dataBagItem.id) {
|
||||
dataBagItem.id = dataBagItemName;
|
||||
}
|
||||
|
||||
return dataBagItem;
|
||||
};
|
||||
|
||||
const getChefSecrets = async (secretSync: TChefSyncWithCredentials): Promise<TChefSecrets> => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName, dataBagItemName }
|
||||
} = secretSync;
|
||||
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
const dataBagItem = await getChefSecretsRaw({
|
||||
serverUrl,
|
||||
orgName,
|
||||
userName,
|
||||
privateKey,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
});
|
||||
|
||||
const { id, ...existingSecrets } = dataBagItem;
|
||||
|
||||
// Convert data bag item to key-value pairs
|
||||
const secrets: ChefSecret[] = [];
|
||||
Object.entries(existingSecrets).forEach(([key, value]) => {
|
||||
if (key !== "id" && value !== null && value !== undefined) {
|
||||
secrets.push({ key, value: String(value) });
|
||||
}
|
||||
});
|
||||
|
||||
return { id, secrets };
|
||||
};
|
||||
|
||||
const updateChefSecrets = async (
|
||||
secretSync: TChefSyncWithCredentials,
|
||||
id: string,
|
||||
secrets: Record<string, TChefSecret>
|
||||
) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { dataBagName, dataBagItemName }
|
||||
} = secretSync;
|
||||
|
||||
const { serverUrl, userName, privateKey, orgName } = connection.credentials;
|
||||
|
||||
// Chef data bag items must have an 'id' field
|
||||
const dataBagItemContent: TChefDataBagItemContent = {
|
||||
id,
|
||||
...secrets
|
||||
};
|
||||
|
||||
await updateChefDataBagItem({
|
||||
serverUrl,
|
||||
orgName,
|
||||
userName,
|
||||
privateKey,
|
||||
dataBagName,
|
||||
dataBagItemName,
|
||||
data: dataBagItemContent
|
||||
});
|
||||
};
|
||||
|
||||
export const ChefSyncFns = {
|
||||
async syncSecrets(secretSync: TChefSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const {
|
||||
environment,
|
||||
syncOptions: { disableSecretDeletion, keySchema }
|
||||
} = secretSync;
|
||||
|
||||
const { id, secrets } = await getChefSecrets(secretSync);
|
||||
|
||||
// Create a map of the existing secrets
|
||||
const updatedSecretsMap = new Map(secrets.map((secret) => [secret.key, secret.value]));
|
||||
|
||||
// Add/update new secrets
|
||||
for (const [key, { value }] of Object.entries(secretMap)) {
|
||||
updatedSecretsMap.set(key, value);
|
||||
}
|
||||
|
||||
// Delete secrets if not disabled
|
||||
if (!disableSecretDeletion) {
|
||||
secrets.forEach((secret) => {
|
||||
if (!matchesSchema(secret.key, environment?.slug || "", keySchema)) return;
|
||||
|
||||
if (!secretMap[secret.key]) {
|
||||
updatedSecretsMap.delete(secret.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Convert map to object for Chef API
|
||||
const updatedSecrets = Object.fromEntries(updatedSecretsMap.entries());
|
||||
|
||||
await updateChefSecrets(secretSync, id, updatedSecrets);
|
||||
},
|
||||
|
||||
async getSecrets(secretSync: TChefSyncWithCredentials): Promise<TSecretMap> {
|
||||
const { secrets } = await getChefSecrets(secretSync);
|
||||
|
||||
return Object.fromEntries(secrets.map((secret) => [secret.key, { value: secret.value }]));
|
||||
},
|
||||
|
||||
async removeSecrets(secretSync: TChefSyncWithCredentials, secretMap: TSecretMap) {
|
||||
const { id, secrets: existingSecrets } = await getChefSecrets(secretSync);
|
||||
|
||||
const newSecrets = existingSecrets.filter((secret) => !Object.hasOwn(secretMap, secret.key));
|
||||
|
||||
if (newSecrets.length === existingSecrets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSecrets = Object.fromEntries(newSecrets.map((secret) => [secret.key, secret.value]));
|
||||
|
||||
await updateChefSecrets(secretSync, id, updatedSecrets);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const ChefSyncDestinationConfigSchema = z.object({
|
||||
dataBagName: z
|
||||
.string()
|
||||
.min(1, "Data Bag Name is required")
|
||||
.max(256, "Data Bag Name cannot exceed 256 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.CHEF.dataBagName),
|
||||
dataBagItemName: z
|
||||
.string()
|
||||
.min(1, "Data Bag Item Name is required")
|
||||
.max(256, "Data Bag Item Name cannot exceed 256 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.CHEF.dataBagItemName)
|
||||
});
|
||||
|
||||
const ChefSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const ChefSyncSchema = BaseSecretSyncSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Chef),
|
||||
destinationConfig: ChefSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateChefSyncSchema = GenericCreateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
|
||||
destinationConfig: ChefSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateChefSyncSchema = GenericUpdateSecretSyncFieldsSchema(SecretSync.Chef, ChefSyncOptionsConfig).extend({
|
||||
destinationConfig: ChefSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const ChefSyncListItemSchema = z.object({
|
||||
name: z.literal("Chef"),
|
||||
connection: z.literal(AppConnection.Chef),
|
||||
destination: z.literal(SecretSync.Chef),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import z from "zod";
|
||||
|
||||
import { TChefConnection } from "@app/services/app-connection/chef";
|
||||
|
||||
import { ChefSyncListItemSchema, ChefSyncSchema, CreateChefSyncSchema } from "./chef-sync-schemas";
|
||||
|
||||
export type TChefSyncListItem = z.infer<typeof ChefSyncListItemSchema>;
|
||||
|
||||
export type TChefSync = z.infer<typeof ChefSyncSchema>;
|
||||
|
||||
export type TChefSyncInput = z.infer<typeof CreateChefSyncSchema>;
|
||||
|
||||
export type TChefSyncWithCredentials = TChefSync & {
|
||||
connection: TChefConnection;
|
||||
};
|
||||
|
||||
export type TGetChefSecrets = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
};
|
||||
|
||||
export type TChefSecret = string | number | boolean | null;
|
||||
|
||||
export type TChefDataBagItemContent = {
|
||||
id: string;
|
||||
[key: string]: TChefSecret;
|
||||
};
|
||||
|
||||
export type TChefSecrets = {
|
||||
id: string;
|
||||
secrets: ChefSecret[];
|
||||
};
|
||||
|
||||
export type ChefSecret = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-sync-constants";
|
||||
export * from "./chef-sync-fns";
|
||||
export * from "./chef-sync-schemas";
|
||||
export * from "./chef-sync-types";
|
||||
@@ -30,7 +30,8 @@ export enum SecretSync {
|
||||
Netlify = "netlify",
|
||||
Northflank = "northflank",
|
||||
Bitbucket = "bitbucket",
|
||||
LaravelForge = "laravel-forge"
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -34,6 +34,7 @@ import { BITBUCKET_SYNC_LIST_OPTION, BitbucketSyncFns } from "./bitbucket";
|
||||
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
|
||||
import { CHECKLY_SYNC_LIST_OPTION } from "./checkly/checkly-sync-constants";
|
||||
import { ChecklySyncFns } from "./checkly/checkly-sync-fns";
|
||||
import { CHEF_SYNC_LIST_OPTION, ChefSyncFns } from "./chef";
|
||||
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
|
||||
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
|
||||
import { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers";
|
||||
@@ -49,8 +50,7 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
|
||||
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
|
||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
|
||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
|
||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
||||
@@ -96,7 +96,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION,
|
||||
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
|
||||
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION,
|
||||
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION
|
||||
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION,
|
||||
[SecretSync.Chef]: CHEF_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -286,6 +287,8 @@ export const SecretSyncFns = {
|
||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Chef:
|
||||
return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -408,6 +411,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.LaravelForge:
|
||||
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Chef:
|
||||
secretMap = await ChefSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -505,6 +511,8 @@ export const SecretSyncFns = {
|
||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.LaravelForge:
|
||||
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Chef:
|
||||
return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
||||
@@ -34,7 +34,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Netlify]: "Netlify",
|
||||
[SecretSync.Northflank]: "Northflank",
|
||||
[SecretSync.Bitbucket]: "Bitbucket",
|
||||
[SecretSync.LaravelForge]: "Laravel Forge"
|
||||
[SecretSync.LaravelForge]: "Laravel Forge",
|
||||
[SecretSync.Chef]: "Chef"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -69,7 +70,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Netlify]: AppConnection.Netlify,
|
||||
[SecretSync.Northflank]: AppConnection.Northflank,
|
||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
|
||||
[SecretSync.LaravelForge]: AppConnection.LaravelForge
|
||||
[SecretSync.LaravelForge]: AppConnection.LaravelForge,
|
||||
[SecretSync.Chef]: AppConnection.Chef
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
@@ -104,7 +106,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Netlify]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular
|
||||
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Chef]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
|
||||
@@ -148,7 +151,8 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
|
||||
[SecretSync.Netlify]: ["accountName", "siteName"],
|
||||
[SecretSync.Northflank]: [],
|
||||
[SecretSync.Bitbucket]: [],
|
||||
[SecretSync.LaravelForge]: []
|
||||
[SecretSync.LaravelForge]: [],
|
||||
[SecretSync.Chef]: []
|
||||
};
|
||||
|
||||
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
|
||||
@@ -209,5 +213,6 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
|
||||
[SecretSync.Netlify]: defaultDuplicateCheck,
|
||||
[SecretSync.Northflank]: defaultDuplicateCheck,
|
||||
[SecretSync.Bitbucket]: defaultDuplicateCheck,
|
||||
[SecretSync.LaravelForge]: defaultDuplicateCheck
|
||||
[SecretSync.LaravelForge]: defaultDuplicateCheck,
|
||||
[SecretSync.Chef]: defaultDuplicateCheck
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TCamundaSyncListItem,
|
||||
TCamundaSyncWithCredentials
|
||||
} from "@app/services/secret-sync/camunda";
|
||||
import { TChefSync, TChefSyncInput, TChefSyncListItem, TChefSyncWithCredentials } from "@app/services/secret-sync/chef";
|
||||
import {
|
||||
TDatabricksSync,
|
||||
TDatabricksSyncInput,
|
||||
@@ -169,6 +170,7 @@ export type TSecretSync =
|
||||
| TGitHubSync
|
||||
| TGcpSync
|
||||
| TAzureKeyVaultSync
|
||||
| TChefSync
|
||||
| TAzureAppConfigurationSync
|
||||
| TAzureDevOpsSync
|
||||
| TDatabricksSync
|
||||
@@ -202,6 +204,7 @@ export type TSecretSyncWithCredentials =
|
||||
| TGitHubSyncWithCredentials
|
||||
| TGcpSyncWithCredentials
|
||||
| TAzureKeyVaultSyncWithCredentials
|
||||
| TChefSyncWithCredentials
|
||||
| TAzureAppConfigurationSyncWithCredentials
|
||||
| TAzureDevOpsSyncWithCredentials
|
||||
| TDatabricksSyncWithCredentials
|
||||
@@ -236,6 +239,7 @@ export type TSecretSyncInput =
|
||||
| TGitHubSyncInput
|
||||
| TGcpSyncInput
|
||||
| TAzureKeyVaultSyncInput
|
||||
| TChefSyncInput
|
||||
| TAzureAppConfigurationSyncInput
|
||||
| TAzureDevOpsSyncInput
|
||||
| TDatabricksSyncInput
|
||||
@@ -270,6 +274,7 @@ export type TSecretSyncListItem =
|
||||
| TGitHubSyncListItem
|
||||
| TGcpSyncListItem
|
||||
| TAzureKeyVaultSyncListItem
|
||||
| TChefSyncListItem
|
||||
| TAzureAppConfigurationSyncListItem
|
||||
| TAzureDevOpsSyncListItem
|
||||
| TDatabricksSyncListItem
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/chef/available"
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/chef"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Chef
|
||||
Connections](/integrations/app-connections/chef) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/chef/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/chef/{connectionId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/chef/connection-name/{connectionName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/chef"
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/chef/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Chef
|
||||
Connections](/integrations/app-connections/chef) to learn how to obtain the
|
||||
required credentials.
|
||||
</Note>
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/chef"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/chef/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/chef/{syncId}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/chef/sync-name/{syncName}"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Import Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/import-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/chef"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/remove-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/chef/{syncId}/sync-secrets"
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/chef/{syncId}"
|
||||
---
|
||||
@@ -114,6 +114,7 @@
|
||||
"integrations/app-connections/bitbucket",
|
||||
"integrations/app-connections/camunda",
|
||||
"integrations/app-connections/checkly",
|
||||
"integrations/app-connections/chef",
|
||||
"integrations/app-connections/cloudflare",
|
||||
"integrations/app-connections/databricks",
|
||||
"integrations/app-connections/digital-ocean",
|
||||
@@ -540,6 +541,7 @@
|
||||
"integrations/secret-syncs/bitbucket",
|
||||
"integrations/secret-syncs/camunda",
|
||||
"integrations/secret-syncs/checkly",
|
||||
"integrations/secret-syncs/chef",
|
||||
"integrations/secret-syncs/cloudflare-pages",
|
||||
"integrations/secret-syncs/cloudflare-workers",
|
||||
"integrations/secret-syncs/databricks",
|
||||
@@ -1658,6 +1660,18 @@
|
||||
"api-reference/endpoints/app-connections/checkly/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Chef",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/chef/list",
|
||||
"api-reference/endpoints/app-connections/chef/available",
|
||||
"api-reference/endpoints/app-connections/chef/get-by-id",
|
||||
"api-reference/endpoints/app-connections/chef/get-by-name",
|
||||
"api-reference/endpoints/app-connections/chef/create",
|
||||
"api-reference/endpoints/app-connections/chef/update",
|
||||
"api-reference/endpoints/app-connections/chef/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloudflare",
|
||||
"pages": [
|
||||
@@ -2149,6 +2163,20 @@
|
||||
"api-reference/endpoints/secret-syncs/checkly/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Chef",
|
||||
"pages": [
|
||||
"api-reference/endpoints/secret-syncs/chef/list",
|
||||
"api-reference/endpoints/secret-syncs/chef/get-by-id",
|
||||
"api-reference/endpoints/secret-syncs/chef/get-by-name",
|
||||
"api-reference/endpoints/secret-syncs/chef/create",
|
||||
"api-reference/endpoints/secret-syncs/chef/update",
|
||||
"api-reference/endpoints/secret-syncs/chef/delete",
|
||||
"api-reference/endpoints/secret-syncs/chef/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/chef/import-secrets",
|
||||
"api-reference/endpoints/secret-syncs/chef/remove-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Cloudflare Pages",
|
||||
"pages": [
|
||||
@@ -2304,6 +2332,7 @@
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/update",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/delete",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/sync-secrets",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/import-secrets",
|
||||
"api-reference/endpoints/secret-syncs/laravel-forge/remove-secrets"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 199 KiB |