mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 19:25:51 +00:00
catch up with main
This commit is contained in:
@@ -52,3 +52,4 @@ docs/integrations/app-connections/railway.mdx:generic-api-key:156
|
|||||||
.github/workflows/validate-db-schemas.yml:generic-api-key:21
|
.github/workflows/validate-db-schemas.yml:generic-api-key:21
|
||||||
k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8
|
k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8
|
||||||
docs/integrations/app-connections/redis.mdx:generic-api-key:80
|
docs/integrations/app-connections/redis.mdx:generic-api-key:80
|
||||||
|
backend/src/ee/services/app-connections/chef/chef-connection-fns.ts:private-key:42
|
||||||
|
|||||||
Vendored
+8
@@ -62,6 +62,9 @@ import {
|
|||||||
TCertificateSecretsUpdate,
|
TCertificateSecretsUpdate,
|
||||||
TCertificatesInsert,
|
TCertificatesInsert,
|
||||||
TCertificatesUpdate,
|
TCertificatesUpdate,
|
||||||
|
TCertificateSyncs,
|
||||||
|
TCertificateSyncsInsert,
|
||||||
|
TCertificateSyncsUpdate,
|
||||||
TCertificateTemplateEstConfigs,
|
TCertificateTemplateEstConfigs,
|
||||||
TCertificateTemplateEstConfigsInsert,
|
TCertificateTemplateEstConfigsInsert,
|
||||||
TCertificateTemplateEstConfigsUpdate,
|
TCertificateTemplateEstConfigsUpdate,
|
||||||
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
|
|||||||
TPkiSubscribersUpdate
|
TPkiSubscribersUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
||||||
|
[TableName.CertificateSync]: KnexOriginal.CompositeTableType<
|
||||||
|
TCertificateSyncs,
|
||||||
|
TCertificateSyncsInsert,
|
||||||
|
TCertificateSyncsUpdate
|
||||||
|
>;
|
||||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.CertificateSync))) {
|
||||||
|
await knex.schema.createTable(TableName.CertificateSync, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("pkiSyncId").notNullable();
|
||||||
|
t.foreign("pkiSyncId").references("id").inTable(TableName.PkiSync).onDelete("CASCADE");
|
||||||
|
t.uuid("certificateId").notNullable();
|
||||||
|
t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||||
|
t.string("syncStatus").defaultTo(CertificateSyncStatus.Pending);
|
||||||
|
t.text("lastSyncMessage");
|
||||||
|
t.datetime("lastSyncedAt");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
// Ensure unique combination of pki sync and certificate
|
||||||
|
t.unique(["pkiSyncId", "certificateId"]);
|
||||||
|
|
||||||
|
t.index("pkiSyncId");
|
||||||
|
t.index("certificateId");
|
||||||
|
t.index("syncStatus");
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.CertificateSync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.CertificateSync);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.CertificateSync);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier"))) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||||
|
t.text("externalIdentifier").nullable();
|
||||||
|
t.index("externalIdentifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier")) {
|
||||||
|
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||||
|
t.dropIndex("externalIdentifier");
|
||||||
|
t.dropColumn("externalIdentifier");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const CertificateSyncsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
pkiSyncId: z.string().uuid(),
|
||||||
|
certificateId: z.string().uuid(),
|
||||||
|
syncStatus: z.string().default("pending").nullable().optional(),
|
||||||
|
lastSyncMessage: z.string().nullable().optional(),
|
||||||
|
lastSyncedAt: z.date().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
externalIdentifier: z.string().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCertificateSyncs = z.infer<typeof CertificateSyncsSchema>;
|
||||||
|
export type TCertificateSyncsInsert = Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TCertificateSyncsUpdate = Partial<Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -17,6 +17,7 @@ export * from "./certificate-authority-crl";
|
|||||||
export * from "./certificate-authority-secret";
|
export * from "./certificate-authority-secret";
|
||||||
export * from "./certificate-bodies";
|
export * from "./certificate-bodies";
|
||||||
export * from "./certificate-secrets";
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificate-syncs";
|
||||||
export * from "./certificate-template-est-configs";
|
export * from "./certificate-template-est-configs";
|
||||||
export * from "./certificate-templates";
|
export * from "./certificate-templates";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export enum TableName {
|
|||||||
AppConnection = "app_connections",
|
AppConnection = "app_connections",
|
||||||
SecretSync = "secret_syncs",
|
SecretSync = "secret_syncs",
|
||||||
PkiSync = "pki_syncs",
|
PkiSync = "pki_syncs",
|
||||||
|
CertificateSync = "certificate_syncs",
|
||||||
KmipClient = "kmip_clients",
|
KmipClient = "kmip_clients",
|
||||||
KmipOrgConfig = "kmip_org_configs",
|
KmipOrgConfig = "kmip_org_configs",
|
||||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateChefConnectionSchema,
|
||||||
|
SanitizedChefConnectionSchema,
|
||||||
|
UpdateChefConnectionSchema
|
||||||
|
} from "@app/ee/services/app-connections/chef";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { registerAppConnectionEndpoints } from "@app/server/routes/v1/app-connection-routers/app-connection-endpoints";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -92,7 +92,8 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
gatewayClientCertificate: z.string(),
|
gatewayClientCertificate: z.string(),
|
||||||
gatewayClientPrivateKey: z.string(),
|
gatewayClientPrivateKey: z.string(),
|
||||||
gatewayServerCertificateChain: z.string(),
|
gatewayServerCertificateChain: z.string(),
|
||||||
relayHost: z.string()
|
relayHost: z.string(),
|
||||||
|
metadata: z.record(z.string(), z.string()).optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -468,7 +468,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|||||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
secretComment: z.string().trim().optional().default(""),
|
secretComment: z.string().trim().optional().default(""),
|
||||||
skipMultilineEncoding: z.boolean().optional(),
|
skipMultilineEncoding: z
|
||||||
|
.boolean()
|
||||||
|
.nullish()
|
||||||
|
.transform((val) => (val === null ? false : val)),
|
||||||
metadata: z.record(z.string()).optional(),
|
metadata: z.record(z.string()).optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional(),
|
secretMetadata: ResourceMetadataSchema.optional(),
|
||||||
tagIds: z.string().array().optional()
|
tagIds: z.string().array().optional()
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/ee/services/secret-sync/chef";
|
||||||
|
import { registerSyncSecretsEndpoints } from "@app/server/routes/v1/secret-sync-routers/secret-sync-endpoints";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
export const registerChefSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.Chef,
|
||||||
|
server,
|
||||||
|
responseSchema: ChefSyncSchema,
|
||||||
|
createSchema: CreateChefSyncSchema,
|
||||||
|
updateSchema: UpdateChefSyncSchema
|
||||||
|
});
|
||||||
@@ -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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types";
|
||||||
|
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,57 @@
|
|||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
|
||||||
|
import { TLicenseServiceFactory } from "../../license/license-service";
|
||||||
|
import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns";
|
||||||
|
import { TChefConnection } from "./chef-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TChefConnection>;
|
||||||
|
|
||||||
|
// Enterprise check
|
||||||
|
export const checkPlan = async (licenseService: Pick<TLicenseServiceFactory, "getPlan">, orgId: string) => {
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.enterpriseAppConnections)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chefConnectionService = (
|
||||||
|
getAppConnection: TGetAppConnectionFunc,
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">
|
||||||
|
) => {
|
||||||
|
const listDataBags = async (appConnectionId: string, actor: OrgServiceActor) => {
|
||||||
|
await checkPlan(licenseService, actor.orgId);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
await checkPlan(licenseService, actor.orgId);
|
||||||
|
|
||||||
|
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 { TChefDataBagItemContent } from "@app/ee/services/secret-sync/chef";
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../../../../services/app-connection/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";
|
||||||
@@ -426,6 +426,7 @@ export enum EventType {
|
|||||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||||
GET_PKI_SYNCS = "get-pki-syncs",
|
GET_PKI_SYNCS = "get-pki-syncs",
|
||||||
GET_PKI_SYNC = "get-pki-sync",
|
GET_PKI_SYNC = "get-pki-sync",
|
||||||
|
GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates",
|
||||||
CREATE_PKI_SYNC = "create-pki-sync",
|
CREATE_PKI_SYNC = "create-pki-sync",
|
||||||
UPDATE_PKI_SYNC = "update-pki-sync",
|
UPDATE_PKI_SYNC = "update-pki-sync",
|
||||||
DELETE_PKI_SYNC = "delete-pki-sync",
|
DELETE_PKI_SYNC = "delete-pki-sync",
|
||||||
@@ -3161,6 +3162,16 @@ interface GetPkiSyncEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetPkiSyncCertificatesEvent {
|
||||||
|
type: EventType.GET_PKI_SYNC_CERTIFICATES;
|
||||||
|
metadata: {
|
||||||
|
syncId: string;
|
||||||
|
count: number;
|
||||||
|
certificateIds: string[];
|
||||||
|
destination: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreatePkiSyncEvent {
|
interface CreatePkiSyncEvent {
|
||||||
type: EventType.CREATE_PKI_SYNC;
|
type: EventType.CREATE_PKI_SYNC;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -4329,6 +4340,7 @@ export type Event =
|
|||||||
| SecretSyncRemoveSecretsEvent
|
| SecretSyncRemoveSecretsEvent
|
||||||
| GetPkiSyncsEvent
|
| GetPkiSyncsEvent
|
||||||
| GetPkiSyncEvent
|
| GetPkiSyncEvent
|
||||||
|
| GetPkiSyncCertificatesEvent
|
||||||
| CreatePkiSyncEvent
|
| CreatePkiSyncEvent
|
||||||
| UpdatePkiSyncEvent
|
| UpdatePkiSyncEvent
|
||||||
| DeletePkiSyncEvent
|
| DeletePkiSyncEvent
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import net from "node:net";
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas";
|
import { OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TRelays } from "@app/db/schemas";
|
||||||
import { PgSqlLock } from "@app/keystore/keystore";
|
import { PgSqlLock } from "@app/keystore/keystore";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
@@ -25,7 +25,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
|||||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||||
import { NotificationType } from "@app/services/notification/notification-types";
|
import { NotificationType } from "@app/services/notification/notification-types";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||||
@@ -61,8 +61,7 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
relayDAL,
|
relayDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
notificationService,
|
notificationService
|
||||||
smtpService
|
|
||||||
}: TGatewayV2ServiceFactoryDep) => {
|
}: TGatewayV2ServiceFactoryDep) => {
|
||||||
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||||
@@ -910,7 +909,9 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
|
|
||||||
for await (const [orgId, gateways] of Object.entries(gatewaysByOrg)) {
|
for await (const [orgId, gateways] of Object.entries(gatewaysByOrg)) {
|
||||||
try {
|
try {
|
||||||
const admins = await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin);
|
const admins = (await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin)).filter(
|
||||||
|
(admin) => admin.status !== OrgMembershipStatus.Invited
|
||||||
|
);
|
||||||
if (admins.length === 0) {
|
if (admins.length === 0) {
|
||||||
logger.warn({ orgId }, "Organization has no admins to notify about unhealthy gateway.");
|
logger.warn({ orgId }, "Organization has no admins to notify about unhealthy gateway.");
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
@@ -931,15 +932,17 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
await smtpService.sendMail({
|
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
//
|
||||||
subjectLine: "Gateway Health Alert",
|
// await smtpService.sendMail({
|
||||||
substitutions: {
|
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||||
type: "gateway",
|
// subjectLine: "Gateway Health Alert",
|
||||||
names: gatewayNames
|
// substitutions: {
|
||||||
},
|
// type: "gateway",
|
||||||
template: SmtpTemplates.HealthAlert
|
// names: gatewayNames
|
||||||
});
|
// },
|
||||||
|
// template: SmtpTemplates.HealthAlert
|
||||||
|
// });
|
||||||
|
|
||||||
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
|
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -480,6 +480,36 @@ export const pamAccountServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` });
|
throw new NotFoundError({ message: `Gateway connection details for gateway '${gatewayId}' not found.` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
|
||||||
|
switch (resourceType) {
|
||||||
|
case PamResource.Postgres:
|
||||||
|
case PamResource.MySQL:
|
||||||
|
{
|
||||||
|
const connectionCredentials = await decryptResourceConnectionDetails({
|
||||||
|
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||||
|
kmsService,
|
||||||
|
projectId: account.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
const credentials = await decryptAccountCredentials({
|
||||||
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
|
kmsService,
|
||||||
|
projectId: account.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
username: credentials.username,
|
||||||
|
database: connectionCredentials.database,
|
||||||
|
accountName: account.name,
|
||||||
|
accountPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
resourceType,
|
resourceType,
|
||||||
@@ -491,7 +521,8 @@ export const pamAccountServiceFactory = ({
|
|||||||
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
|
gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain,
|
||||||
relayHost: gatewayConnectionDetails.relayHost,
|
relayHost: gatewayConnectionDetails.relayHost,
|
||||||
projectId: account.projectId,
|
projectId: account.projectId,
|
||||||
account
|
account,
|
||||||
|
metadata
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import knex from "knex";
|
import knex from "knex";
|
||||||
import mysql, { Connection } from "mysql2/promise";
|
import mysql, { Connection } from "mysql2/promise";
|
||||||
import * as pg from "pg";
|
|
||||||
import tls, { PeerCertificate } from "tls";
|
import tls, { PeerCertificate } from "tls";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
@@ -97,7 +96,7 @@ const makeSqlConnection = (
|
|||||||
try {
|
try {
|
||||||
await client.raw(SIMPLE_QUERY);
|
await client.raw(SIMPLE_QUERY);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof pg.DatabaseError) {
|
if (error instanceof Error) {
|
||||||
// Hacky way to know if we successfully hit the database.
|
// Hacky way to know if we successfully hit the database.
|
||||||
// TODO: potentially two approaches to solve the problem.
|
// TODO: potentially two approaches to solve the problem.
|
||||||
// 1. change the work flow, add account first then resource
|
// 1. change the work flow, add account first then resource
|
||||||
|
|||||||
@@ -201,11 +201,11 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
if (actorType === ActorType.USER) {
|
if (actorType === ActorType.USER) {
|
||||||
void queryBuilder
|
void queryBuilder
|
||||||
.on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`)
|
.on(`${TableName.IdentityMetadata}.userId`, db.raw("?", [actorId]))
|
||||||
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
} else if (actorType === ActorType.IDENTITY) {
|
} else if (actorType === ActorType.IDENTITY) {
|
||||||
void queryBuilder
|
void queryBuilder
|
||||||
.on(`${TableName.Membership}.actorIdentityId`, `${TableName.IdentityMetadata}.identityId`)
|
.on(`${TableName.IdentityMetadata}.identityId`, db.raw("?", [actorId]))
|
||||||
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -488,7 +488,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
|||||||
})
|
})
|
||||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
void queryBuilder
|
void queryBuilder
|
||||||
.on(`${TableName.Membership}.actorUserId`, `${TableName.IdentityMetadata}.userId`)
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
.andOn(`${TableName.Membership}.scopeOrgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
})
|
})
|
||||||
.where(`${TableName.Membership}.scopeOrgId`, orgId)
|
.where(`${TableName.Membership}.scopeOrgId`, orgId)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { isIP } from "node:net";
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas";
|
import { OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus, TRelays } from "@app/db/schemas";
|
||||||
import { PgSqlLock } from "@app/keystore/keystore";
|
import { PgSqlLock } from "@app/keystore/keystore";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -996,7 +996,9 @@ export const relayServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existingRelay && (existingRelay.host !== host || existingRelay.name !== name)) {
|
if (existingRelay && (existingRelay.host !== host || existingRelay.name !== name)) {
|
||||||
return relayDAL.updateById(existingRelay.id, { host, name }, tx);
|
throw new BadRequestError({
|
||||||
|
message: `Machine identity already has an existing relay with the name "${existingRelay.name}" and host "${existingRelay.host}". Delete the existing relay or use a different machine identity.`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existingRelay) {
|
if (!existingRelay) {
|
||||||
@@ -1248,7 +1250,9 @@ export const relayServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const admins = await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin);
|
const admins = (await orgDAL.findOrgMembersByRole(orgId, OrgMembershipRole.Admin)).filter(
|
||||||
|
(admin) => admin.status !== OrgMembershipStatus.Invited
|
||||||
|
);
|
||||||
if (admins.length === 0) {
|
if (admins.length === 0) {
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
@@ -1268,15 +1272,17 @@ export const relayServiceFactory = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
await smtpService.sendMail({
|
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
//
|
||||||
subjectLine: "Relay Health Alert",
|
// await smtpService.sendMail({
|
||||||
substitutions: {
|
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||||
type: "relay",
|
// subjectLine: "Relay Health Alert",
|
||||||
names: relayNames
|
// substitutions: {
|
||||||
},
|
// type: "relay",
|
||||||
template: SmtpTemplates.HealthAlert
|
// names: relayNames
|
||||||
});
|
// },
|
||||||
|
// template: SmtpTemplates.HealthAlert
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() })));
|
await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() })));
|
||||||
|
|||||||
@@ -670,6 +670,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.select(
|
.select(
|
||||||
db.ref("projectId").withSchema(TableName.Environment),
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("environmentName"),
|
||||||
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
|
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
|
||||||
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
@@ -699,30 +700,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
)
|
)
|
||||||
.as("inner");
|
.as("inner");
|
||||||
|
|
||||||
const countQuery = (await (tx || db)
|
|
||||||
.select(db.raw("count(*) OVER() as total_count"))
|
|
||||||
.from(innerQuery.clone().distinctOn(`${TableName.SecretApprovalRequest}.id`))) as Array<{
|
|
||||||
total_count: number;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const query = (tx || db).select("*").from(innerQuery).orderBy("createdAt", "desc") as typeof innerQuery;
|
const query = (tx || db).select("*").from(innerQuery).orderBy("createdAt", "desc") as typeof innerQuery;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
void query.where((qb) => {
|
void query.where((qb) => {
|
||||||
void qb
|
void qb
|
||||||
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||||
db.ref("firstName").withSchema("committerUser"),
|
db.ref("committerUserFirstName"),
|
||||||
db.ref("lastName").withSchema("committerUser"),
|
db.ref("committerUserLastName"),
|
||||||
`%${search}%`
|
`%${search}%`
|
||||||
])
|
])
|
||||||
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
.orWhereRaw(`?? ilike ?`, [db.ref("committerUserUsername"), `%${search}%`])
|
||||||
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
.orWhereRaw(`?? ilike ?`, [db.ref("committerUserEmail"), `%${search}%`])
|
||||||
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
.orWhereILike(`environmentName`, `%${search}%`)
|
||||||
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
.orWhereILike(`environment`, `%${search}%`)
|
||||||
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
.orWhereILike(`policySecretPath`, `%${search}%`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const countQuery = (await (tx || db)
|
||||||
|
.select(db.raw("count(*) OVER() as total_count"))
|
||||||
|
.from(query.clone().as("outer"))) as Array<{
|
||||||
|
total_count: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
const rankOffset = offset + 1;
|
const rankOffset = offset + 1;
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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,
|
||||||
|
enterprise: true
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { getChefDataBagItem, updateChefDataBagItem } from "@app/ee/services/app-connections/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,47 @@
|
|||||||
|
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),
|
||||||
|
enterprise: z.boolean()
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { TChefConnection } from "@app/ee/services/app-connections/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";
|
||||||
@@ -2379,6 +2379,12 @@ export const AppConnections = {
|
|||||||
},
|
},
|
||||||
LARAVEL_FORGE: {
|
LARAVEL_FORGE: {
|
||||||
apiToken: "The API token used to authenticate with 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.",
|
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||||
context: "The Netlify context 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: {
|
NORTHFLANK: {
|
||||||
projectId: "The ID of the Northflank project to sync secrets to.",
|
projectId: "The ID of the Northflank project to sync secrets to.",
|
||||||
projectName: "The name 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 { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
||||||
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
||||||
|
import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
@@ -1064,6 +1065,7 @@ export const registerRoutes = async (
|
|||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||||
|
const certificateSyncDAL = certificateSyncDALFactory(db);
|
||||||
|
|
||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||||
@@ -2027,7 +2029,8 @@ export const registerRoutes = async (
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL
|
certificateAuthorityCertDAL,
|
||||||
|
certificateSyncDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||||
@@ -2138,6 +2141,7 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
});
|
});
|
||||||
@@ -2149,7 +2153,10 @@ export const registerRoutes = async (
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
internalCaService: internalCertificateAuthorityService,
|
internalCaService: internalCertificateAuthorityService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
@@ -2191,6 +2198,8 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const pkiSyncService = pkiSyncServiceFactory({
|
const pkiSyncService = pkiSyncServiceFactory({
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectType } from "@app/db/schemas";
|
import { ProjectType } from "@app/db/schemas";
|
||||||
|
import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/ee/services/app-connections/chef";
|
||||||
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci";
|
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci";
|
||||||
import {
|
import {
|
||||||
OracleDBConnectionListItemSchema,
|
OracleDBConnectionListItemSchema,
|
||||||
@@ -168,7 +169,8 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedOktaConnectionSchema.options,
|
...SanitizedOktaConnectionSchema.options,
|
||||||
...SanitizedAzureADCSConnectionSchema.options,
|
...SanitizedAzureADCSConnectionSchema.options,
|
||||||
...SanitizedRedisConnectionSchema.options,
|
...SanitizedRedisConnectionSchema.options,
|
||||||
...SanitizedLaravelForgeConnectionSchema.options
|
...SanitizedLaravelForgeConnectionSchema.options,
|
||||||
|
...SanitizedChefConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -212,7 +214,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
OktaConnectionListItemSchema,
|
OktaConnectionListItemSchema,
|
||||||
AzureADCSConnectionListItemSchema,
|
AzureADCSConnectionListItemSchema,
|
||||||
RedisConnectionListItemSchema,
|
RedisConnectionListItemSchema,
|
||||||
LaravelForgeConnectionListItemSchema
|
LaravelForgeConnectionListItemSchema,
|
||||||
|
ChefConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerChefConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/chef-connection-router";
|
||||||
import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router";
|
import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router";
|
||||||
import { registerOracleDBConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oracledb-connection-router";
|
import { registerOracleDBConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oracledb-connection-router";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
@@ -86,5 +87,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||||
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
[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),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
caId: z.string().uuid().optional(),
|
caId: z.string().uuid().optional()
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
|
||||||
}),
|
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
@@ -232,16 +226,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
renewBeforeDays: z.number().optional()
|
renewBeforeDays: z.number().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
|
||||||
metrics: z
|
|
||||||
.object({
|
|
||||||
profileId: z.string(),
|
|
||||||
totalCertificates: z.number(),
|
|
||||||
activeCertificates: z.number(),
|
|
||||||
expiredCertificates: z.number(),
|
|
||||||
expiringCertificates: z.number(),
|
|
||||||
revokedCertificates: z.number()
|
|
||||||
})
|
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
profileId: req.params.id
|
profileId: req.params.id
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = certificateProfile;
|
|
||||||
|
|
||||||
if (req.query.includeMetrics) {
|
|
||||||
const metrics = await server.services.certificateProfile.getProfileMetrics({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
profileId: req.params.id,
|
|
||||||
expiringDays: req.query.expiringDays
|
|
||||||
});
|
|
||||||
result = { ...certificateProfile, metrics };
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateProfile.projectId,
|
projectId: certificateProfile.projectId,
|
||||||
@@ -283,7 +253,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { certificateProfile: result };
|
return { certificateProfile };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
|
|||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
updateSchema: z.ZodType<{
|
updateSchema: z.ZodType<{
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
@@ -35,7 +35,7 @@ export const registerSyncPkiEndpoints = ({
|
|||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
responseSchema: z.ZodTypeAny;
|
responseSchema: z.ZodTypeAny;
|
||||||
syncOptions: {
|
syncOptions: {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||||
|
|
||||||
const PkiSyncSchema = z.object({
|
const PkiSyncSchema = z.object({
|
||||||
@@ -60,7 +61,8 @@ const PkiSyncSchema = z.object({
|
|||||||
name: z.string()
|
name: z.string()
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional()
|
.optional(),
|
||||||
|
hasCertificate: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const PkiSyncOptionsSchema = z.object({
|
const PkiSyncOptionsSchema = z.object({
|
||||||
@@ -76,6 +78,27 @@ const PkiSyncOptionsSchema = z.object({
|
|||||||
minCertificateNameLength: z.number().optional()
|
minCertificateNameLength: z.number().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PkiSyncCertificateSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
pkiSyncId: z.string().uuid(),
|
||||||
|
certificateId: z.string().uuid(),
|
||||||
|
syncStatus: z.nativeEnum(CertificateSyncStatus),
|
||||||
|
lastSyncMessage: z.string().nullable().optional(),
|
||||||
|
lastSyncedAt: z.date().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
certificateSerialNumber: z.string().optional(),
|
||||||
|
certificateCommonName: z.string().optional(),
|
||||||
|
certificateAltNames: z.string().optional(),
|
||||||
|
certificateStatus: z.string().optional(),
|
||||||
|
certificateNotBefore: z.date().optional(),
|
||||||
|
certificateNotAfter: z.date().optional(),
|
||||||
|
certificateRenewBeforeDays: z.number().nullish(),
|
||||||
|
certificateRenewalError: z.string().nullish(),
|
||||||
|
pkiSyncName: z.string().optional(),
|
||||||
|
pkiSyncDestination: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -111,7 +134,8 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
tags: [ApiDocsTags.PkiSyncs],
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
description: "List all the PKI Syncs for the specified project.",
|
description: "List all the PKI Syncs for the specified project.",
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateId: z.string().uuid().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
||||||
@@ -120,11 +144,11 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
const {
|
||||||
query: { projectId },
|
query: { projectId, certificateId },
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
|
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId, certificateId }, permission);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -179,4 +203,163 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
return pkiSync;
|
return pkiSync;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:pkiSyncId/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
|
description: "List all certificates associated with a PKI Sync.",
|
||||||
|
params: z.object({
|
||||||
|
pkiSyncId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(20)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificates: PkiSyncCertificateSchema.array(),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { pkiSyncId } = req.params;
|
||||||
|
const { offset, limit } = req.query;
|
||||||
|
|
||||||
|
const { certificates, totalCount, pkiSyncInfo } = await server.services.pkiSync.listPkiSyncCertificates(
|
||||||
|
{ pkiSyncId, offset, limit },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: pkiSyncInfo.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PKI_SYNC_CERTIFICATES,
|
||||||
|
metadata: {
|
||||||
|
syncId: pkiSyncId,
|
||||||
|
destination: pkiSyncInfo.destination,
|
||||||
|
count: certificates.length,
|
||||||
|
certificateIds: certificates.map((c) => c.certificateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { certificates, totalCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:pkiSyncId/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
|
description: "Add certificates to a PKI Sync.",
|
||||||
|
params: z.object({
|
||||||
|
pkiSyncId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
addedCertificates: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
pkiSyncId: z.string().uuid(),
|
||||||
|
certificateId: z.string().uuid(),
|
||||||
|
syncStatus: z.string().default("pending").optional().nullable(),
|
||||||
|
lastSyncMessage: z.string().optional().nullable(),
|
||||||
|
lastSyncedAt: z.date().optional().nullable(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { pkiSyncId } = req.params;
|
||||||
|
const { certificateIds } = req.body;
|
||||||
|
|
||||||
|
const { addedCertificates, pkiSyncInfo } = await server.services.pkiSync.addCertificatesToPkiSync(
|
||||||
|
{ pkiSyncId, certificateIds },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: pkiSyncInfo.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_PKI_SYNC,
|
||||||
|
metadata: {
|
||||||
|
pkiSyncId,
|
||||||
|
name: pkiSyncInfo.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { addedCertificates };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:pkiSyncId/certificates",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
|
description: "Remove certificates from a PKI Sync.",
|
||||||
|
params: z.object({
|
||||||
|
pkiSyncId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
removedCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { pkiSyncId } = req.params;
|
||||||
|
const { certificateIds } = req.body;
|
||||||
|
|
||||||
|
const { removedCount, pkiSyncInfo } = await server.services.pkiSync.removeCertificatesFromPkiSync(
|
||||||
|
{ pkiSyncId, certificateIds },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: pkiSyncInfo.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_PKI_SYNC,
|
||||||
|
metadata: {
|
||||||
|
pkiSyncId,
|
||||||
|
name: pkiSyncInfo.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { removedCount };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AccessScope,
|
AccessScope,
|
||||||
|
OrgMembershipRole,
|
||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
ProjectUserMembershipRolesSchema,
|
ProjectUserMembershipRolesSchema,
|
||||||
@@ -266,6 +267,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
|
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
|
||||||
|
|
||||||
|
await server.services.membershipUser.createMembership({
|
||||||
|
permission: req.permission,
|
||||||
|
scopeData: {
|
||||||
|
scope: AccessScope.Organization,
|
||||||
|
orgId: req.permission.orgId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
roles: [{ isTemporary: false, role: OrgMembershipRole.NoAccess }],
|
||||||
|
usernames: usernamesAndEmails
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { memberships } = await server.services.membershipUser.createMembership({
|
const { memberships } = await server.services.membershipUser.createMembership({
|
||||||
permission: req.permission,
|
permission: req.permission,
|
||||||
scopeData: {
|
scopeData: {
|
||||||
|
|||||||
@@ -1195,8 +1195,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
offset: z.coerce.number().min(0).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
|
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit),
|
||||||
|
forPkiSync: z.coerce
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.optional()
|
||||||
|
.describe("Retrieve only certificates available for PKI sync")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerChefSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/chef-sync-router";
|
||||||
import { registerOCIVaultSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/oci-vault-sync-router";
|
import { registerOCIVaultSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/oci-vault-sync-router";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
@@ -67,5 +68,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||||
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
||||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
||||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter
|
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
|
||||||
|
[SecretSync.Chef]: registerChefSyncRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/ee/services/secret-sync/chef";
|
||||||
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/ee/services/secret-sync/oci-vault";
|
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/ee/services/secret-sync/oci-vault";
|
||||||
import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs";
|
import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -88,7 +89,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
NetlifySyncSchema,
|
NetlifySyncSchema,
|
||||||
NorthflankSyncSchema,
|
NorthflankSyncSchema,
|
||||||
BitbucketSyncSchema,
|
BitbucketSyncSchema,
|
||||||
LaravelForgeSyncSchema
|
LaravelForgeSyncSchema,
|
||||||
|
ChefSyncSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
@@ -123,7 +125,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
NetlifySyncListItemSchema,
|
NetlifySyncListItemSchema,
|
||||||
NorthflankSyncListItemSchema,
|
NorthflankSyncListItemSchema,
|
||||||
BitbucketSyncListItemSchema,
|
BitbucketSyncListItemSchema,
|
||||||
LaravelForgeSyncListItemSchema
|
LaravelForgeSyncListItemSchema,
|
||||||
|
ChefSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export enum AppConnection {
|
|||||||
Okta = "okta",
|
Okta = "okta",
|
||||||
Redis = "redis",
|
Redis = "redis",
|
||||||
LaravelForge = "laravel-forge",
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef",
|
||||||
Northflank = "northflank"
|
Northflank = "northflank"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { ProjectType } from "@app/db/schemas";
|
import { ProjectType } from "@app/db/schemas";
|
||||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||||
|
import {
|
||||||
|
ChefConnectionMethod,
|
||||||
|
getChefConnectionListItem,
|
||||||
|
validateChefConnectionCredentials
|
||||||
|
} from "@app/ee/services/app-connections/chef";
|
||||||
import {
|
import {
|
||||||
getOCIConnectionListItem,
|
getOCIConnectionListItem,
|
||||||
OCIConnectionMethod,
|
OCIConnectionMethod,
|
||||||
@@ -210,7 +215,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
|||||||
getNetlifyConnectionListItem(),
|
getNetlifyConnectionListItem(),
|
||||||
getNorthflankConnectionListItem(),
|
getNorthflankConnectionListItem(),
|
||||||
getOktaConnectionListItem(),
|
getOktaConnectionListItem(),
|
||||||
getRedisConnectionListItem()
|
getRedisConnectionListItem(),
|
||||||
|
getChefConnectionListItem()
|
||||||
]
|
]
|
||||||
.filter((option) => {
|
.filter((option) => {
|
||||||
switch (projectType) {
|
switch (projectType) {
|
||||||
@@ -341,6 +347,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,6 +416,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return "API Key";
|
return "API Key";
|
||||||
|
case ChefConnectionMethod.UserKey:
|
||||||
|
return "User Key";
|
||||||
case SupabaseConnectionMethod.AccessToken:
|
case SupabaseConnectionMethod.AccessToken:
|
||||||
return "Access Token";
|
return "Access Token";
|
||||||
default:
|
default:
|
||||||
@@ -483,7 +492,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
|
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enterpriseAppCheck = async (
|
export const enterpriseAppCheck = async (
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.Netlify]: "Netlify",
|
[AppConnection.Netlify]: "Netlify",
|
||||||
[AppConnection.Okta]: "Okta",
|
[AppConnection.Okta]: "Okta",
|
||||||
[AppConnection.Redis]: "Redis",
|
[AppConnection.Redis]: "Redis",
|
||||||
|
[AppConnection.Chef]: "Chef",
|
||||||
[AppConnection.Northflank]: "Northflank"
|
[AppConnection.Northflank]: "Northflank"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,5 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
|||||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||||
|
[AppConnection.Chef]: AppConnectionPlanType.Enterprise,
|
||||||
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType, OrganizationActionScope, TAppConnections } from "@app/db/schemas";
|
import { ActionProjectType, OrganizationActionScope, TAppConnections } from "@app/db/schemas";
|
||||||
|
import { ValidateChefConnectionCredentialsSchema } from "@app/ee/services/app-connections/chef";
|
||||||
|
import { chefConnectionService } from "@app/ee/services/app-connections/chef/chef-connection-service";
|
||||||
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
||||||
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
|
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
|
||||||
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
|
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
|
||||||
@@ -174,7 +176,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||||
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||||
|
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -881,6 +884,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||||
northflank: northflankConnectionService(connectAppConnectionById),
|
northflank: northflankConnectionService(connectAppConnectionById),
|
||||||
okta: oktaConnectionService(connectAppConnectionById),
|
okta: oktaConnectionService(connectAppConnectionById),
|
||||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||||
|
chef: chefConnectionService(connectAppConnectionById, licenseService)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import {
|
||||||
|
TChefConnection,
|
||||||
|
TChefConnectionConfig,
|
||||||
|
TChefConnectionInput,
|
||||||
|
TValidateChefConnectionCredentialsSchema
|
||||||
|
} from "@app/ee/services/app-connections/chef";
|
||||||
import {
|
import {
|
||||||
TOCIConnection,
|
TOCIConnection,
|
||||||
TOCIConnectionConfig,
|
TOCIConnectionConfig,
|
||||||
@@ -282,6 +288,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TNorthflankConnection
|
| TNorthflankConnection
|
||||||
| TOktaConnection
|
| TOktaConnection
|
||||||
| TRedisConnection
|
| TRedisConnection
|
||||||
|
| TChefConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||||
@@ -330,6 +337,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TNorthflankConnectionInput
|
| TNorthflankConnectionInput
|
||||||
| TOktaConnectionInput
|
| TOktaConnectionInput
|
||||||
| TRedisConnectionInput
|
| TRedisConnectionInput
|
||||||
|
| TChefConnectionInput
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TSqlConnectionInput =
|
export type TSqlConnectionInput =
|
||||||
@@ -395,7 +403,8 @@ export type TAppConnectionConfig =
|
|||||||
| TNetlifyConnectionConfig
|
| TNetlifyConnectionConfig
|
||||||
| TNorthflankConnectionConfig
|
| TNorthflankConnectionConfig
|
||||||
| TOktaConnectionConfig
|
| TOktaConnectionConfig
|
||||||
| TRedisConnectionConfig;
|
| TRedisConnectionConfig
|
||||||
|
| TChefConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentialsSchema =
|
export type TValidateAppConnectionCredentialsSchema =
|
||||||
| TValidateAwsConnectionCredentialsSchema
|
| TValidateAwsConnectionCredentialsSchema
|
||||||
@@ -438,7 +447,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateNetlifyConnectionCredentialsSchema
|
| TValidateNetlifyConnectionCredentialsSchema
|
||||||
| TValidateNorthflankConnectionCredentialsSchema
|
| TValidateNorthflankConnectionCredentialsSchema
|
||||||
| TValidateOktaConnectionCredentialsSchema
|
| TValidateOktaConnectionCredentialsSchema
|
||||||
| TValidateRedisConnectionCredentialsSchema;
|
| TValidateRedisConnectionCredentialsSchema
|
||||||
|
| TValidateChefConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
|||||||
+2
-2
@@ -192,7 +192,7 @@ export const castDbEntryToAzureAdCsCertificateAuthority = (
|
|||||||
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||||
): TAzureAdCsCertificateAuthority & { credentials: unknown } => {
|
): TAzureAdCsCertificateAuthority & { credentials: unknown } => {
|
||||||
if (!ca.externalCa?.id) {
|
if (!ca.externalCa?.id) {
|
||||||
throw new BadRequestError({ message: "Malformed Azure AD Certificate Service certificate authority" });
|
throw new BadRequestError({ message: "Malformed Active Directory Certificate Service certificate authority" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ca.externalCa.dnsAppConnectionId) {
|
if (!ca.externalCa.dnsAppConnectionId) {
|
||||||
@@ -776,7 +776,7 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
|||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||||
if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) {
|
if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) {
|
||||||
throw new BadRequestError({ message: "CA is not an Azure AD Certificate Service CA" });
|
throw new BadRequestError({ message: "CA is not an Active Directory Certificate Service CA" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca);
|
const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { CaCapability, CaType } from "./certificate-authority-enums";
|
|||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
||||||
[CaType.INTERNAL]: "Internal",
|
[CaType.INTERNAL]: "Internal",
|
||||||
[CaType.ACME]: "ACME",
|
[CaType.ACME]: "ACME-compatible CA",
|
||||||
[CaType.AZURE_AD_CS]: "Azure AD Certificate Service"
|
[CaType.AZURE_AD_CS]: "Active Directory Certificate Service"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ import {
|
|||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
TCertificateProfileMetrics,
|
|
||||||
TCertificateProfileUpdate,
|
TCertificateProfileUpdate,
|
||||||
TCertificateProfileWithConfigs,
|
TCertificateProfileWithConfigs
|
||||||
TCertificateProfileWithRawMetrics
|
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
|
||||||
@@ -203,21 +201,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
includeMetrics?: boolean;
|
|
||||||
expiringDays?: number;
|
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
|
): Promise<TCertificateProfile[] | TCertificateProfileWithConfigs[]> => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
||||||
offset = 0,
|
|
||||||
limit = 20,
|
|
||||||
search,
|
|
||||||
enrollmentType,
|
|
||||||
caId,
|
|
||||||
includeMetrics = false,
|
|
||||||
expiringDays = 7
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||||
`${TableName.PkiCertificateProfile}.projectId`,
|
`${TableName.PkiCertificateProfile}.projectId`,
|
||||||
@@ -242,7 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = baseQuery
|
const query = baseQuery
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.PkiEstEnrollmentConfig,
|
TableName.PkiEstEnrollmentConfig,
|
||||||
`${TableName.PkiCertificateProfile}.estConfigId`,
|
`${TableName.PkiCertificateProfile}.estConfigId`,
|
||||||
@@ -267,52 +255,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
query = query.leftJoin(
|
|
||||||
TableName.Certificate,
|
|
||||||
`${TableName.PkiCertificateProfile}.id`,
|
|
||||||
`${TableName.Certificate}.profileId`
|
|
||||||
);
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const expiringDate = new Date();
|
|
||||||
expiringDate.setDate(now.getDate() + expiringDays);
|
|
||||||
|
|
||||||
query = query
|
|
||||||
.select(
|
|
||||||
selectAllTableCols(TableName.PkiCertificateProfile),
|
|
||||||
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
|
|
||||||
db
|
|
||||||
.ref("disableBootstrapCaValidation")
|
|
||||||
.withSchema(TableName.PkiEstEnrollmentConfig)
|
|
||||||
.as("estDisableBootstrapCaValidation"),
|
|
||||||
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
|
|
||||||
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
|
|
||||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
|
||||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
|
||||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
|
|
||||||
db.raw("COUNT(certificates.id) as total_certificates"),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
|
||||||
[expiringDate]
|
|
||||||
),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
|
|
||||||
[now]
|
|
||||||
),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
|
|
||||||
[now, expiringDate]
|
|
||||||
),
|
|
||||||
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
|
||||||
)
|
|
||||||
.groupBy(
|
|
||||||
`${TableName.PkiCertificateProfile}.id`,
|
|
||||||
`${TableName.PkiEstEnrollmentConfig}.id`,
|
|
||||||
`${TableName.PkiApiEnrollmentConfig}.id`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = (await query
|
const results = (await query
|
||||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
@@ -353,17 +295,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
apiConfig
|
apiConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
return {
|
|
||||||
...baseProfile,
|
|
||||||
total_certificates: result.total_certificates,
|
|
||||||
active_certificates: result.active_certificates,
|
|
||||||
expired_certificates: result.expired_certificates,
|
|
||||||
expiring_certificates: result.expiring_certificates,
|
|
||||||
revoked_certificates: result.revoked_certificates
|
|
||||||
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseProfile as TCertificateProfileWithConfigs;
|
return baseProfile as TCertificateProfileWithConfigs;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -485,45 +416,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfileMetrics = async (
|
|
||||||
profileId: string,
|
|
||||||
expiringDays: number = 7,
|
|
||||||
tx?: Knex
|
|
||||||
): Promise<TCertificateProfileMetrics> => {
|
|
||||||
try {
|
|
||||||
const now = new Date();
|
|
||||||
const expiringDate = new Date();
|
|
||||||
expiringDate.setDate(now.getDate() + expiringDays);
|
|
||||||
|
|
||||||
const metrics = await (tx || db)(TableName.Certificate)
|
|
||||||
.where("profileId", profileId)
|
|
||||||
.select(
|
|
||||||
db.raw("COUNT(*) as total_certificates"),
|
|
||||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
|
|
||||||
expiringDate
|
|
||||||
]),
|
|
||||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
|
|
||||||
db.raw(
|
|
||||||
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
|
|
||||||
[now, expiringDate]
|
|
||||||
),
|
|
||||||
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
|
||||||
)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return {
|
|
||||||
profileId,
|
|
||||||
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
|
|
||||||
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
|
|
||||||
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
|
|
||||||
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
|
|
||||||
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
||||||
@@ -546,7 +438,6 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
countByProjectId,
|
countByProjectId,
|
||||||
findByNameAndProjectId,
|
findByNameAndProjectId,
|
||||||
getCertificatesByProfile,
|
getCertificatesByProfile,
|
||||||
getProfileMetrics,
|
|
||||||
isProfileInUse
|
isProfileInUse
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -127,8 +127,3 @@ export const listCertificatesByProfileSchema = z.object({
|
|||||||
status: z.enum(["active", "expired", "revoked"]).optional(),
|
status: z.enum(["active", "expired", "revoked"]).optional(),
|
||||||
search: z.string().optional()
|
search: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCertificateProfileMetricsSchema = z.object({
|
|
||||||
profileId: z.string().uuid(),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).default(30)
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ describe("CertificateProfileService", () => {
|
|||||||
findByNameAndProjectId: vi.fn(),
|
findByNameAndProjectId: vi.fn(),
|
||||||
findByIdWithConfigs: vi.fn(),
|
findByIdWithConfigs: vi.fn(),
|
||||||
getCertificatesByProfile: vi.fn(),
|
getCertificatesByProfile: vi.fn(),
|
||||||
getProfileMetrics: vi.fn(),
|
|
||||||
isProfileInUse: vi.fn(),
|
isProfileInUse: vi.fn(),
|
||||||
transaction: vi.fn(),
|
transaction: vi.fn(),
|
||||||
find: vi.fn(),
|
find: vi.fn(),
|
||||||
@@ -493,9 +492,7 @@ describe("CertificateProfileService", () => {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
enrollmentType: undefined,
|
enrollmentType: undefined,
|
||||||
caId: undefined,
|
caId: undefined
|
||||||
includeMetrics: false,
|
|
||||||
expiringDays: 30
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -515,51 +512,7 @@ describe("CertificateProfileService", () => {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
search: "test",
|
search: "test",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123"
|
||||||
includeMetrics: false,
|
|
||||||
expiringDays: 30
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should list profiles with metrics when includeMetrics is true", async () => {
|
|
||||||
const mockProfilesWithMetrics = [
|
|
||||||
{
|
|
||||||
...sampleProfile,
|
|
||||||
total_certificates: 10,
|
|
||||||
active_certificates: 8,
|
|
||||||
expired_certificates: 1,
|
|
||||||
expiring_certificates: 1,
|
|
||||||
revoked_certificates: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
(mockCertificateProfileDAL.findByProjectId as any).mockResolvedValue(mockProfilesWithMetrics);
|
|
||||||
|
|
||||||
const result = await service.listProfiles({
|
|
||||||
...mockActor,
|
|
||||||
projectId: "project-123",
|
|
||||||
includeMetrics: true,
|
|
||||||
expiringDays: 15
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.profiles).toHaveLength(1);
|
|
||||||
expect(result.profiles[0]).toHaveProperty("metrics");
|
|
||||||
expect(result.profiles[0].metrics).toEqual({
|
|
||||||
profileId: sampleProfile.id,
|
|
||||||
totalCertificates: 10,
|
|
||||||
activeCertificates: 8,
|
|
||||||
expiredCertificates: 1,
|
|
||||||
expiringCertificates: 1,
|
|
||||||
revokedCertificates: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockCertificateProfileDAL.findByProjectId).toHaveBeenCalledWith("project-123", {
|
|
||||||
offset: 0,
|
|
||||||
limit: 20,
|
|
||||||
search: undefined,
|
|
||||||
enrollmentType: undefined,
|
|
||||||
caId: undefined,
|
|
||||||
includeMetrics: true,
|
|
||||||
expiringDays: 15
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -659,54 +612,6 @@ describe("CertificateProfileService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getProfileMetrics", () => {
|
|
||||||
const mockMetrics = {
|
|
||||||
profileId: "profile-123",
|
|
||||||
totalCertificates: 10,
|
|
||||||
activeCertificates: 8,
|
|
||||||
expiredCertificates: 1,
|
|
||||||
expiringCertificates: 2,
|
|
||||||
revokedCertificates: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
|
||||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(mockMetrics);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get profile metrics successfully", async () => {
|
|
||||||
const result = await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual(mockMetrics);
|
|
||||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
|
||||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get profile metrics with custom expiring days", async () => {
|
|
||||||
await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123",
|
|
||||||
expiringDays: 60
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 60);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw NotFoundError when profile not found", async () => {
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123"
|
|
||||||
})
|
|
||||||
).rejects.toThrow(NotFoundError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("comprehensive certificate profile scenarios", () => {
|
describe("comprehensive certificate profile scenarios", () => {
|
||||||
describe("profile configuration validation", () => {
|
describe("profile configuration validation", () => {
|
||||||
it("should validate EST enrollment configuration", async () => {
|
it("should validate EST enrollment configuration", async () => {
|
||||||
@@ -929,53 +834,6 @@ describe("CertificateProfileService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("metrics and monitoring", () => {
|
|
||||||
it("should calculate profile metrics correctly", async () => {
|
|
||||||
const detailedMetrics = {
|
|
||||||
profileId: "profile-123",
|
|
||||||
totalCertificates: 50,
|
|
||||||
activeCertificates: 40,
|
|
||||||
expiredCertificates: 5,
|
|
||||||
expiringCertificates: 3,
|
|
||||||
revokedCertificates: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
|
||||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(detailedMetrics);
|
|
||||||
|
|
||||||
const result = await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123",
|
|
||||||
expiringDays: 14
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual(detailedMetrics);
|
|
||||||
expect(mockCertificateProfileDAL.getProfileMetrics).toHaveBeenCalledWith("profile-123", 14);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle zero certificate metrics", async () => {
|
|
||||||
const emptyMetrics = {
|
|
||||||
profileId: "profile-123",
|
|
||||||
totalCertificates: 0,
|
|
||||||
activeCertificates: 0,
|
|
||||||
expiredCertificates: 0,
|
|
||||||
expiringCertificates: 0,
|
|
||||||
revokedCertificates: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
|
||||||
(mockCertificateProfileDAL.getProfileMetrics as any).mockResolvedValue(emptyMetrics);
|
|
||||||
|
|
||||||
const result = await service.getProfileMetrics({
|
|
||||||
...mockActor,
|
|
||||||
profileId: "profile-123"
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.totalCertificates).toBe(0);
|
|
||||||
expect(result.activeCertificates).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("error scenarios", () => {
|
describe("error scenarios", () => {
|
||||||
it("should handle database connection errors gracefully", async () => {
|
it("should handle database connection errors gracefully", async () => {
|
||||||
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
|
(mockCertificateProfileDAL.findById as any).mockRejectedValue(new Error("Database connection failed"));
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ import {
|
|||||||
TCertificateProfile,
|
TCertificateProfile,
|
||||||
TCertificateProfileCertificate,
|
TCertificateProfileCertificate,
|
||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
TCertificateProfileMetrics,
|
|
||||||
TCertificateProfileUpdate,
|
TCertificateProfileUpdate,
|
||||||
TCertificateProfileWithConfigs,
|
TCertificateProfileWithConfigs
|
||||||
TCertificateProfileWithRawMetrics
|
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
const validateAndEncryptPemCaChain = async (
|
const validateAndEncryptPemCaChain = async (
|
||||||
@@ -361,18 +359,14 @@ export const certificateProfileServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
profileId,
|
profileId
|
||||||
includeMetrics = false,
|
|
||||||
expiringDays = 30
|
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
profileId: string;
|
profileId: string;
|
||||||
includeMetrics?: boolean;
|
}): Promise<TCertificateProfile> => {
|
||||||
expiringDays?: number;
|
|
||||||
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
|
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
const profile = await certificateProfileDAL.findById(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
@@ -393,14 +387,6 @@ export const certificateProfileServiceFactory = ({
|
|||||||
|
|
||||||
const converted = convertDalToService(profile);
|
const converted = convertDalToService(profile);
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
|
||||||
return {
|
|
||||||
...converted,
|
|
||||||
metrics
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
return converted;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -506,9 +492,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit = 20,
|
limit = 20,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
caId,
|
caId
|
||||||
includeMetrics = false,
|
|
||||||
expiringDays = 30
|
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -520,10 +504,8 @@ export const certificateProfileServiceFactory = ({
|
|||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
includeMetrics?: boolean;
|
|
||||||
expiringDays?: number;
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
|
profiles: TCertificateProfileWithConfigs[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}> => {
|
}> => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
@@ -544,9 +526,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
caId,
|
caId
|
||||||
includeMetrics,
|
|
||||||
expiringDays
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||||
@@ -591,27 +571,12 @@ export const certificateProfileServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const converted = convertDalToService(profileWithConfigs);
|
const converted = convertDalToService(profileWithConfigs);
|
||||||
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
|
const result: TCertificateProfileWithConfigs = {
|
||||||
...converted,
|
...converted,
|
||||||
estConfig: decryptedEstConfig,
|
estConfig: decryptedEstConfig,
|
||||||
apiConfig: profileWithConfigs.apiConfig
|
apiConfig: profileWithConfigs.apiConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeMetrics) {
|
|
||||||
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
|
|
||||||
result = {
|
|
||||||
...result,
|
|
||||||
metrics: {
|
|
||||||
profileId: converted.id,
|
|
||||||
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
|
|
||||||
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
|
|
||||||
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
|
|
||||||
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
|
|
||||||
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -709,43 +674,6 @@ export const certificateProfileServiceFactory = ({
|
|||||||
return certificates;
|
return certificates;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfileMetrics = async ({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
profileId,
|
|
||||||
expiringDays = 30
|
|
||||||
}: {
|
|
||||||
actor: ActorType;
|
|
||||||
actorId: string;
|
|
||||||
actorAuthMethod: ActorAuthMethod;
|
|
||||||
actorOrgId: string;
|
|
||||||
profileId: string;
|
|
||||||
expiringDays?: number;
|
|
||||||
}): Promise<TCertificateProfileMetrics> => {
|
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
|
||||||
if (!profile) {
|
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
projectId: profile.projectId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId,
|
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionCertificateProfileActions.Read,
|
|
||||||
ProjectPermissionSub.CertificateProfiles
|
|
||||||
);
|
|
||||||
|
|
||||||
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
|
||||||
return metrics;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEstConfigurationByProfile = async (
|
const getEstConfigurationByProfile = async (
|
||||||
params:
|
params:
|
||||||
| {
|
| {
|
||||||
@@ -818,7 +746,6 @@ export const certificateProfileServiceFactory = ({
|
|||||||
listProfiles,
|
listProfiles,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
getProfileCertificates,
|
getProfileCertificates,
|
||||||
getProfileMetrics,
|
|
||||||
getEstConfigurationByProfile
|
getEstConfigurationByProfile
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,18 +54,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
|||||||
autoRenew: boolean;
|
autoRenew: boolean;
|
||||||
renewBeforeDays?: number;
|
renewBeforeDays?: number;
|
||||||
};
|
};
|
||||||
metrics?: TCertificateProfileMetrics;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TCertificateProfileMetrics {
|
|
||||||
profileId: string;
|
|
||||||
totalCertificates: number;
|
|
||||||
activeCertificates: number;
|
|
||||||
expiredCertificates: number;
|
|
||||||
expiringCertificates: number;
|
|
||||||
revokedCertificates: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TCertificateProfileCertificate {
|
export interface TCertificateProfileCertificate {
|
||||||
id: string;
|
id: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
@@ -76,11 +66,3 @@ export interface TCertificateProfileCertificate {
|
|||||||
revokedAt: Date | null;
|
revokedAt: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
|
|
||||||
total_certificates?: string;
|
|
||||||
active_certificates?: string;
|
|
||||||
expired_certificates?: string;
|
|
||||||
expiring_certificates?: string;
|
|
||||||
revoked_certificates?: string;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TCertificateSyncs } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { CertificateSyncStatus } from "./certificate-sync-enums";
|
||||||
|
|
||||||
|
export type TCertificateSyncDALFactory = ReturnType<typeof certificateSyncDALFactory>;
|
||||||
|
|
||||||
|
type CertificateSyncFindFilter = Parameters<typeof buildFindFilter<TCertificateSyncs>>[0];
|
||||||
|
|
||||||
|
export const certificateSyncDALFactory = (db: TDbClient) => {
|
||||||
|
const certificateSyncOrm = ormify(db, TableName.CertificateSync);
|
||||||
|
|
||||||
|
const findByPkiSyncId = async (pkiSyncId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId })
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync));
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByPkiSyncId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByCertificateId = async (certificateId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ certificateId })
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync));
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByCertificateId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByPkiSyncAndCertificate = async (pkiSyncId: string, certificateId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId, certificateId })
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync))
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByPkiSyncAndCertificate" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCertificateIdsByPkiSyncId = async (pkiSyncId: string, tx?: Knex): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId })
|
||||||
|
.select("certificateId")) as Array<{ certificateId: string }>;
|
||||||
|
return docs.map((doc) => doc.certificateId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindCertificateIdsByPkiSyncId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPkiSyncIdsByCertificateId = async (certificateId: string, tx?: Knex): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
const docs = (await (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.where({ certificateId })
|
||||||
|
.select("pkiSyncId")) as Array<{ pkiSyncId: string }>;
|
||||||
|
return docs.map((doc) => doc.pkiSyncId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindPkiSyncIdsByCertificateId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCertificates = async (
|
||||||
|
pkiSyncId: string,
|
||||||
|
certificateData: Array<{ certificateId: string; externalIdentifier?: string }>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TCertificateSyncs[]> => {
|
||||||
|
try {
|
||||||
|
const insertData = certificateData.map(({ certificateId, externalIdentifier }) => ({
|
||||||
|
pkiSyncId,
|
||||||
|
certificateId,
|
||||||
|
syncStatus: CertificateSyncStatus.Pending,
|
||||||
|
externalIdentifier
|
||||||
|
}));
|
||||||
|
|
||||||
|
const docs = await (tx || db)(TableName.CertificateSync).insert(insertData).returning("*");
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "AddCertificates" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCertificates = async (pkiSyncId: string, certificateIds: string[], tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const deletedCount = await (tx || db)(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId })
|
||||||
|
.whereIn("certificateId", certificateIds)
|
||||||
|
.del();
|
||||||
|
|
||||||
|
return deletedCount;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "RemoveCertificates" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAllCertificatesFromSync = async (pkiSyncId: string, tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const deletedCount = await (tx || db)(TableName.CertificateSync).where({ pkiSyncId }).del();
|
||||||
|
return deletedCount;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "RemoveAllCertificatesFromSync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSyncStatus = async (
|
||||||
|
pkiSyncId: string,
|
||||||
|
certificateId: string,
|
||||||
|
status: string,
|
||||||
|
message?: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TCertificateSyncs | undefined> => {
|
||||||
|
try {
|
||||||
|
const updateData: Partial<TCertificateSyncs> = {
|
||||||
|
syncStatus: status,
|
||||||
|
lastSyncedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (message !== undefined) {
|
||||||
|
updateData.lastSyncMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await (tx || db)(TableName.CertificateSync)
|
||||||
|
.where({ pkiSyncId, certificateId })
|
||||||
|
.update(updateData)
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
return docs[0];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "UpdateSyncStatus" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkUpdateSyncStatus = async (
|
||||||
|
updates: Array<{
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (tx) {
|
||||||
|
for (const update of updates) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, tx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await certificateSyncOrm.transaction(async (trx) => {
|
||||||
|
for (const update of updates) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await updateSyncStatus(update.pkiSyncId, update.certificateId, update.status, update.message, trx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "BulkUpdateSyncStatus" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithDetails = async (
|
||||||
|
options: {
|
||||||
|
filter?: CertificateSyncFindFilter;
|
||||||
|
pkiSyncId?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<{
|
||||||
|
certificateDetails: (TCertificateSyncs & {
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number | null;
|
||||||
|
certificateRenewedByCertificateId?: string;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
|
})[];
|
||||||
|
totalCount: number;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const { filter, pkiSyncId, offset, limit } = options;
|
||||||
|
|
||||||
|
const baseQuery = (tx || db.replicaNode())(TableName.CertificateSync)
|
||||||
|
.leftJoin(TableName.Certificate, `${TableName.CertificateSync}.certificateId`, `${TableName.Certificate}.id`)
|
||||||
|
.leftJoin(TableName.PkiSync, `${TableName.CertificateSync}.pkiSyncId`, `${TableName.PkiSync}.id`);
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
void baseQuery.where(buildFindFilter(filter));
|
||||||
|
}
|
||||||
|
if (pkiSyncId) {
|
||||||
|
void baseQuery.where(`${TableName.CertificateSync}.pkiSyncId`, pkiSyncId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const countResult = await baseQuery.clone().count("* as count");
|
||||||
|
const totalCount = Number((countResult[0] as unknown as { count: string | number }).count);
|
||||||
|
|
||||||
|
const query = baseQuery
|
||||||
|
.select(selectAllTableCols(TableName.CertificateSync))
|
||||||
|
.select(
|
||||||
|
db.ref("serialNumber").withSchema(TableName.Certificate).as("certificateSerialNumber"),
|
||||||
|
db.ref("commonName").withSchema(TableName.Certificate).as("certificateCommonName"),
|
||||||
|
db.ref("altNames").withSchema(TableName.Certificate).as("certificateAltNames"),
|
||||||
|
db.ref("status").withSchema(TableName.Certificate).as("certificateStatus"),
|
||||||
|
db.ref("notBefore").withSchema(TableName.Certificate).as("certificateNotBefore"),
|
||||||
|
db.ref("notAfter").withSchema(TableName.Certificate).as("certificateNotAfter"),
|
||||||
|
db.ref("renewBeforeDays").withSchema(TableName.Certificate).as("certificateRenewBeforeDays"),
|
||||||
|
db.ref("renewedByCertificateId").withSchema(TableName.Certificate).as("certificateRenewedByCertificateId"),
|
||||||
|
db.ref("renewalError").withSchema(TableName.Certificate).as("certificateRenewalError"),
|
||||||
|
db.ref("name").withSchema(TableName.PkiSync).as("pkiSyncName"),
|
||||||
|
db.ref("destination").withSchema(TableName.PkiSync).as("pkiSyncDestination")
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.CertificateSync}.createdAt`, "desc");
|
||||||
|
|
||||||
|
if (offset !== undefined) {
|
||||||
|
void query.offset(offset);
|
||||||
|
}
|
||||||
|
if (limit !== undefined) {
|
||||||
|
void query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateDetails = (await query) as (TCertificateSyncs & {
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number;
|
||||||
|
certificateRenewedByCertificateId?: string;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
|
})[];
|
||||||
|
|
||||||
|
return { certificateDetails, totalCount };
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindWithDetails" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...certificateSyncOrm,
|
||||||
|
findByPkiSyncId,
|
||||||
|
findByCertificateId,
|
||||||
|
findByPkiSyncAndCertificate,
|
||||||
|
findCertificateIdsByPkiSyncId,
|
||||||
|
findPkiSyncIdsByCertificateId,
|
||||||
|
addCertificates,
|
||||||
|
removeCertificates,
|
||||||
|
removeAllCertificatesFromSync,
|
||||||
|
updateSyncStatus,
|
||||||
|
bulkUpdateSyncStatus,
|
||||||
|
findWithDetails
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum CertificateSyncStatus {
|
||||||
|
Pending = "pending",
|
||||||
|
Syncing = "syncing",
|
||||||
|
Succeeded = "succeeded",
|
||||||
|
Failed = "failed",
|
||||||
|
Running = "running"
|
||||||
|
}
|
||||||
@@ -133,7 +133,18 @@ describe("CertificateV3Service", () => {
|
|||||||
certificateProfileDAL: mockCertificateProfileDAL,
|
certificateProfileDAL: mockCertificateProfileDAL,
|
||||||
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
certificateTemplateV2Service: mockCertificateTemplateV2Service,
|
||||||
internalCaService: mockInternalCaService,
|
internalCaService: mockInternalCaService,
|
||||||
permissionService: mockPermissionService
|
permissionService: mockPermissionService,
|
||||||
|
certificateSyncDAL: {
|
||||||
|
findPkiSyncIdsByCertificateId: vi.fn().mockResolvedValue([]),
|
||||||
|
addCertificates: vi.fn().mockResolvedValue([]),
|
||||||
|
findByPkiSyncAndCertificate: vi.fn().mockResolvedValue(null)
|
||||||
|
},
|
||||||
|
pkiSyncDAL: {
|
||||||
|
find: vi.fn().mockResolvedValue([])
|
||||||
|
},
|
||||||
|
pkiSyncQueue: {
|
||||||
|
queuePkiSyncSyncCertificatesById: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ import {
|
|||||||
mapEnumsForValidation,
|
mapEnumsForValidation,
|
||||||
normalizeDateForApi
|
normalizeDateForApi
|
||||||
} from "../certificate-common/certificate-utils";
|
} from "../certificate-common/certificate-utils";
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||||
|
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||||
|
import { addRenewedCertificateToSyncs, triggerAutoSyncForCertificate } from "../pki-sync/pki-sync-utils";
|
||||||
import {
|
import {
|
||||||
TCertificateFromProfileResponse,
|
TCertificateFromProfileResponse,
|
||||||
TCertificateOrderResponse,
|
TCertificateOrderResponse,
|
||||||
@@ -72,6 +76,12 @@ type TCertificateV3ServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
|
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||||
|
>;
|
||||||
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
|
||||||
@@ -328,7 +338,10 @@ export const certificateV3ServiceFactory = ({
|
|||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
internalCaService,
|
internalCaService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
}: TCertificateV3ServiceFactoryDep) => {
|
}: TCertificateV3ServiceFactoryDep) => {
|
||||||
const issueCertificateFromProfile = async ({
|
const issueCertificateFromProfile = async ({
|
||||||
profileId,
|
profileId,
|
||||||
@@ -872,6 +885,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await addRenewedCertificateToSyncs(originalCert.id, newCert.id, { certificateSyncDAL }, tx);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
@@ -883,6 +898,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await triggerAutoSyncForCertificate(renewalResult.newCert.id, {
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: renewalResult.certificate,
|
certificate: renewalResult.certificate,
|
||||||
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
issuingCaCertificate: renewalResult.issuingCaCertificate,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TCertificates } from "@app/db/schemas";
|
import { TableName, TCertificates } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
@@ -60,11 +62,13 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
.where(`${TableName.Project}.id`, projectId);
|
.where(`${TableName.Project}.id`, projectId);
|
||||||
|
|
||||||
if (friendlyName) {
|
if (friendlyName) {
|
||||||
query = query.andWhere(`${TableName.Certificate}.friendlyName`, friendlyName);
|
const sanitizedValue = String(friendlyName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.friendlyName`, "like", `%${sanitizedValue}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commonName) {
|
if (commonName) {
|
||||||
query = query.andWhere(`${TableName.Certificate}.commonName`, commonName);
|
const sanitizedValue = String(commonName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.commonName`, "like", `%${sanitizedValue}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = await query.count("*").first();
|
const count = await query.count("*").first();
|
||||||
@@ -114,6 +118,109 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findActiveCertificatesByIds = async (certificateIds: string[]): Promise<TCertificates[]> => {
|
||||||
|
try {
|
||||||
|
if (certificateIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const certs = await db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.whereIn("id", certificateIds)
|
||||||
|
.where({ status: CertStatus.ACTIVE })
|
||||||
|
.where("notAfter", ">", new Date())
|
||||||
|
.orderBy("notBefore", "desc")
|
||||||
|
.select("*");
|
||||||
|
|
||||||
|
return certs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find active certificates by IDs" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findActiveCertificatesForSync = async (
|
||||||
|
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
|
||||||
|
options?: { limit?: number; offset?: number }
|
||||||
|
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||||
|
try {
|
||||||
|
let query = db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
||||||
|
.select(selectAllTableCols(TableName.Certificate))
|
||||||
|
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
|
||||||
|
.where({ status: CertStatus.ACTIVE })
|
||||||
|
.where("notAfter", ">", new Date())
|
||||||
|
.whereNull("renewedByCertificateId");
|
||||||
|
|
||||||
|
Object.entries(filter).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (key === "friendlyName" || key === "commonName") {
|
||||||
|
const sanitizedValue = String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, "like", `%${sanitizedValue}%`);
|
||||||
|
} else {
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options?.offset) {
|
||||||
|
query = query.offset(options.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.limit) {
|
||||||
|
query = query.limit(options.limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.orderBy("createdAt", "desc");
|
||||||
|
|
||||||
|
const certs = await query;
|
||||||
|
return certs.map((cert) => ({ ...cert, hasPrivateKey: Boolean(cert.privateKeyRef) }));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find active certificates for sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const countActiveCertificatesForSync = async ({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
commonName
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName?: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = db
|
||||||
|
.replicaNode()(TableName.Certificate)
|
||||||
|
.join(TableName.CertificateAuthority, `${TableName.Certificate}.caId`, `${TableName.CertificateAuthority}.id`)
|
||||||
|
.join(TableName.Project, `${TableName.CertificateAuthority}.projectId`, `${TableName.Project}.id`)
|
||||||
|
.where(`${TableName.Project}.id`, projectId)
|
||||||
|
.where(`${TableName.Certificate}.status`, CertStatus.ACTIVE)
|
||||||
|
.where(`${TableName.Certificate}.notAfter`, ">", new Date())
|
||||||
|
.whereNull(`${TableName.Certificate}.renewedByCertificateId`);
|
||||||
|
|
||||||
|
if (friendlyName) {
|
||||||
|
const sanitizedValue = String(friendlyName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.friendlyName`, "like", `%${sanitizedValue}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commonName) {
|
||||||
|
const sanitizedValue = String(commonName).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.commonName`, "like", `%${sanitizedValue}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await query.count("*").first();
|
||||||
|
|
||||||
|
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Count active certificates for sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findCertificatesEligibleForRenewal = async ({
|
const findCertificatesEligibleForRenewal = async ({
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
@@ -159,7 +266,7 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findWithPrivateKeyInfo = async (
|
const findWithPrivateKeyInfo = async (
|
||||||
filter: Partial<TCertificates>,
|
filter: Partial<TCertificates & { friendlyName?: string; commonName?: string }>,
|
||||||
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
|
options?: { offset?: number; limit?: number; sort?: [string, "asc" | "desc"][] }
|
||||||
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
): Promise<(TCertificates & { hasPrivateKey: boolean })[]> => {
|
||||||
try {
|
try {
|
||||||
@@ -167,8 +274,18 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
.replicaNode()(TableName.Certificate)
|
.replicaNode()(TableName.Certificate)
|
||||||
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
.leftJoin(TableName.CertificateSecret, `${TableName.Certificate}.id`, `${TableName.CertificateSecret}.certId`)
|
||||||
.select(selectAllTableCols(TableName.Certificate))
|
.select(selectAllTableCols(TableName.Certificate))
|
||||||
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"))
|
.select(db.ref(`${TableName.CertificateSecret}.certId`).as("privateKeyRef"));
|
||||||
.where(filter);
|
|
||||||
|
Object.entries(filter).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (key === "friendlyName" || key === "commonName") {
|
||||||
|
const sanitizedValue = String(value).replace(new RE2("[%_\\\\]", "g"), "\\$&");
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, "like", `%${sanitizedValue}%`);
|
||||||
|
} else {
|
||||||
|
query = query.andWhere(`${TableName.Certificate}.${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (options?.offset) {
|
if (options?.offset) {
|
||||||
query = query.offset(options.offset);
|
query = query.offset(options.offset);
|
||||||
@@ -197,10 +314,13 @@ export const certificateDALFactory = (db: TDbClient) => {
|
|||||||
return {
|
return {
|
||||||
...certificateOrm,
|
...certificateOrm,
|
||||||
countCertificatesInProject,
|
countCertificatesInProject,
|
||||||
|
countActiveCertificatesForSync,
|
||||||
countCertificatesForPkiSubscriber,
|
countCertificatesForPkiSubscriber,
|
||||||
findLatestActiveCertForSubscriber,
|
findLatestActiveCertForSubscriber,
|
||||||
findAllActiveCertsForSubscriber,
|
findAllActiveCertsForSubscriber,
|
||||||
findExpiredSyncedCertificates,
|
findExpiredSyncedCertificates,
|
||||||
|
findActiveCertificatesByIds,
|
||||||
|
findActiveCertificatesForSync,
|
||||||
findCertificatesEligibleForRenewal,
|
findCertificatesEligibleForRenewal,
|
||||||
findWithPrivateKeyInfo
|
findWithPrivateKeyInfo
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ import { TCertificateAuthorityDALFactory } from "@app/services/certificate-autho
|
|||||||
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||||
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
import { triggerAutoSyncForCertificate } from "@app/services/pki-sync/pki-sync-utils";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ type TCertificateServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
|
||||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
};
|
};
|
||||||
@@ -76,6 +78,7 @@ export const certificateServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
}: TCertificateServiceFactoryDep) => {
|
}: TCertificateServiceFactoryDep) => {
|
||||||
@@ -166,10 +169,12 @@ export const certificateServiceFactory = ({
|
|||||||
|
|
||||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||||
|
|
||||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
// Trigger auto sync for PKI syncs connected to this certificate
|
||||||
if (cert.pkiSubscriberId) {
|
await triggerAutoSyncForCertificate(cert.id, {
|
||||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
certificateSyncDAL,
|
||||||
}
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deletedCert
|
deletedCert
|
||||||
@@ -235,10 +240,12 @@ export const certificateServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
// Trigger auto sync for PKI syncs connected to this certificate
|
||||||
if (cert.pkiSubscriberId) {
|
await triggerAutoSyncForCertificate(cert.id, {
|
||||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
certificateSyncDAL,
|
||||||
}
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
// Note: External CA revocation handling would go here for supported CA types
|
// Note: External CA revocation handling would go here for supported CA types
|
||||||
// Currently, only internal CAs and ACME CAs support revocation
|
// Currently, only internal CAs and ACME CAs support revocation
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
|
||||||
throw new BadRequestError({
|
throw new NotFoundError({
|
||||||
message: "The identity does not have Token Auth attached"
|
message: "Token Auth configuration not found for identity"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ export enum IntegrationUrls {
|
|||||||
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
GCP_SERVICE_USAGE_URL = "https://serviceusage.googleapis.com",
|
||||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
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 () => {
|
export const getIntegrationOptions = async () => {
|
||||||
|
|||||||
+297
-85
@@ -3,7 +3,9 @@ import * as AWS from "aws-sdk";
|
|||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TCertificateSyncs } from "@app/db/schemas";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||||
@@ -14,6 +16,9 @@ import {
|
|||||||
AwsConnectionAssumeRoleCredentialsSchema
|
AwsConnectionAssumeRoleCredentialsSchema
|
||||||
} from "@app/services/app-connection/aws/aws-connection-schemas";
|
} from "@app/services/app-connection/aws/aws-connection-schemas";
|
||||||
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
import { TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
|
import { TCertificateMap } from "@app/services/pki-sync/pki-sync-types";
|
||||||
@@ -88,39 +93,6 @@ const shouldSkipCertificateExport = (certificate: AWS.ACM.CertificateSummary): b
|
|||||||
return isAwsIssuedCertificate(certificate);
|
return isAwsIssuedCertificate(certificate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const findTagByKey = (tags: AWS.ACM.TagList | undefined, key: string): AWS.ACM.Tag | undefined => {
|
|
||||||
if (!tags || !Array.isArray(tags)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return tags.find((tag: AWS.ACM.Tag) => tag.Key === key && tag.Value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const findInfisicalCertificateTag = (tags: AWS.ACM.TagList | undefined): AWS.ACM.Tag | undefined => {
|
|
||||||
return findTagByKey(tags, INFISICAL_CERTIFICATE_TAG);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateCertificateIdentification = (
|
|
||||||
certName: string,
|
|
||||||
existingCert: { arn?: string; Tags?: AWS.ACM.TagList; cert?: string; privateKey?: string; certificateChain?: string }
|
|
||||||
): boolean => {
|
|
||||||
if (!existingCert?.arn || !existingCert?.Tags) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const certNameTag = findInfisicalCertificateTag(existingCert.Tags);
|
|
||||||
|
|
||||||
if (!certNameTag || !certNameTag.Value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return certNameTag.Value === certName;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateCertificateNameSchema = (schema: string): void => {
|
const validateCertificateNameSchema = (schema: string): void => {
|
||||||
if (!schema.includes("{{certificateId}}")) {
|
if (!schema.includes("{{certificateId}}")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -174,6 +146,21 @@ const generateCertificateName = (certificateName: string, pkiSync: TPkiSyncWithC
|
|||||||
return sanitizedCertificateName;
|
return sanitizedCertificateName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TAwsCertificateManagerPkiSyncFactoryDeps = {
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
| "removeCertificates"
|
||||||
|
| "addCertificates"
|
||||||
|
| "findByPkiSyncAndCertificate"
|
||||||
|
| "updateSyncStatus"
|
||||||
|
| "updateById"
|
||||||
|
| "findByPkiSyncId"
|
||||||
|
>;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|
||||||
const getAwsAcmClient = async (
|
const getAwsAcmClient = async (
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
region: AWSRegion,
|
region: AWSRegion,
|
||||||
@@ -230,7 +217,9 @@ const getAwsAcmClient = async (
|
|||||||
|
|
||||||
export const awsCertificateManagerPkiSyncFactory = ({
|
export const awsCertificateManagerPkiSyncFactory = ({
|
||||||
kmsService,
|
kmsService,
|
||||||
appConnectionDAL
|
appConnectionDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
|
certificateDAL
|
||||||
}: TAwsCertificateManagerPkiSyncFactoryDeps) => {
|
}: TAwsCertificateManagerPkiSyncFactoryDeps) => {
|
||||||
const deleteCertificateFromAcm = async (
|
const deleteCertificateFromAcm = async (
|
||||||
acm: AWS.ACM,
|
acm: AWS.ACM,
|
||||||
@@ -392,79 +381,201 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
);
|
);
|
||||||
|
|
||||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
const {
|
||||||
|
acmCertificates
|
||||||
|
}: {
|
||||||
|
acmCertificates: Record<
|
||||||
|
string,
|
||||||
|
{ cert: string; privateKey: string; certificateChain?: string; arn?: string; Tags?: AWS.ACM.TagList }
|
||||||
|
>;
|
||||||
|
} = await $getAwsAcmCertificates(acm, pkiSync.id);
|
||||||
|
|
||||||
|
const acmCertificatesByArn = new Map<string, (typeof acmCertificates)[string]>();
|
||||||
|
Object.values(acmCertificates).forEach((acmCert) => {
|
||||||
|
if (acmCert.arn) {
|
||||||
|
acmCertificatesByArn.set(acmCert.arn, acmCert);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
|
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||||
|
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||||
|
|
||||||
|
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||||
|
if (record.certificateId) {
|
||||||
|
syncRecordsByCertId.set(record.certificateId, record);
|
||||||
|
}
|
||||||
|
if (record.externalIdentifier) {
|
||||||
|
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const setCertificates: CertificateImportRequest[] = [];
|
const setCertificates: CertificateImportRequest[] = [];
|
||||||
|
const validationErrors: Array<{ name: string; error: string }> = [];
|
||||||
|
|
||||||
const activeCertificateNames = Object.keys(certificateMap);
|
const syncOptions = pkiSync.syncOptions as { preserveArn?: boolean; canRemoveCertificates?: boolean } | undefined;
|
||||||
|
const preserveArn = syncOptions?.preserveArn ?? true;
|
||||||
|
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||||
|
|
||||||
Object.entries(certificateMap).forEach(([certName, certData]) => {
|
const activeExternalIdentifiers = new Set<string>();
|
||||||
const { cert, privateKey, certificateChain } = certData;
|
|
||||||
const certificateName = generateCertificateName(certName, pkiSync);
|
|
||||||
|
|
||||||
const existingCert = Object.values(acmCertificates).find((acmCert) =>
|
for (const [certName, certData] of Object.entries(certificateMap)) {
|
||||||
validateCertificateIdentification(certName, acmCert)
|
const { cert, privateKey, certificateChain, certificateId } = certData;
|
||||||
);
|
|
||||||
|
|
||||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
validateCertificateContent(cert, privateKey);
|
validateCertificateContent(cert, privateKey);
|
||||||
} catch (validationError) {
|
} catch (validationError) {
|
||||||
throw new PkiSyncError({
|
const errorMessage = validationError instanceof Error ? validationError.message : String(validationError);
|
||||||
message: `Certificate validation failed for ${certName}: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
validationErrors.push({
|
||||||
shouldRetry: false,
|
name: certName,
|
||||||
context: {
|
error: `Certificate validation failed: ${errorMessage}`
|
||||||
certificateName,
|
|
||||||
certName
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdateCert) {
|
if (preserveArn && certificateId && typeof certificateId === "string") {
|
||||||
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (certificate?.renewedByCertificateId) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateName = generateCertificateName(certName, pkiSync);
|
||||||
|
|
||||||
|
let targetArn: string | undefined;
|
||||||
|
let shouldCreateNew = false;
|
||||||
|
|
||||||
|
if (!certificateId || typeof certificateId !== "string") {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
} else {
|
||||||
|
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||||
|
const isRenewal = !!currentCertificate?.renewedFromCertificateId;
|
||||||
|
|
||||||
|
if (isRenewal) {
|
||||||
|
const currentSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||||
|
const oldCertificateId = currentCertificate.renewedFromCertificateId;
|
||||||
|
const oldSyncRecord = oldCertificateId ? syncRecordsByCertId.get(oldCertificateId) : undefined;
|
||||||
|
|
||||||
|
if (currentSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAcmCert = acmCertificatesByArn.get(currentSyncRecord.externalIdentifier);
|
||||||
|
|
||||||
|
if (existingAcmCert) {
|
||||||
|
if (!preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
} else if (preserveArn && oldSyncRecord?.externalIdentifier === currentSyncRecord.externalIdentifier) {
|
||||||
|
targetArn = currentSyncRecord.externalIdentifier;
|
||||||
|
shouldCreateNew = true;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
|
||||||
|
if (oldCertificateId && oldSyncRecord) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetArn = currentSyncRecord.externalIdentifier;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
shouldCreateNew = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else if (preserveArn && oldSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAcmCert = acmCertificatesByArn.get(oldSyncRecord.externalIdentifier);
|
||||||
|
|
||||||
|
if (existingAcmCert) {
|
||||||
|
targetArn = oldSyncRecord.externalIdentifier;
|
||||||
|
shouldCreateNew = true;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
if (oldCertificateId) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, [oldCertificateId]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||||
|
if (existingSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAcmCert = acmCertificatesByArn.get(existingSyncRecord.externalIdentifier);
|
||||||
|
if (existingAcmCert) {
|
||||||
|
targetArn = existingSyncRecord.externalIdentifier;
|
||||||
|
activeExternalIdentifiers.add(targetArn);
|
||||||
|
shouldCreateNew = false;
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCreateNew) {
|
||||||
setCertificates.push({
|
setCertificates.push({
|
||||||
key: certName,
|
key: certName,
|
||||||
name: certificateName,
|
name: certificateName,
|
||||||
cert,
|
cert,
|
||||||
privateKey,
|
privateKey,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
existingArn: existingCert?.arn
|
existingArn: targetArn,
|
||||||
|
certificateId: certificateId as string
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Identify expired/removed certificates that need to be cleaned up from ACM
|
if (targetArn) {
|
||||||
const certificatesToRemove = Object.values(acmCertificates)
|
activeExternalIdentifiers.add(targetArn);
|
||||||
.filter((acmCert) => {
|
}
|
||||||
if (!acmCert.arn || !acmCert.Tags) {
|
}
|
||||||
return false;
|
|
||||||
|
const certificatesToRemove: string[] = [];
|
||||||
|
|
||||||
|
if (canRemoveCertificates) {
|
||||||
|
existingSyncRecords.forEach((syncRecord) => {
|
||||||
|
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||||
|
const acmCert = acmCertificatesByArn.get(syncRecord.externalIdentifier);
|
||||||
|
if (acmCert?.arn) {
|
||||||
|
certificatesToRemove.push(acmCert.arn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const certNameTag = findInfisicalCertificateTag(acmCert.Tags);
|
Object.values(acmCertificates).forEach((acmCert) => {
|
||||||
if (!certNameTag || !certNameTag.Value) {
|
if (acmCert.arn && acmCert.Tags) {
|
||||||
return false;
|
const hasInfisicalTag = acmCert.Tags.some((tag) => tag.Key === INFISICAL_CERTIFICATE_TAG && tag.Value);
|
||||||
|
|
||||||
|
if (hasInfisicalTag) {
|
||||||
|
const isTrackedInSyncRecords = existingSyncRecords.some(
|
||||||
|
(record) => record.externalIdentifier === acmCert.arn
|
||||||
|
);
|
||||||
|
const isInActiveSet = activeExternalIdentifiers.has(acmCert.arn);
|
||||||
|
if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(acmCert.arn)) {
|
||||||
|
certificatesToRemove.push(acmCert.arn);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const isActive = activeCertificateNames.includes(certNameTag.Value);
|
}
|
||||||
return !isActive;
|
|
||||||
})
|
|
||||||
.map((acmCert) => acmCert.arn!)
|
|
||||||
.filter((arn) => arn);
|
|
||||||
|
|
||||||
const uploadResults = await executeWithConcurrencyLimit(
|
const uploadResults = await executeWithConcurrencyLimit(
|
||||||
setCertificates,
|
setCertificates,
|
||||||
async ({ key, name, cert, privateKey, certificateChain, existingArn }) => {
|
async ({ key, name, cert, privateKey, certificateChain, existingArn, certificateId }) => {
|
||||||
try {
|
try {
|
||||||
const importParams: AWS.ACM.ImportCertificateRequest = {
|
const importParams: AWS.ACM.ImportCertificateRequest = {
|
||||||
Certificate: cert,
|
Certificate: cert,
|
||||||
PrivateKey: privateKey,
|
PrivateKey: privateKey
|
||||||
Tags: [
|
};
|
||||||
|
|
||||||
|
if (!existingArn) {
|
||||||
|
importParams.Tags = [
|
||||||
{
|
{
|
||||||
Key: INFISICAL_CERTIFICATE_TAG,
|
Key: INFISICAL_CERTIFICATE_TAG,
|
||||||
Value: key
|
Value: key
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
};
|
}
|
||||||
|
|
||||||
if (certificateChain && certificateChain.trim().length > 0) {
|
if (certificateChain && certificateChain.trim().length > 0) {
|
||||||
importParams.CertificateChain = certificateChain;
|
importParams.CertificateChain = certificateChain;
|
||||||
@@ -478,6 +589,57 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
syncId: pkiSync.id
|
syncId: pkiSync.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (existingArn && response.CertificateArn) {
|
||||||
|
try {
|
||||||
|
// Small delay to ensure AWS ACM has processed the certificate import
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => resolve(), 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
await withRateLimitRetry(
|
||||||
|
() =>
|
||||||
|
acm
|
||||||
|
.addTagsToCertificate({
|
||||||
|
CertificateArn: response.CertificateArn!,
|
||||||
|
Tags: [
|
||||||
|
{
|
||||||
|
Key: INFISICAL_CERTIFICATE_TAG,
|
||||||
|
Value: key
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.promise(),
|
||||||
|
{
|
||||||
|
operation: "add-tags-to-certificate",
|
||||||
|
syncId: pkiSync.id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (tagError) {
|
||||||
|
const errorMessage = tagError instanceof Error ? tagError.message : "Unknown tagging error";
|
||||||
|
logger.warn(
|
||||||
|
`Failed to add tags to certificate ${key} (ARN: ${response.CertificateArn}): ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.CertificateArn && certificateId) {
|
||||||
|
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||||
|
if (existingCertSync) {
|
||||||
|
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||||
|
externalIdentifier: response.CertificateArn,
|
||||||
|
syncStatus: CertificateSyncStatus.Succeeded,
|
||||||
|
lastSyncedAt: new Date()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||||
|
{
|
||||||
|
certificateId,
|
||||||
|
externalIdentifier: response.CertificateArn
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { key, name, success: true, response };
|
return { key, name, success: true, response };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
@@ -520,15 +682,21 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
const details: {
|
const details: {
|
||||||
failedUploads?: Array<{ name: string; error: string }>;
|
failedUploads?: Array<{ name: string; error: string }>;
|
||||||
failedRemovals?: Array<{ name: string; error: string }>;
|
failedRemovals?: Array<{ name: string; error: string }>;
|
||||||
|
validationErrors?: Array<{ name: string; error: string }>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
details.validationErrors = validationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
if (failedUploads.length > 0) {
|
if (failedUploads.length > 0) {
|
||||||
details.failedUploads = failedUploads.map((failure, index) => {
|
details.failedUploads = failedUploads.map((failure, index) => {
|
||||||
const certificateName = setCertificates[index]?.name || "unknown";
|
const certificateRequest = setCertificates[index];
|
||||||
|
const certificateName = certificateRequest?.name || certificateRequest?.key || "unknown";
|
||||||
let errorMessage = "Unknown error";
|
let errorMessage = "Unknown error";
|
||||||
|
|
||||||
if (failure.status === "rejected") {
|
if (failure.status === "rejected") {
|
||||||
errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
errorMessage = failure.reason instanceof Error ? failure.reason.message : String(failure.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -567,7 +735,8 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
|
|
||||||
const removeCertificates = async (
|
const removeCertificates = async (
|
||||||
pkiSync: TPkiSyncWithCredentials,
|
pkiSync: TPkiSyncWithCredentials,
|
||||||
certificateNames: string[]
|
certificateNames: string[],
|
||||||
|
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||||
): Promise<RemoveCertificatesResult> => {
|
): Promise<RemoveCertificatesResult> => {
|
||||||
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
const destinationConfig = pkiSync.destinationConfig as TAwsCertificateManagerPkiSyncConfig;
|
||||||
const acm = await getAwsAcmClient(
|
const acm = await getAwsAcmClient(
|
||||||
@@ -577,22 +746,33 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
);
|
);
|
||||||
|
|
||||||
const { acmCertificates } = await $getAwsAcmCertificates(acm, pkiSync.id);
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
|
|
||||||
const certificateArnsToRemove: string[] = [];
|
const certificateArnsToRemove: string[] = [];
|
||||||
|
const certificateIdToArnMap = new Map<string, string>();
|
||||||
for (const certName of certificateNames) {
|
for (const certName of certificateNames) {
|
||||||
const matchingCerts = Object.values(acmCertificates).filter((acmCert) =>
|
const certificateData = deps?.certificateMap?.[certName];
|
||||||
validateCertificateIdentification(certName, acmCert)
|
if (certificateData?.certificateId) {
|
||||||
);
|
const { certificateId } = certificateData;
|
||||||
|
|
||||||
for (const acmCert of matchingCerts) {
|
if (typeof certificateId === "string") {
|
||||||
if (acmCert.arn) {
|
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId);
|
||||||
certificateArnsToRemove.push(acmCert.arn);
|
|
||||||
|
if (syncRecord?.externalIdentifier) {
|
||||||
|
certificateArnsToRemove.push(syncRecord.externalIdentifier);
|
||||||
|
certificateIdToArnMap.set(certificateId, syncRecord.externalIdentifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certificateArnsToRemove.length === 0) {
|
||||||
|
return {
|
||||||
|
removed: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: certificateNames.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const results = await executeWithConcurrencyLimit(
|
const results = await executeWithConcurrencyLimit(
|
||||||
certificateArnsToRemove,
|
certificateArnsToRemove,
|
||||||
async (certificateArn) =>
|
async (certificateArn) =>
|
||||||
@@ -602,6 +782,38 @@ export const awsCertificateManagerPkiSyncFactory = ({
|
|||||||
|
|
||||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||||
|
|
||||||
|
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||||
|
for (const failure of failedRemovals) {
|
||||||
|
if (failure.status === "rejected") {
|
||||||
|
const failedArn = certificateArnsToRemove[results.indexOf(failure)];
|
||||||
|
const certificateId = Array.from(certificateIdToArnMap.entries()).find(([, arn]) => arn === failedArn)?.[0];
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const errorMessage = failure.reason instanceof Error ? failure.reason.message : "Unknown error";
|
||||||
|
await deps.certificateSyncDAL.updateSyncStatus(
|
||||||
|
pkiSync.id,
|
||||||
|
certificateId,
|
||||||
|
CertificateSyncStatus.Failed,
|
||||||
|
`Failed to remove from AWS: ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||||
|
if (successfulRemovals.length > 0) {
|
||||||
|
const successfulArns = new Set(successfulRemovals.map((_, index) => certificateArnsToRemove[index]));
|
||||||
|
|
||||||
|
const certificateIdsToRemove = Array.from(certificateIdToArnMap.entries())
|
||||||
|
.filter(([, arn]) => successfulArns.has(arn))
|
||||||
|
.map(([certificateId]) => certificateId);
|
||||||
|
|
||||||
|
if (certificateIdsToRemove.length > 0) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (failedRemovals.length > 0) {
|
if (failedRemovals.length > 0) {
|
||||||
const failedReasons = failedRemovals.map((failure) => {
|
const failedReasons = failedRemovals.map((failure) => {
|
||||||
if (failure.status === "rejected") {
|
if (failure.status === "rejected") {
|
||||||
|
|||||||
+9
-4
@@ -14,6 +14,7 @@ export const AwsCertificateManagerPkiSyncConfigSchema = z.object({
|
|||||||
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
||||||
canImportCertificates: z.boolean().default(false),
|
canImportCertificates: z.boolean().default(false),
|
||||||
canRemoveCertificates: z.boolean().default(true),
|
canRemoveCertificates: z.boolean().default(true),
|
||||||
|
preserveArn: z.boolean().default(true),
|
||||||
certificateNameSchema: z
|
certificateNameSchema: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -28,6 +29,9 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
|||||||
|
|
||||||
const testName = schema
|
const testName = schema
|
||||||
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
|
.replace(new RE2("\\{\\{certificateId\\}\\}", "g"), "test-cert-id")
|
||||||
|
.replace(new RE2("\\{\\{profileId\\}\\}", "g"), "test-profile-id")
|
||||||
|
.replace(new RE2("\\{\\{commonName\\}\\}", "g"), "test-common-name")
|
||||||
|
.replace(new RE2("\\{\\{friendlyName\\}\\}", "g"), "test-friendly-name")
|
||||||
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
|
.replace(new RE2("\\{\\{environment\\}\\}", "g"), "test-env");
|
||||||
|
|
||||||
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
const hasForbiddenChars = AWS_CERTIFICATE_MANAGER_CERTIFICATE_NAMING.FORBIDDEN_CHARACTERS.split("").some(
|
||||||
@@ -43,7 +47,7 @@ const AwsCertificateManagerPkiSyncOptionsSchema = z.object({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager"
|
"Certificate name schema must include {{certificateId}} placeholder and result in names that contain only alphanumeric characters, spaces, hyphens, and underscores and be 1-256 characters long when compiled for AWS Certificate Manager. Available placeholders: {{certificateId}}, {{profileId}}, {{commonName}}, {{friendlyName}}, {{environment}}"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -60,9 +64,10 @@ export const CreateAwsCertificateManagerPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().default(true),
|
isAutoSyncEnabled: z.boolean().default(true),
|
||||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema,
|
||||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional().default({}),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string(),
|
connectionId: z.string(),
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateIds: z.array(z.string().uuid()).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
||||||
@@ -71,7 +76,7 @@ export const UpdateAwsCertificateManagerPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().optional(),
|
isAutoSyncEnabled: z.boolean().optional(),
|
||||||
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
destinationConfig: AwsCertificateManagerPkiSyncConfigSchema.optional(),
|
||||||
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
syncOptions: AwsCertificateManagerPkiSyncOptionsSchema.optional(),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string().optional()
|
connectionId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
@@ -39,6 +39,7 @@ export interface SyncCertificatesResult {
|
|||||||
details?: {
|
details?: {
|
||||||
failedUploads?: Array<{ name: string; error: string }>;
|
failedUploads?: Array<{ name: string; error: string }>;
|
||||||
failedRemovals?: Array<{ name: string; error: string }>;
|
failedRemovals?: Array<{ name: string; error: string }>;
|
||||||
|
validationErrors?: Array<{ name: string; error: string }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,4 +56,5 @@ export interface CertificateImportRequest {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
certificateChain?: string;
|
certificateChain?: string;
|
||||||
existingArn?: string;
|
existingArn?: string;
|
||||||
|
certificateId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
|
import { TCertificateSyncs } from "@app/db/schemas";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
import { createConnectionQueue, RateLimitConfig } from "@app/services/connection-queue";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
|
import { matchesCertificateNameSchema } from "@app/services/pki-sync/pki-sync-fns";
|
||||||
@@ -32,7 +36,9 @@ const extractCertificateNameFromId = (certificateId: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
|
const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyncWithCredentials): boolean => {
|
||||||
const syncOptions = pkiSync.syncOptions as { certificateNameSchema?: string } | undefined;
|
const syncOptions = pkiSync.syncOptions as
|
||||||
|
| { certificateNameSchema?: string; canRemoveCertificates?: boolean }
|
||||||
|
| undefined;
|
||||||
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
const certificateNameSchema = syncOptions?.certificateNameSchema;
|
||||||
|
|
||||||
if (certificateNameSchema) {
|
if (certificateNameSchema) {
|
||||||
@@ -46,6 +52,16 @@ const isInfisicalManagedCertificate = (certificateName: string, pkiSync: TPkiSyn
|
|||||||
type TAzureKeyVaultPkiSyncFactoryDeps = {
|
type TAzureKeyVaultPkiSyncFactoryDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
| "removeCertificates"
|
||||||
|
| "addCertificates"
|
||||||
|
| "findByPkiSyncAndCertificate"
|
||||||
|
| "updateById"
|
||||||
|
| "findByPkiSyncId"
|
||||||
|
| "updateSyncStatus"
|
||||||
|
>;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseCertificateX509Props = (certPem: string) => {
|
const parseCertificateX509Props = (certPem: string) => {
|
||||||
@@ -188,7 +204,12 @@ const parseCertificateKeyProps = (certPem: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
export const azureKeyVaultPkiSyncFactory = ({
|
||||||
|
kmsService,
|
||||||
|
appConnectionDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
|
certificateDAL
|
||||||
|
}: TAzureKeyVaultPkiSyncFactoryDeps) => {
|
||||||
const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
|
const $getAzureKeyVaultCertificates = async (accessToken: string, vaultBaseUrl: string, syncId = "unknown") => {
|
||||||
const paginateAzureKeyVaultCertificates = async () => {
|
const paginateAzureKeyVaultCertificates = async () => {
|
||||||
let result: GetAzureKeyVaultCertificate[] = [];
|
let result: GetAzureKeyVaultCertificate[] = [];
|
||||||
@@ -325,48 +346,126 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
pkiSync.id
|
pkiSync.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
|
const syncRecordsByCertId = new Map<string, TCertificateSyncs>();
|
||||||
|
const syncRecordsByExternalId = new Map<string, TCertificateSyncs>();
|
||||||
|
|
||||||
|
existingSyncRecords.forEach((record: TCertificateSyncs) => {
|
||||||
|
if (record.certificateId) {
|
||||||
|
syncRecordsByCertId.set(record.certificateId, record);
|
||||||
|
}
|
||||||
|
if (record.externalIdentifier) {
|
||||||
|
syncRecordsByExternalId.set(record.externalIdentifier, record);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const setCertificates: {
|
const setCertificates: {
|
||||||
key: string;
|
key: string;
|
||||||
cert: string;
|
cert: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
certificateChain?: string;
|
certificateChain?: string;
|
||||||
|
certificateId?: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
// Track which certificates should exist in Azure Key Vault
|
const syncOptions = pkiSync.syncOptions as
|
||||||
const activeCertificateNames = Object.keys(certificateMap);
|
| { certificateNameSchema?: string; canRemoveCertificates?: boolean; enableVersioning?: boolean }
|
||||||
|
| undefined;
|
||||||
|
const canRemoveCertificates = syncOptions?.canRemoveCertificates ?? true;
|
||||||
|
const enableVersioning = syncOptions?.enableVersioning ?? true;
|
||||||
|
|
||||||
|
const activeExternalIdentifiers = new Set<string>();
|
||||||
|
|
||||||
// Iterate through certificates to sync to Azure Key Vault
|
// Iterate through certificates to sync to Azure Key Vault
|
||||||
Object.entries(certificateMap).forEach(([certName, { cert, privateKey, certificateChain }]) => {
|
for (const [certName, { cert, privateKey, certificateChain, certificateId }] of Object.entries(certificateMap)) {
|
||||||
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
|
if (disabledAzureKeyVaultCertificateKeys.includes(certName)) {
|
||||||
return;
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCert = vaultCertificates[certName];
|
if (enableVersioning && typeof certificateId === "string") {
|
||||||
const shouldUpdateCert = !existingCert || existingCert.cert !== cert;
|
const certificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (certificate?.renewedByCertificateId) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldUpdateCert) {
|
let targetCertName = certName;
|
||||||
|
let shouldCreateNew = false;
|
||||||
|
|
||||||
|
if (typeof certificateId === "string") {
|
||||||
|
const existingSyncRecord = syncRecordsByCertId.get(certificateId);
|
||||||
|
|
||||||
|
if (existingSyncRecord?.externalIdentifier) {
|
||||||
|
const existingAzureCert = vaultCertificates[existingSyncRecord.externalIdentifier];
|
||||||
|
|
||||||
|
if (existingAzureCert && enableVersioning) {
|
||||||
|
targetCertName = existingSyncRecord.externalIdentifier;
|
||||||
|
activeExternalIdentifiers.add(targetCertName);
|
||||||
|
|
||||||
|
const shouldUpdateCert = existingAzureCert.cert !== cert;
|
||||||
|
if (shouldUpdateCert) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else if (!existingAzureCert) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
} else if (!enableVersioning) {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldCreateNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCreateNew || !vaultCertificates[targetCertName] || vaultCertificates[targetCertName].cert !== cert) {
|
||||||
setCertificates.push({
|
setCertificates.push({
|
||||||
key: certName,
|
key: targetCertName,
|
||||||
cert,
|
cert,
|
||||||
privateKey,
|
privateKey,
|
||||||
certificateChain
|
certificateChain,
|
||||||
|
certificateId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Identify expired/removed certificates that need to be cleaned up from Azure Key Vault
|
if (targetCertName) {
|
||||||
// Only remove certificates that were managed by Infisical (match naming schema)
|
activeExternalIdentifiers.add(targetCertName);
|
||||||
const certificatesToRemove = Object.keys(vaultCertificates).filter(
|
}
|
||||||
(vaultCertName) =>
|
}
|
||||||
isInfisicalManagedCertificate(vaultCertName, pkiSync) &&
|
|
||||||
!activeCertificateNames.includes(vaultCertName) &&
|
const certificatesToRemove: string[] = [];
|
||||||
!disabledAzureKeyVaultCertificateKeys.includes(vaultCertName)
|
|
||||||
);
|
if (canRemoveCertificates) {
|
||||||
|
existingSyncRecords.forEach((syncRecord) => {
|
||||||
|
if (syncRecord.externalIdentifier && !activeExternalIdentifiers.has(syncRecord.externalIdentifier)) {
|
||||||
|
if (vaultCertificates[syncRecord.externalIdentifier]) {
|
||||||
|
certificatesToRemove.push(syncRecord.externalIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(vaultCertificates).forEach((certificateName) => {
|
||||||
|
const isInfisicalManaged = isInfisicalManagedCertificate(certificateName, pkiSync);
|
||||||
|
|
||||||
|
if (isInfisicalManaged) {
|
||||||
|
const isTrackedInSyncRecords = existingSyncRecords.some(
|
||||||
|
(record) => record.externalIdentifier === certificateName
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInActiveSet = activeExternalIdentifiers.has(certificateName);
|
||||||
|
|
||||||
|
if (!isTrackedInSyncRecords && !isInActiveSet && !certificatesToRemove.includes(certificateName)) {
|
||||||
|
certificatesToRemove.push(certificateName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Upload certificates to Azure Key Vault with rate limiting
|
// Upload certificates to Azure Key Vault with rate limiting
|
||||||
const uploadResults = await executeWithConcurrencyLimit(
|
const uploadResults = await executeWithConcurrencyLimit(
|
||||||
setCertificates,
|
setCertificates,
|
||||||
async ({ key, cert, privateKey, certificateChain }) => {
|
async ({ key, cert, privateKey, certificateChain, certificateId }) => {
|
||||||
try {
|
try {
|
||||||
// Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
|
// Combine private key, certificate, and certificate chain in PEM format for Azure Key Vault
|
||||||
let combinedPem = "";
|
let combinedPem = "";
|
||||||
@@ -428,6 +527,31 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const existingCertSync = await certificateSyncDAL.findByPkiSyncAndCertificate(pkiSync.id, certificateId);
|
||||||
|
if (existingCertSync) {
|
||||||
|
await certificateSyncDAL.updateById(existingCertSync.id, {
|
||||||
|
externalIdentifier: key,
|
||||||
|
syncStatus: CertificateSyncStatus.Succeeded,
|
||||||
|
lastSyncedAt: new Date()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await certificateSyncDAL.addCertificates(pkiSync.id, [
|
||||||
|
{
|
||||||
|
certificateId,
|
||||||
|
externalIdentifier: key
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableVersioning) {
|
||||||
|
const currentCertificate = await certificateDAL.findById(certificateId);
|
||||||
|
if (currentCertificate?.renewedFromCertificateId) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, [currentCertificate.renewedFromCertificateId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { key, success: true, response: response.data as unknown };
|
return { key, success: true, response: response.data as unknown };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
@@ -599,19 +723,43 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCertificates = async (pkiSync: TPkiSyncWithCredentials, certificateNames: string[]) => {
|
const removeCertificates = async (
|
||||||
|
pkiSync: TPkiSyncWithCredentials,
|
||||||
|
certificateNames: string[],
|
||||||
|
deps?: { certificateSyncDAL?: TCertificateSyncDALFactory; certificateMap?: TCertificateMap }
|
||||||
|
) => {
|
||||||
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
|
const { accessToken } = await getAzureConnectionAccessToken(pkiSync.connection.id, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
// Cast destination config to Azure Key Vault config
|
// Cast destination config to Azure Key Vault config
|
||||||
const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
|
const destinationConfig = pkiSync.destinationConfig as TAzureKeyVaultPkiSyncConfig;
|
||||||
|
|
||||||
// Only remove certificates that are managed by Infisical (match naming schema)
|
const existingSyncRecords = await certificateSyncDAL.findByPkiSyncId(pkiSync.id);
|
||||||
const infisicalManagedCertNames = certificateNames.filter((certName) =>
|
const certificateNamesToRemove: string[] = [];
|
||||||
isInfisicalManagedCertificate(certName, pkiSync)
|
const certificateIdToNameMap = new Map<string, string>();
|
||||||
);
|
|
||||||
|
for (const certName of certificateNames) {
|
||||||
|
if (deps?.certificateMap?.[certName]?.certificateId) {
|
||||||
|
const { certificateId } = deps.certificateMap[certName];
|
||||||
|
|
||||||
|
const syncRecord = existingSyncRecords.find((record) => record.certificateId === certificateId);
|
||||||
|
|
||||||
|
if (syncRecord?.externalIdentifier && typeof certificateId === "string") {
|
||||||
|
certificateNamesToRemove.push(syncRecord.externalIdentifier);
|
||||||
|
certificateIdToNameMap.set(certificateId, syncRecord.externalIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certificateNamesToRemove.length === 0) {
|
||||||
|
return {
|
||||||
|
removed: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: certificateNames.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const results = await executeWithConcurrencyLimit(
|
const results = await executeWithConcurrencyLimit(
|
||||||
infisicalManagedCertNames,
|
certificateNamesToRemove,
|
||||||
async (certName) => {
|
async (certName) => {
|
||||||
try {
|
try {
|
||||||
const response = await request.delete(
|
const response = await request.delete(
|
||||||
@@ -646,8 +794,44 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
},
|
},
|
||||||
{ operation: "remove-specific-certificates", syncId: pkiSync.id }
|
{ operation: "remove-specific-certificates", syncId: pkiSync.id }
|
||||||
);
|
);
|
||||||
|
|
||||||
const failedRemovals = results.filter((result) => result.status === "rejected");
|
const failedRemovals = results.filter((result) => result.status === "rejected");
|
||||||
|
|
||||||
|
if (failedRemovals.length > 0 && deps?.certificateSyncDAL) {
|
||||||
|
for (const failure of failedRemovals) {
|
||||||
|
if (failure.status === "rejected") {
|
||||||
|
const failedCertName = certificateNamesToRemove[results.indexOf(failure)];
|
||||||
|
|
||||||
|
const certificateId = Array.from(certificateIdToNameMap.entries()).find(
|
||||||
|
([, name]) => name === failedCertName
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const errorMessage = (failure.reason as Error)?.message || "Unknown error";
|
||||||
|
await deps.certificateSyncDAL.updateSyncStatus(
|
||||||
|
pkiSync.id,
|
||||||
|
certificateId,
|
||||||
|
CertificateSyncStatus.Failed,
|
||||||
|
`Failed to remove from Azure: ${errorMessage}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulRemovals = results.filter((result) => result.status === "fulfilled");
|
||||||
|
if (successfulRemovals.length > 0) {
|
||||||
|
const successfulCertNames = new Set(successfulRemovals.map((_, index) => certificateNamesToRemove[index]));
|
||||||
|
|
||||||
|
const certificateIdsToRemove = Array.from(certificateIdToNameMap.entries())
|
||||||
|
.filter(([, name]) => successfulCertNames.has(name))
|
||||||
|
.map(([certificateId]) => certificateId);
|
||||||
|
|
||||||
|
if (certificateIdsToRemove.length > 0) {
|
||||||
|
await certificateSyncDAL.removeCertificates(pkiSync.id, certificateIdsToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (failedRemovals.length > 0) {
|
if (failedRemovals.length > 0) {
|
||||||
const failedReasons = failedRemovals.map((failure) => {
|
const failedReasons = failedRemovals.map((failure) => {
|
||||||
if (failure.status === "rejected") {
|
if (failure.status === "rejected") {
|
||||||
@@ -660,16 +844,16 @@ export const azureKeyVaultPkiSyncFactory = ({ kmsService, appConnectionDAL }: TA
|
|||||||
message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`,
|
message: `Failed to remove ${failedRemovals.length} certificate(s) from Azure Key Vault`,
|
||||||
context: {
|
context: {
|
||||||
failedReasons,
|
failedReasons,
|
||||||
totalCertificates: infisicalManagedCertNames.length,
|
totalCertificates: certificateNamesToRemove.length,
|
||||||
failedCount: failedRemovals.length
|
failedCount: failedRemovals.length
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
removed: infisicalManagedCertNames.length - failedRemovals.length,
|
removed: certificateNamesToRemove.length - failedRemovals.length,
|
||||||
failed: failedRemovals.length,
|
failed: failedRemovals.length,
|
||||||
skipped: certificateNames.length - infisicalManagedCertNames.length
|
skipped: certificateNames.length - certificateNamesToRemove.length
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const AzureKeyVaultPkiSyncConfigSchema = z.object({
|
|||||||
const AzureKeyVaultPkiSyncOptionsSchema = z.object({
|
const AzureKeyVaultPkiSyncOptionsSchema = z.object({
|
||||||
canImportCertificates: z.boolean().default(false),
|
canImportCertificates: z.boolean().default(false),
|
||||||
canRemoveCertificates: z.boolean().default(true),
|
canRemoveCertificates: z.boolean().default(true),
|
||||||
|
enableVersioning: z.boolean().default(true),
|
||||||
certificateNameSchema: z
|
certificateNameSchema: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -50,9 +51,10 @@ export const CreateAzureKeyVaultPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().default(true),
|
isAutoSyncEnabled: z.boolean().default(true),
|
||||||
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
|
destinationConfig: AzureKeyVaultPkiSyncConfigSchema,
|
||||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
|
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional().default({}),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string(),
|
connectionId: z.string(),
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateIds: z.array(z.string().uuid()).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
||||||
@@ -61,7 +63,7 @@ export const UpdateAzureKeyVaultPkiSyncSchema = z.object({
|
|||||||
isAutoSyncEnabled: z.boolean().optional(),
|
isAutoSyncEnabled: z.boolean().optional(),
|
||||||
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
|
destinationConfig: AzureKeyVaultPkiSyncConfigSchema.optional(),
|
||||||
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
|
syncOptions: AzureKeyVaultPkiSyncOptionsSchema.optional(),
|
||||||
subscriberId: z.string().optional(),
|
subscriberId: z.string().nullish(),
|
||||||
connectionId: z.string().optional()
|
connectionId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { z, ZodSchema } from "zod";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
|
import { AWS_CERTIFICATE_MANAGER_PKI_SYNC_LIST_OPTION } from "./aws-certificate-manager/aws-certificate-manager-pki-sync-constants";
|
||||||
@@ -184,6 +186,8 @@ export const PkiSyncFns = {
|
|||||||
dependencies: {
|
dependencies: {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||||
}
|
}
|
||||||
): Promise<{
|
): Promise<{
|
||||||
uploaded: number;
|
uploaded: number;
|
||||||
@@ -194,17 +198,28 @@ export const PkiSyncFns = {
|
|||||||
failedUploads?: Array<{ name: string; error: string }>;
|
failedUploads?: Array<{ name: string; error: string }>;
|
||||||
failedRemovals?: Array<{ name: string; error: string }>;
|
failedRemovals?: Array<{ name: string; error: string }>;
|
||||||
skippedCertificates?: Array<{ name: string; reason: string }>;
|
skippedCertificates?: Array<{ name: string; reason: string }>;
|
||||||
|
validationErrors?: Array<{ name: string; error: string }>;
|
||||||
};
|
};
|
||||||
}> => {
|
}> => {
|
||||||
switch (pkiSync.destination) {
|
switch (pkiSync.destination) {
|
||||||
case PkiSync.AzureKeyVault: {
|
case PkiSync.AzureKeyVault: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
|
||||||
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
|
return azureKeyVaultPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||||
}
|
}
|
||||||
case PkiSync.AwsCertificateManager: {
|
case PkiSync.AwsCertificateManager: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
|
||||||
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
return awsCertificateManagerPkiSync.syncCertificates(pkiSync, certificateMap);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -218,19 +233,38 @@ export const PkiSyncFns = {
|
|||||||
dependencies: {
|
dependencies: {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||||
|
certificateDAL: TCertificateDALFactory;
|
||||||
|
certificateMap: TCertificateMap;
|
||||||
}
|
}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
switch (pkiSync.destination) {
|
switch (pkiSync.destination) {
|
||||||
case PkiSync.AzureKeyVault: {
|
case PkiSync.AzureKeyVault: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
checkPkiSyncDestination(pkiSync, PkiSync.AzureKeyVault);
|
||||||
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory(dependencies);
|
const azureKeyVaultPkiSync = azureKeyVaultPkiSyncFactory({
|
||||||
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames);
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
|
await azureKeyVaultPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||||
|
certificateMap: dependencies.certificateMap
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PkiSync.AwsCertificateManager: {
|
case PkiSync.AwsCertificateManager: {
|
||||||
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
checkPkiSyncDestination(pkiSync, PkiSync.AwsCertificateManager);
|
||||||
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory(dependencies);
|
const awsCertificateManagerPkiSync = awsCertificateManagerPkiSyncFactory({
|
||||||
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames);
|
appConnectionDAL: dependencies.appConnectionDAL,
|
||||||
|
kmsService: dependencies.kmsService,
|
||||||
|
certificateDAL: dependencies.certificateDAL,
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL
|
||||||
|
});
|
||||||
|
await awsCertificateManagerPkiSync.removeCertificates(pkiSync, certificateNames, {
|
||||||
|
certificateSyncDAL: dependencies.certificateSyncDAL,
|
||||||
|
certificateMap: dependencies.certificateMap
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AxiosError } from "axios";
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
|
import { TCertificates } from "@app/db/schemas";
|
||||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -25,6 +26,8 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
|
|||||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||||
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
|
import { getCaCertChain } from "../certificate-authority/certificate-authority-fns";
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
import { PkiSyncStatus } from "./pki-sync-enums";
|
import { PkiSyncStatus } from "./pki-sync-enums";
|
||||||
import { PkiSyncError } from "./pki-sync-errors";
|
import { PkiSyncError } from "./pki-sync-errors";
|
||||||
@@ -55,14 +58,12 @@ type TPkiSyncQueueFactoryDep = {
|
|||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
certificateDAL: Pick<
|
certificateDAL: TCertificateDALFactory;
|
||||||
TCertificateDALFactory,
|
|
||||||
"findLatestActiveCertForSubscriber" | "findAllActiveCertsForSubscriber" | "create"
|
|
||||||
>;
|
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
|
certificateSyncDAL: TCertificateSyncDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PkiSyncActionJob = Job<
|
type PkiSyncActionJob = Job<
|
||||||
@@ -93,7 +94,8 @@ export const pkiSyncQueueFactory = ({
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL
|
certificateAuthorityCertDAL,
|
||||||
|
certificateSyncDAL
|
||||||
}: TPkiSyncQueueFactoryDep) => {
|
}: TPkiSyncQueueFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -153,25 +155,39 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
const $getInfisicalCertificates = async (
|
const $getInfisicalCertificates = async (
|
||||||
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
|
pkiSync: TPkiSyncRaw | TPkiSyncWithCredentials
|
||||||
): Promise<TCertificateMap> => {
|
): Promise<{ certificateMap: TCertificateMap; certificateMetadata: Map<string, { id: string; name: string }> }> => {
|
||||||
const { projectId, subscriberId } = pkiSync;
|
const { projectId, subscriberId, id: pkiSyncId } = pkiSync;
|
||||||
|
|
||||||
if (!subscriberId) {
|
|
||||||
throw new PkiSyncError({
|
|
||||||
message: "Invalid PKI Sync source configuration: subscriber no longer exists. Please update source subscriber.",
|
|
||||||
shouldRetry: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const certificateMap: TCertificateMap = {};
|
const certificateMap: TCertificateMap = {};
|
||||||
|
const certificateMetadata = new Map<string, { id: string; name: string }>();
|
||||||
|
let certificates: Array<{ id: string; projectId: string; caCertId?: string | null }> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all active certificates for the subscriber (not just the latest)
|
if (subscriberId) {
|
||||||
const certificates = await certificateDAL.findAllActiveCertsForSubscriber({
|
const subscriberCertificates = await certificateDAL.findAllActiveCertsForSubscriber({
|
||||||
subscriberId
|
subscriberId
|
||||||
});
|
});
|
||||||
|
certificates.push(...subscriberCertificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateIds = await certificateSyncDAL.findCertificateIdsByPkiSyncId(pkiSyncId);
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
const directCertificates = await certificateDAL.findActiveCertificatesByIds(certificateIds);
|
||||||
|
certificates.push(...directCertificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueCertificates = certificates.filter(
|
||||||
|
(cert, index, self) => self.findIndex((c) => c.id === cert.id) === index
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uniqueCertificates.length === 0) {
|
||||||
|
return { certificateMap, certificateMetadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
certificates = uniqueCertificates;
|
||||||
|
|
||||||
for (const certificate of certificates) {
|
for (const certificate of certificates) {
|
||||||
|
const cert = certificate as TCertificates;
|
||||||
try {
|
try {
|
||||||
// Get the certificate body and decrypt the certificate data
|
// Get the certificate body and decrypt the certificate data
|
||||||
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
|
const certBody = await certificateBodyDAL.findOne({ certId: certificate.id });
|
||||||
@@ -246,19 +262,45 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
if (certificateNameSchema) {
|
if (certificateNameSchema) {
|
||||||
const environment = "global";
|
const environment = "global";
|
||||||
certificateName = handlebars.compile(certificateNameSchema)({
|
const templateData = {
|
||||||
certificateId: certificate.id.replace(/-/g, ""),
|
certificateId: certificate.id.replace(/-/g, ""),
|
||||||
|
profileId: cert.profileId?.replace(/-/g, "") || certificate.id.replace(/-/g, ""),
|
||||||
|
commonName: cert.commonName || "",
|
||||||
|
friendlyName: cert.friendlyName || "",
|
||||||
environment
|
environment
|
||||||
});
|
};
|
||||||
|
certificateName = handlebars.compile(certificateNameSchema)(templateData);
|
||||||
} else {
|
} else {
|
||||||
certificateName = `Infisical-${certificate.id.replace(/-/g, "")}`;
|
const stableId = cert.profileId
|
||||||
|
? `${cert.profileId.replace(/-/g, "")}-${(cert.commonName || "").replace(/[^a-zA-Z0-9]/g, "")}`
|
||||||
|
: certificate.id.replace(/-/g, "");
|
||||||
|
certificateName = `Infisical-${stableId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alternativeNames: string[] = [];
|
||||||
|
|
||||||
|
const legacyName = `Infisical-${certificate.id.replace(/-/g, "")}`;
|
||||||
|
if (legacyName !== certificateName) {
|
||||||
|
alternativeNames.push(legacyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert.renewedFromCertificateId) {
|
||||||
|
const originalLegacyName = `Infisical-${cert.renewedFromCertificateId.replace(/-/g, "")}`;
|
||||||
|
alternativeNames.push(originalLegacyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
certificateMap[certificateName] = {
|
certificateMap[certificateName] = {
|
||||||
cert: certificatePem,
|
cert: certificatePem,
|
||||||
privateKey: certPrivateKey || "",
|
privateKey: certPrivateKey || "",
|
||||||
certificateChain
|
certificateChain,
|
||||||
|
alternativeNames,
|
||||||
|
certificateId: certificate.id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
certificateMetadata.set(certificateName, {
|
||||||
|
id: certificate.id,
|
||||||
|
name: certificateName
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
|
logger.warn({ certificateId: certificate.id, subscriberId }, "Certificate body not found for certificate");
|
||||||
}
|
}
|
||||||
@@ -281,7 +323,7 @@ export const pkiSyncQueueFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return certificateMap;
|
return { certificateMap, certificateMetadata };
|
||||||
};
|
};
|
||||||
|
|
||||||
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
|
const queuePkiSyncSyncCertificatesById = async (payload: TQueuePkiSyncSyncCertificatesByIdDTO) =>
|
||||||
@@ -348,12 +390,17 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
|
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
|
||||||
} = pkiSync;
|
} = pkiSync;
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new Error(`App connection not found: ${connectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await decryptAppConnectionCredentials({
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
orgId,
|
orgId,
|
||||||
encryptedCredentials,
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectId: appConnectionProjectId
|
projectId: appConnectionProjectId
|
||||||
});
|
});
|
||||||
@@ -366,11 +413,24 @@ export const pkiSyncQueueFactory = ({
|
|||||||
}
|
}
|
||||||
} as TPkiSyncWithCredentials;
|
} as TPkiSyncWithCredentials;
|
||||||
|
|
||||||
const certificateMap = await $getInfisicalCertificates(pkiSync);
|
const { certificateMap, certificateMetadata } = await $getInfisicalCertificates(pkiSync);
|
||||||
|
|
||||||
|
const statusUpdates = Array.from(certificateMetadata.entries()).map(([, metadata]) => ({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Running,
|
||||||
|
message: "Syncing certificate to destination"
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (statusUpdates.length > 0) {
|
||||||
|
await certificateSyncDAL.bulkUpdateSyncStatus(statusUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
|
const syncResult = await PkiSyncFns.syncCertificates(pkiSyncWithCredentials, certificateMap, {
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -384,6 +444,60 @@ export const pkiSyncQueueFactory = ({
|
|||||||
"PKI sync operation completed with certificate cleanup"
|
"PKI sync operation completed with certificate cleanup"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const postSyncUpdates: Array<{
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const [, metadata] of certificateMetadata.entries()) {
|
||||||
|
postSyncUpdates.push({
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Succeeded,
|
||||||
|
message: "Certificate successfully synced to destination"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncResult.details?.validationErrors) {
|
||||||
|
for (const validationError of syncResult.details.validationErrors) {
|
||||||
|
const metadata = certificateMetadata.get(validationError.name);
|
||||||
|
if (metadata) {
|
||||||
|
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
|
||||||
|
if (updateIndex >= 0) {
|
||||||
|
postSyncUpdates[updateIndex] = {
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Failed,
|
||||||
|
message: `${validationError.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncResult.details?.failedUploads) {
|
||||||
|
for (const failure of syncResult.details.failedUploads) {
|
||||||
|
const metadata = certificateMetadata.get(failure.name);
|
||||||
|
if (metadata) {
|
||||||
|
const updateIndex = postSyncUpdates.findIndex((u) => u.certificateId === metadata.id);
|
||||||
|
if (updateIndex >= 0) {
|
||||||
|
postSyncUpdates[updateIndex] = {
|
||||||
|
pkiSyncId: pkiSync.id,
|
||||||
|
certificateId: metadata.id,
|
||||||
|
status: CertificateSyncStatus.Failed,
|
||||||
|
message: `Failed to sync certificate: ${failure.error}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postSyncUpdates.length > 0) {
|
||||||
|
await certificateSyncDAL.bulkUpdateSyncStatus(postSyncUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
isSynced = true;
|
isSynced = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -550,17 +664,22 @@ export const pkiSyncQueueFactory = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
connection: { orgId, encryptedCredentials, projectId: appConnectionProjectId }
|
connection: { id: connectionId, orgId, projectId: appConnectionProjectId }
|
||||||
} = pkiSync;
|
} = pkiSync;
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new Error(`App connection not found: ${connectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await decryptAppConnectionCredentials({
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
orgId,
|
orgId,
|
||||||
encryptedCredentials,
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectId: appConnectionProjectId
|
projectId: appConnectionProjectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateMap = await $getInfisicalCertificates(pkiSync);
|
const { certificateMap } = await $getInfisicalCertificates(pkiSync);
|
||||||
|
|
||||||
await PkiSyncFns.removeCertificates(
|
await PkiSyncFns.removeCertificates(
|
||||||
{
|
{
|
||||||
@@ -573,7 +692,10 @@ export const pkiSyncQueueFactory = ({
|
|||||||
Object.keys(certificateMap),
|
Object.keys(certificateMap),
|
||||||
{
|
{
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateMap
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType, TCertificateSyncs } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionPkiSyncActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@@ -10,17 +10,24 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
|||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||||
|
|
||||||
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
|
import { CertificateSyncStatus } from "../certificate-sync/certificate-sync-enums";
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
|
import { PkiSync, PkiSyncStatus } from "./pki-sync-enums";
|
||||||
import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
|
import { enterprisePkiSyncCheck, getPkiSyncProviderCapabilities, listPkiSyncOptions } from "./pki-sync-fns";
|
||||||
import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
|
import { PKI_SYNC_CONNECTION_MAP, PKI_SYNC_NAME_MAP } from "./pki-sync-maps";
|
||||||
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
||||||
import {
|
import {
|
||||||
|
TAddCertificatesToPkiSyncDTO,
|
||||||
TCreatePkiSyncDTO,
|
TCreatePkiSyncDTO,
|
||||||
TDeletePkiSyncDTO,
|
TDeletePkiSyncDTO,
|
||||||
TFindPkiSyncByIdDTO,
|
TFindPkiSyncByIdDTO,
|
||||||
|
TListPkiSyncCertificatesDTO,
|
||||||
TListPkiSyncsByProjectId,
|
TListPkiSyncsByProjectId,
|
||||||
TPkiSync,
|
TPkiSync,
|
||||||
|
TPkiSyncCertificate,
|
||||||
|
TRemoveCertificatesFromPkiSyncDTO,
|
||||||
TTriggerPkiSyncImportCertificatesByIdDTO,
|
TTriggerPkiSyncImportCertificatesByIdDTO,
|
||||||
TTriggerPkiSyncRemoveCertificatesByIdDTO,
|
TTriggerPkiSyncRemoveCertificatesByIdDTO,
|
||||||
TTriggerPkiSyncSyncCertificatesByIdDTO,
|
TTriggerPkiSyncSyncCertificatesByIdDTO,
|
||||||
@@ -42,6 +49,17 @@ type TPkiSyncServiceFactoryDep = {
|
|||||||
TPkiSyncDALFactory,
|
TPkiSyncDALFactory,
|
||||||
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
"findById" | "findByProjectIdWithSubscribers" | "findByNameAndProjectId" | "create" | "updateById" | "deleteById"
|
||||||
>;
|
>;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "findActiveCertificatesByIds">;
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
| "findByPkiSyncId"
|
||||||
|
| "findByCertificateId"
|
||||||
|
| "findCertificateIdsByPkiSyncId"
|
||||||
|
| "addCertificates"
|
||||||
|
| "removeCertificates"
|
||||||
|
| "removeAllCertificatesFromSync"
|
||||||
|
| "findWithDetails"
|
||||||
|
>;
|
||||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
@@ -56,12 +74,41 @@ export type TPkiSyncServiceFactory = ReturnType<typeof pkiSyncServiceFactory>;
|
|||||||
|
|
||||||
export const pkiSyncServiceFactory = ({
|
export const pkiSyncServiceFactory = ({
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
}: TPkiSyncServiceFactoryDep) => {
|
}: TPkiSyncServiceFactoryDep) => {
|
||||||
|
const validateCertificatesProjectOwnership = async (certificateIds: string[], expectedProjectId: string) => {
|
||||||
|
if (certificateIds.length === 0) return;
|
||||||
|
|
||||||
|
const certificates = await certificateDAL.findActiveCertificatesByIds(certificateIds);
|
||||||
|
|
||||||
|
if (certificates.length !== certificateIds.length) {
|
||||||
|
const foundIds = certificates.map((cert) => cert.id);
|
||||||
|
const missingIds = certificateIds.filter((id) => !foundIds.includes(id));
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Certificates not found or not active: ${missingIds.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidProjectCertificates = certificates.filter((cert) => cert.projectId !== expectedProjectId);
|
||||||
|
if (invalidProjectCertificates.length > 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Certificates do not belong to the same project: ${invalidProjectCertificates.map((cert) => cert.id).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidRenewedCertificates = certificates.filter((cert) => cert.renewedByCertificateId);
|
||||||
|
if (invalidRenewedCertificates.length > 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Cannot add renewed certificates to PKI sync: ${invalidRenewedCertificates.map((cert) => cert.id).join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
const createPkiSync = async (
|
const createPkiSync = async (
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@@ -72,7 +119,8 @@ export const pkiSyncServiceFactory = ({
|
|||||||
syncOptions = {},
|
syncOptions = {},
|
||||||
subscriberId,
|
subscriberId,
|
||||||
connectionId,
|
connectionId,
|
||||||
projectId
|
projectId,
|
||||||
|
certificateIds = []
|
||||||
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
|
}: Omit<TCreatePkiSyncDTO, "auditLogInfo">,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
): Promise<TPkiSync> => {
|
): Promise<TPkiSync> => {
|
||||||
@@ -114,6 +162,10 @@ export const pkiSyncServiceFactory = ({
|
|||||||
...syncOptions
|
...syncOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await validateCertificatesProjectOwnership(certificateIds, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pkiSync = await pkiSyncDAL.create({
|
const pkiSync = await pkiSyncDAL.create({
|
||||||
name,
|
name,
|
||||||
@@ -128,6 +180,13 @@ export const pkiSyncServiceFactory = ({
|
|||||||
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
|
...(isAutoSyncEnabled && { syncStatus: PkiSyncStatus.Pending })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await certificateSyncDAL.addCertificates(
|
||||||
|
pkiSync.id,
|
||||||
|
certificateIds.map((id) => ({ certificateId: id }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (pkiSync.isAutoSyncEnabled) {
|
if (pkiSync.isAutoSyncEnabled) {
|
||||||
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
|
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id });
|
||||||
}
|
}
|
||||||
@@ -152,7 +211,8 @@ export const pkiSyncServiceFactory = ({
|
|||||||
destinationConfig,
|
destinationConfig,
|
||||||
syncOptions,
|
syncOptions,
|
||||||
subscriberId,
|
subscriberId,
|
||||||
connectionId
|
connectionId,
|
||||||
|
certificateIds
|
||||||
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
|
}: Omit<TUpdatePkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
): Promise<TPkiSync> => {
|
): Promise<TPkiSync> => {
|
||||||
@@ -221,6 +281,20 @@ export const pkiSyncServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (certificateIds !== undefined) {
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await certificateSyncDAL.removeAllCertificatesFromSync(id);
|
||||||
|
if (certificateIds.length > 0) {
|
||||||
|
await certificateSyncDAL.addCertificates(
|
||||||
|
id,
|
||||||
|
certificateIds.map((certId) => ({ certificateId: certId }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
|
const updatedPkiSync = await pkiSyncDAL.updateById(id, {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -266,7 +340,7 @@ export const pkiSyncServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listPkiSyncsByProjectId = async (
|
const listPkiSyncsByProjectId = async (
|
||||||
{ projectId }: TListPkiSyncsByProjectId,
|
{ projectId, certificateId }: TListPkiSyncsByProjectId,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
): Promise<TPkiSync[]> => {
|
): Promise<TPkiSync[]> => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
@@ -282,6 +356,29 @@ export const pkiSyncServiceFactory = ({
|
|||||||
|
|
||||||
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
|
const pkiSyncsWithSubscribers = await pkiSyncDAL.findByProjectIdWithSubscribers(projectId);
|
||||||
|
|
||||||
|
if (certificateId) {
|
||||||
|
const syncsWithCertificateInfo = await Promise.all(
|
||||||
|
pkiSyncsWithSubscribers.map(async (sync) => {
|
||||||
|
try {
|
||||||
|
const certificateSyncs = await certificateSyncDAL.findByPkiSyncId(sync.id);
|
||||||
|
const hasCertificate = certificateSyncs.some((certSync) => certSync.certificateId === certificateId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sync,
|
||||||
|
hasCertificate
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...sync,
|
||||||
|
hasCertificate: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return syncsWithCertificateInfo as TPkiSync[];
|
||||||
|
}
|
||||||
|
|
||||||
return pkiSyncsWithSubscribers as TPkiSync[];
|
return pkiSyncsWithSubscribers as TPkiSync[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -433,6 +530,145 @@ export const pkiSyncServiceFactory = ({
|
|||||||
return listPkiSyncOptions();
|
return listPkiSyncOptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addCertificatesToPkiSync = async (
|
||||||
|
{ pkiSyncId, certificateIds }: Omit<TAddCertificatesToPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
): Promise<{
|
||||||
|
addedCertificates: TCertificateSyncs[];
|
||||||
|
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||||
|
}> => {
|
||||||
|
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||||
|
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager,
|
||||||
|
projectId: pkiSync.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||||
|
|
||||||
|
await validateCertificatesProjectOwnership(certificateIds, pkiSync.projectId);
|
||||||
|
|
||||||
|
const addedCertificates = await certificateSyncDAL.addCertificates(
|
||||||
|
pkiSyncId,
|
||||||
|
certificateIds.map((id) => ({ certificateId: id }))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pkiSync.isAutoSyncEnabled) {
|
||||||
|
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addedCertificates,
|
||||||
|
pkiSyncInfo: {
|
||||||
|
projectId: pkiSync.projectId,
|
||||||
|
destination: pkiSync.destination,
|
||||||
|
name: pkiSync.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCertificatesFromPkiSync = async (
|
||||||
|
{ pkiSyncId, certificateIds }: Omit<TRemoveCertificatesFromPkiSyncDTO, "auditLogInfo" | "projectId">,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
): Promise<{ removedCount: number; pkiSyncInfo: { projectId: string; destination: string; name: string } }> => {
|
||||||
|
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||||
|
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager,
|
||||||
|
projectId: pkiSync.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Edit, ProjectPermissionSub.PkiSyncs);
|
||||||
|
|
||||||
|
const removedCount = await certificateSyncDAL.removeCertificates(pkiSyncId, certificateIds);
|
||||||
|
|
||||||
|
if (pkiSync.isAutoSyncEnabled) {
|
||||||
|
await pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSyncId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
removedCount,
|
||||||
|
pkiSyncInfo: {
|
||||||
|
projectId: pkiSync.projectId,
|
||||||
|
destination: pkiSync.destination,
|
||||||
|
name: pkiSync.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const listPkiSyncCertificates = async (
|
||||||
|
{ pkiSyncId, offset = 0, limit = 20 }: Omit<TListPkiSyncCertificatesDTO, "projectId">,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
): Promise<{
|
||||||
|
certificates: TPkiSyncCertificate[];
|
||||||
|
totalCount: number;
|
||||||
|
pkiSyncInfo: { projectId: string; destination: string; name: string };
|
||||||
|
}> => {
|
||||||
|
const pkiSync = await pkiSyncDAL.findById(pkiSyncId);
|
||||||
|
if (!pkiSync) throw new NotFoundError({ message: "PKI sync not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager,
|
||||||
|
projectId: pkiSync.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||||
|
|
||||||
|
const result = await certificateSyncDAL.findWithDetails({
|
||||||
|
pkiSyncId,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
const { certificateDetails, totalCount } = result;
|
||||||
|
|
||||||
|
const certificates = certificateDetails.map((detail) => ({
|
||||||
|
id: detail.id,
|
||||||
|
pkiSyncId: detail.pkiSyncId,
|
||||||
|
certificateId: detail.certificateId,
|
||||||
|
syncStatus: (detail.syncStatus as CertificateSyncStatus) || CertificateSyncStatus.Pending,
|
||||||
|
lastSyncMessage: detail.lastSyncMessage || undefined,
|
||||||
|
lastSyncedAt: detail.lastSyncedAt || undefined,
|
||||||
|
createdAt: detail.createdAt,
|
||||||
|
updatedAt: detail.updatedAt,
|
||||||
|
certificateSerialNumber: detail.certificateSerialNumber || undefined,
|
||||||
|
certificateCommonName: detail.certificateCommonName || undefined,
|
||||||
|
certificateAltNames: detail.certificateAltNames || undefined,
|
||||||
|
certificateStatus: detail.certificateStatus || undefined,
|
||||||
|
certificateNotBefore: detail.certificateNotBefore || undefined,
|
||||||
|
certificateNotAfter: detail.certificateNotAfter || undefined,
|
||||||
|
certificateRenewBeforeDays: !detail.certificateRenewedByCertificateId
|
||||||
|
? detail.certificateRenewBeforeDays || undefined
|
||||||
|
: undefined,
|
||||||
|
certificateRenewalError: detail.certificateRenewalError || undefined,
|
||||||
|
pkiSyncName: detail.pkiSyncName || undefined,
|
||||||
|
pkiSyncDestination: detail.pkiSyncDestination || undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificates,
|
||||||
|
totalCount,
|
||||||
|
pkiSyncInfo: {
|
||||||
|
projectId: pkiSync.projectId,
|
||||||
|
destination: pkiSync.destination,
|
||||||
|
name: pkiSync.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createPkiSync,
|
createPkiSync,
|
||||||
updatePkiSync,
|
updatePkiSync,
|
||||||
@@ -442,6 +678,9 @@ export const pkiSyncServiceFactory = ({
|
|||||||
triggerPkiSyncSyncCertificatesById,
|
triggerPkiSyncSyncCertificatesById,
|
||||||
triggerPkiSyncImportCertificatesById,
|
triggerPkiSyncImportCertificatesById,
|
||||||
triggerPkiSyncRemoveCertificatesById,
|
triggerPkiSyncRemoveCertificatesById,
|
||||||
getPkiSyncOptions
|
getPkiSyncOptions,
|
||||||
|
addCertificatesToPkiSync,
|
||||||
|
removeCertificatesFromPkiSync,
|
||||||
|
listPkiSyncCertificates
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Job } from "bullmq";
|
|||||||
|
|
||||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { QueueJobs } from "@app/queue";
|
import { QueueJobs } from "@app/queue";
|
||||||
|
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
@@ -70,7 +71,10 @@ export type TPkiSyncListItem = TPkiSync & {
|
|||||||
appConnectionApp: string;
|
appConnectionApp: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateMap = Record<string, { cert: string; privateKey: string; certificateChain?: string }>;
|
export type TCertificateMap = Record<
|
||||||
|
string,
|
||||||
|
{ cert: string; privateKey: string; certificateChain?: string; alternativeNames?: string[]; certificateId?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export type TCreatePkiSyncDTO = {
|
export type TCreatePkiSyncDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -79,9 +83,10 @@ export type TCreatePkiSyncDTO = {
|
|||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
destinationConfig: Record<string, unknown>;
|
destinationConfig: Record<string, unknown>;
|
||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
certificateIds?: string[];
|
||||||
auditLogInfo: AuditLogInfo;
|
auditLogInfo: AuditLogInfo;
|
||||||
resourceMetadata?: ResourceMetadataDTO;
|
resourceMetadata?: ResourceMetadataDTO;
|
||||||
};
|
};
|
||||||
@@ -94,8 +99,9 @@ export type TUpdatePkiSyncDTO = {
|
|||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
destinationConfig?: Record<string, unknown>;
|
destinationConfig?: Record<string, unknown>;
|
||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
|
certificateIds?: string[];
|
||||||
auditLogInfo: AuditLogInfo;
|
auditLogInfo: AuditLogInfo;
|
||||||
resourceMetadata?: ResourceMetadataDTO;
|
resourceMetadata?: ResourceMetadataDTO;
|
||||||
};
|
};
|
||||||
@@ -108,6 +114,7 @@ export type TDeletePkiSyncDTO = {
|
|||||||
|
|
||||||
export type TListPkiSyncsByProjectId = {
|
export type TListPkiSyncsByProjectId = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
certificateId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TFindPkiSyncByIdDTO = {
|
export type TFindPkiSyncByIdDTO = {
|
||||||
@@ -133,6 +140,48 @@ export type TTriggerPkiSyncRemoveCertificatesByIdDTO = {
|
|||||||
auditLogInfo: AuditLogInfo;
|
auditLogInfo: AuditLogInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAddCertificatesToPkiSyncDTO = {
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateIds: string[];
|
||||||
|
projectId?: string;
|
||||||
|
auditLogInfo: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRemoveCertificatesFromPkiSyncDTO = {
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateIds: string[];
|
||||||
|
projectId?: string;
|
||||||
|
auditLogInfo: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListPkiSyncCertificatesDTO = {
|
||||||
|
pkiSyncId: string;
|
||||||
|
projectId?: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPkiSyncCertificate = {
|
||||||
|
id: string;
|
||||||
|
pkiSyncId: string;
|
||||||
|
certificateId: string;
|
||||||
|
syncStatus: CertificateSyncStatus;
|
||||||
|
lastSyncMessage?: string;
|
||||||
|
lastSyncedAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
certificateSerialNumber?: string;
|
||||||
|
certificateCommonName?: string;
|
||||||
|
certificateAltNames?: string;
|
||||||
|
certificateStatus?: string;
|
||||||
|
certificateNotBefore?: Date;
|
||||||
|
certificateNotAfter?: Date;
|
||||||
|
certificateRenewBeforeDays?: number;
|
||||||
|
certificateRenewalError?: string;
|
||||||
|
pkiSyncName?: string;
|
||||||
|
pkiSyncDestination?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
export type TPkiSyncRaw = NonNullable<Awaited<ReturnType<TPkiSyncDALFactory["findById"]>>>;
|
||||||
|
|
||||||
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
|
export type TQueuePkiSyncSyncCertificatesByIdDTO = {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { TCertificateSyncDALFactory } from "../certificate-sync/certificate-sync-dal";
|
||||||
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
import { TPkiSyncDALFactory } from "./pki-sync-dal";
|
||||||
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
import { TPkiSyncQueueFactory } from "./pki-sync-queue";
|
||||||
|
|
||||||
@@ -25,3 +28,78 @@ export const triggerAutoSyncForSubscriber = async (
|
|||||||
logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
|
logger.error(error, `Failed to trigger auto sync for subscriber ${subscriberId}:`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const triggerAutoSyncForCertificate = async (
|
||||||
|
certificateId: string,
|
||||||
|
dependencies: {
|
||||||
|
certificateSyncDAL: Pick<TCertificateSyncDALFactory, "findPkiSyncIdsByCertificateId">;
|
||||||
|
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||||
|
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(certificateId);
|
||||||
|
|
||||||
|
if (pkiSyncIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPkiSyncs = await dependencies.pkiSyncDAL.find({
|
||||||
|
isAutoSyncEnabled: true,
|
||||||
|
$in: {
|
||||||
|
id: pkiSyncIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncPromises = allPkiSyncs.map((pkiSync) =>
|
||||||
|
dependencies.pkiSyncQueue.queuePkiSyncSyncCertificatesById({ syncId: pkiSync.id })
|
||||||
|
);
|
||||||
|
await Promise.all(syncPromises);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to trigger auto sync for certificate ${certificateId}:`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addRenewedCertificateToSyncs = async (
|
||||||
|
oldCertificateId: string,
|
||||||
|
newCertificateId: string,
|
||||||
|
dependencies: {
|
||||||
|
certificateSyncDAL: Pick<
|
||||||
|
TCertificateSyncDALFactory,
|
||||||
|
"findPkiSyncIdsByCertificateId" | "addCertificates" | "findByPkiSyncAndCertificate"
|
||||||
|
>;
|
||||||
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const pkiSyncIds = await dependencies.certificateSyncDAL.findPkiSyncIdsByCertificateId(oldCertificateId);
|
||||||
|
|
||||||
|
if (pkiSyncIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPromises = pkiSyncIds.map(async (pkiSyncId) => {
|
||||||
|
const oldCertificateRecord = await dependencies.certificateSyncDAL.findByPkiSyncAndCertificate(
|
||||||
|
pkiSyncId,
|
||||||
|
oldCertificateId
|
||||||
|
);
|
||||||
|
|
||||||
|
await dependencies.certificateSyncDAL.addCertificates(
|
||||||
|
pkiSyncId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
certificateId: newCertificateId,
|
||||||
|
externalIdentifier: oldCertificateRecord?.externalIdentifier || undefined
|
||||||
|
}
|
||||||
|
],
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(addPromises);
|
||||||
|
|
||||||
|
logger.info(`Successfully added renewed certificate ${newCertificateId} to PKI sync(s)`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Failed to add renewed certificate ${newCertificateId} to syncs:`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -156,7 +156,14 @@ type TProjectServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find" | "findWithAssociatedCa">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject" | "findWithPrivateKeyInfo">;
|
certificateDAL: Pick<
|
||||||
|
TCertificateDALFactory,
|
||||||
|
| "find"
|
||||||
|
| "countCertificatesInProject"
|
||||||
|
| "findWithPrivateKeyInfo"
|
||||||
|
| "findActiveCertificatesForSync"
|
||||||
|
| "countActiveCertificatesForSync"
|
||||||
|
>;
|
||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
@@ -929,6 +936,7 @@ export const projectServiceFactory = ({
|
|||||||
offset = 0,
|
offset = 0,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
|
forPkiSync = false,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -952,20 +960,35 @@ export const projectServiceFactory = ({
|
|||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
const certificates = await certificateDAL.findWithPrivateKeyInfo(
|
const certificates = forPkiSync
|
||||||
{
|
? await certificateDAL.findActiveCertificatesForSync(
|
||||||
projectId,
|
{
|
||||||
...(friendlyName && { friendlyName }),
|
projectId,
|
||||||
...(commonName && { commonName })
|
...(friendlyName && { friendlyName }),
|
||||||
},
|
...(commonName && { commonName })
|
||||||
{ offset, limit, sort: [["notAfter", "desc"]] }
|
},
|
||||||
);
|
{ offset, limit }
|
||||||
|
)
|
||||||
|
: await certificateDAL.findWithPrivateKeyInfo(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
...(friendlyName && { friendlyName }),
|
||||||
|
...(commonName && { commonName })
|
||||||
|
},
|
||||||
|
{ offset, limit, sort: [["notAfter", "desc"]] }
|
||||||
|
);
|
||||||
|
|
||||||
const count = await certificateDAL.countCertificatesInProject({
|
const count = forPkiSync
|
||||||
projectId,
|
? await certificateDAL.countActiveCertificatesForSync({
|
||||||
friendlyName,
|
projectId,
|
||||||
commonName
|
friendlyName,
|
||||||
});
|
commonName
|
||||||
|
})
|
||||||
|
: await certificateDAL.countCertificatesInProject({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
commonName
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
certificates,
|
certificates,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ export type TListProjectCertsDTO = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
|
forPkiSync?: boolean;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TListProjectAlertsDTO = TProjectPermission;
|
export type TListProjectAlertsDTO = TProjectPermission;
|
||||||
|
|||||||
@@ -419,13 +419,14 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.SecretFolder),
|
selectAllTableCols(TableName.SecretFolder),
|
||||||
db.raw(
|
db.raw(
|
||||||
`DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" ${
|
`DENSE_RANK() OVER (ORDER BY ${TableName.SecretFolder}."name" COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}) as rank`
|
||||||
orderDirection ?? OrderByDirection.ASC
|
|
||||||
}) as rank`
|
|
||||||
),
|
),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("environment")
|
db.ref("slug").withSchema(TableName.Environment).as("environment")
|
||||||
)
|
)
|
||||||
.orderBy(`${TableName.SecretFolder}.${orderBy}`, orderDirection);
|
.orderByRaw(
|
||||||
|
`${TableName.SecretFolder}.?? COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}`,
|
||||||
|
[orderBy]
|
||||||
|
);
|
||||||
|
|
||||||
if (limit) {
|
if (limit) {
|
||||||
const rankOffset = offset + 1; // ranks start from 1
|
const rankOffset = offset + 1; // ranks start from 1
|
||||||
@@ -434,7 +435,10 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
.select("*")
|
.select("*")
|
||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
.where("w.rank", ">=", rankOffset)
|
.where("w.rank", ">=", rankOffset)
|
||||||
.andWhere("w.rank", "<", rankOffset + limit);
|
.andWhere("w.rank", "<", rankOffset + limit)
|
||||||
|
.orderByRaw(`"w".?? COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}`, [
|
||||||
|
orderBy
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = await query;
|
const folders = await query;
|
||||||
@@ -445,7 +449,10 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByEnvsDeep = async ({ parentIds }: TFindFoldersDeepByParentIdsDTO, tx?: Knex) => {
|
const findByEnvsDeep = async (
|
||||||
|
{ parentIds, orderBy = SecretsOrderBy.Name, orderDirection = OrderByDirection.ASC }: TFindFoldersDeepByParentIdsDTO,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const folders = await (tx || db.replicaNode())
|
const folders = await (tx || db.replicaNode())
|
||||||
.withRecursive("parents", (qb) =>
|
.withRecursive("parents", (qb) =>
|
||||||
@@ -480,7 +487,9 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
|
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
|
||||||
.from("parents")
|
.from("parents")
|
||||||
.orderBy("depth")
|
.orderBy("depth")
|
||||||
.orderBy(`name`);
|
.orderByRaw(`"parents".?? COLLATE "en-x-icu" ${orderDirection === OrderByDirection.ASC ? "ASC" : "DESC"}`, [
|
||||||
|
orderBy
|
||||||
|
]);
|
||||||
|
|
||||||
return folders;
|
return folders;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { PgSqlLock } from "@app/keystore/keystore";
|
|||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -781,7 +782,11 @@ export const secretFolderServiceFactory = ({
|
|||||||
if (!parentFolder) return [];
|
if (!parentFolder) return [];
|
||||||
|
|
||||||
if (recursive) {
|
if (recursive) {
|
||||||
const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] });
|
const recursiveFolders = await folderDAL.findByEnvsDeep({
|
||||||
|
parentIds: [parentFolder.id],
|
||||||
|
orderBy: orderBy || SecretsOrderBy.Name,
|
||||||
|
orderDirection: orderDirection || OrderByDirection.ASC
|
||||||
|
});
|
||||||
// remove the parent folder
|
// remove the parent folder
|
||||||
return recursiveFolders
|
return recursiveFolders
|
||||||
.filter((folder) => {
|
.filter((folder) => {
|
||||||
@@ -800,19 +805,15 @@ export const secretFolderServiceFactory = ({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = await folderDAL.find(
|
const folders = await folderDAL.findByMultiEnv({
|
||||||
{
|
environmentIds: [env.id],
|
||||||
envId: env.id,
|
parentIds: [parentFolder.id],
|
||||||
parentId: parentFolder.id,
|
search,
|
||||||
isReserved: false,
|
orderBy: orderBy || SecretsOrderBy.Name,
|
||||||
$search: search ? { name: `%${search}%` } : undefined
|
orderDirection: orderDirection || OrderByDirection.ASC,
|
||||||
},
|
limit,
|
||||||
{
|
offset
|
||||||
sort: orderBy ? [[orderBy, orderDirection ?? OrderByDirection.ASC]] : undefined,
|
});
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (lastSecretModified) {
|
if (lastSecretModified) {
|
||||||
return folders.filter((el) =>
|
return folders.filter((el) =>
|
||||||
el.lastSecretModified ? el.lastSecretModified >= new Date(lastSecretModified) : false
|
el.lastSecretModified ? el.lastSecretModified >= new Date(lastSecretModified) : false
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export type TGetFoldersDeepByEnvsDTO = {
|
|||||||
|
|
||||||
export type TFindFoldersDeepByParentIdsDTO = {
|
export type TFindFoldersDeepByParentIdsDTO = {
|
||||||
parentIds: string[];
|
parentIds: string[];
|
||||||
|
orderBy?: SecretsOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateManyFoldersDTO = {
|
export type TCreateManyFoldersDTO = {
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export enum SecretSync {
|
|||||||
Netlify = "netlify",
|
Netlify = "netlify",
|
||||||
Northflank = "northflank",
|
Northflank = "northflank",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
LaravelForge = "laravel-forge"
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncInitialSyncBehavior {
|
export enum SecretSyncInitialSyncBehavior {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import handlebars from "handlebars";
|
|||||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { CHEF_SYNC_LIST_OPTION, ChefSyncFns } from "@app/ee/services/secret-sync/chef";
|
||||||
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
|
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import {
|
import {
|
||||||
@@ -49,8 +50,7 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
|||||||
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
|
||||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||||
import { LARAVEL_FORGE_SYNC_LIST_OPTION } from "./laravel-forge";
|
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
|
||||||
import { LaravelForgeSyncFns } from "./laravel-forge/laravel-forge-sync-fns";
|
|
||||||
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
|
||||||
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
|
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
|
||||||
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
|
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.Netlify]: NETLIFY_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
|
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
|
||||||
[SecretSync.Bitbucket]: BITBUCKET_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 = () => {
|
export const listSecretSyncOptions = () => {
|
||||||
@@ -286,6 +287,8 @@ export const SecretSyncFns = {
|
|||||||
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return LaravelForgeSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.Chef:
|
||||||
|
return ChefSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -408,6 +411,9 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
secretMap = await LaravelForgeSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.Chef:
|
||||||
|
secretMap = await ChefSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -505,6 +511,8 @@ export const SecretSyncFns = {
|
|||||||
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.LaravelForge:
|
case SecretSync.LaravelForge:
|
||||||
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return LaravelForgeSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
|
case SecretSync.Chef:
|
||||||
|
return ChefSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`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.Netlify]: "Netlify",
|
||||||
[SecretSync.Northflank]: "Northflank",
|
[SecretSync.Northflank]: "Northflank",
|
||||||
[SecretSync.Bitbucket]: "Bitbucket",
|
[SecretSync.Bitbucket]: "Bitbucket",
|
||||||
[SecretSync.LaravelForge]: "Laravel Forge"
|
[SecretSync.LaravelForge]: "Laravel Forge",
|
||||||
|
[SecretSync.Chef]: "Chef"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
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.Netlify]: AppConnection.Netlify,
|
||||||
[SecretSync.Northflank]: AppConnection.Northflank,
|
[SecretSync.Northflank]: AppConnection.Northflank,
|
||||||
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
|
[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> = {
|
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.Netlify]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
|
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
|
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
|
||||||
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular
|
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular,
|
||||||
|
[SecretSync.Chef]: SecretSyncPlanType.Enterprise
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
|
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.Netlify]: ["accountName", "siteName"],
|
||||||
[SecretSync.Northflank]: [],
|
[SecretSync.Northflank]: [],
|
||||||
[SecretSync.Bitbucket]: [],
|
[SecretSync.Bitbucket]: [],
|
||||||
[SecretSync.LaravelForge]: []
|
[SecretSync.LaravelForge]: [],
|
||||||
|
[SecretSync.Chef]: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
|
const defaultDuplicateCheck: DestinationDuplicateCheckFn = () => true;
|
||||||
@@ -209,5 +213,6 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
|
|||||||
[SecretSync.Netlify]: defaultDuplicateCheck,
|
[SecretSync.Netlify]: defaultDuplicateCheck,
|
||||||
[SecretSync.Northflank]: defaultDuplicateCheck,
|
[SecretSync.Northflank]: defaultDuplicateCheck,
|
||||||
[SecretSync.Bitbucket]: defaultDuplicateCheck,
|
[SecretSync.Bitbucket]: defaultDuplicateCheck,
|
||||||
[SecretSync.LaravelForge]: defaultDuplicateCheck
|
[SecretSync.LaravelForge]: defaultDuplicateCheck,
|
||||||
|
[SecretSync.Chef]: defaultDuplicateCheck
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
|
|
||||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import {
|
||||||
|
TChefSync,
|
||||||
|
TChefSyncInput,
|
||||||
|
TChefSyncListItem,
|
||||||
|
TChefSyncWithCredentials
|
||||||
|
} from "@app/ee/services/secret-sync/chef";
|
||||||
import {
|
import {
|
||||||
TOCIVaultSync,
|
TOCIVaultSync,
|
||||||
TOCIVaultSyncInput,
|
TOCIVaultSyncInput,
|
||||||
@@ -169,6 +175,7 @@ export type TSecretSync =
|
|||||||
| TGitHubSync
|
| TGitHubSync
|
||||||
| TGcpSync
|
| TGcpSync
|
||||||
| TAzureKeyVaultSync
|
| TAzureKeyVaultSync
|
||||||
|
| TChefSync
|
||||||
| TAzureAppConfigurationSync
|
| TAzureAppConfigurationSync
|
||||||
| TAzureDevOpsSync
|
| TAzureDevOpsSync
|
||||||
| TDatabricksSync
|
| TDatabricksSync
|
||||||
@@ -202,6 +209,7 @@ export type TSecretSyncWithCredentials =
|
|||||||
| TGitHubSyncWithCredentials
|
| TGitHubSyncWithCredentials
|
||||||
| TGcpSyncWithCredentials
|
| TGcpSyncWithCredentials
|
||||||
| TAzureKeyVaultSyncWithCredentials
|
| TAzureKeyVaultSyncWithCredentials
|
||||||
|
| TChefSyncWithCredentials
|
||||||
| TAzureAppConfigurationSyncWithCredentials
|
| TAzureAppConfigurationSyncWithCredentials
|
||||||
| TAzureDevOpsSyncWithCredentials
|
| TAzureDevOpsSyncWithCredentials
|
||||||
| TDatabricksSyncWithCredentials
|
| TDatabricksSyncWithCredentials
|
||||||
@@ -236,6 +244,7 @@ export type TSecretSyncInput =
|
|||||||
| TGitHubSyncInput
|
| TGitHubSyncInput
|
||||||
| TGcpSyncInput
|
| TGcpSyncInput
|
||||||
| TAzureKeyVaultSyncInput
|
| TAzureKeyVaultSyncInput
|
||||||
|
| TChefSyncInput
|
||||||
| TAzureAppConfigurationSyncInput
|
| TAzureAppConfigurationSyncInput
|
||||||
| TAzureDevOpsSyncInput
|
| TAzureDevOpsSyncInput
|
||||||
| TDatabricksSyncInput
|
| TDatabricksSyncInput
|
||||||
@@ -270,6 +279,7 @@ export type TSecretSyncListItem =
|
|||||||
| TGitHubSyncListItem
|
| TGitHubSyncListItem
|
||||||
| TGcpSyncListItem
|
| TGcpSyncListItem
|
||||||
| TAzureKeyVaultSyncListItem
|
| TAzureKeyVaultSyncListItem
|
||||||
|
| TChefSyncListItem
|
||||||
| TAzureAppConfigurationSyncListItem
|
| TAzureAppConfigurationSyncListItem
|
||||||
| TAzureDevOpsSyncListItem
|
| TAzureDevOpsSyncListItem
|
||||||
| TDatabricksSyncListItem
|
| 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}"
|
||||||
|
---
|
||||||
+33
-1
@@ -114,6 +114,7 @@
|
|||||||
"integrations/app-connections/bitbucket",
|
"integrations/app-connections/bitbucket",
|
||||||
"integrations/app-connections/camunda",
|
"integrations/app-connections/camunda",
|
||||||
"integrations/app-connections/checkly",
|
"integrations/app-connections/checkly",
|
||||||
|
"integrations/app-connections/chef",
|
||||||
"integrations/app-connections/cloudflare",
|
"integrations/app-connections/cloudflare",
|
||||||
"integrations/app-connections/databricks",
|
"integrations/app-connections/databricks",
|
||||||
"integrations/app-connections/digital-ocean",
|
"integrations/app-connections/digital-ocean",
|
||||||
@@ -540,6 +541,7 @@
|
|||||||
"integrations/secret-syncs/bitbucket",
|
"integrations/secret-syncs/bitbucket",
|
||||||
"integrations/secret-syncs/camunda",
|
"integrations/secret-syncs/camunda",
|
||||||
"integrations/secret-syncs/checkly",
|
"integrations/secret-syncs/checkly",
|
||||||
|
"integrations/secret-syncs/chef",
|
||||||
"integrations/secret-syncs/cloudflare-pages",
|
"integrations/secret-syncs/cloudflare-pages",
|
||||||
"integrations/secret-syncs/cloudflare-workers",
|
"integrations/secret-syncs/cloudflare-workers",
|
||||||
"integrations/secret-syncs/databricks",
|
"integrations/secret-syncs/databricks",
|
||||||
@@ -814,7 +816,10 @@
|
|||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"group": "Infisical PAM",
|
"group": "Infisical PAM",
|
||||||
"pages": ["documentation/platform/pam/overview"]
|
"pages": [
|
||||||
|
"documentation/platform/pam/overview",
|
||||||
|
"documentation/platform/pam/session-recording"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1690,6 +1695,18 @@
|
|||||||
"api-reference/endpoints/app-connections/checkly/delete"
|
"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",
|
"group": "Cloudflare",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -2181,6 +2198,20 @@
|
|||||||
"api-reference/endpoints/secret-syncs/checkly/remove-secrets"
|
"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",
|
"group": "Cloudflare Pages",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -2336,6 +2367,7 @@
|
|||||||
"api-reference/endpoints/secret-syncs/laravel-forge/update",
|
"api-reference/endpoints/secret-syncs/laravel-forge/update",
|
||||||
"api-reference/endpoints/secret-syncs/laravel-forge/delete",
|
"api-reference/endpoints/secret-syncs/laravel-forge/delete",
|
||||||
"api-reference/endpoints/secret-syncs/laravel-forge/sync-secrets",
|
"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"
|
"api-reference/endpoints/secret-syncs/laravel-forge/remove-secrets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ Infisical is designed to provide comprehensive, centralized, and efficient manag
|
|||||||
### 2. Projects
|
### 2. Projects
|
||||||
|
|
||||||
- **Definition and Role**: [Projects](/documentation/platform/project) are the highest-level construct within an [organization](/documentation/platform/organization) in Infisical. They serve as the primary container for all functionalities.
|
- **Definition and Role**: [Projects](/documentation/platform/project) are the highest-level construct within an [organization](/documentation/platform/organization) in Infisical. They serve as the primary container for all functionalities.
|
||||||
- **Correspondence to Code Repositories**: Projects typically align with specific code repositories.
|
- **Common Project Mappings**: Projects typically align with applications, services, or code repositories — each being a valid and common approach depending on your organizational structure.
|
||||||
- **Functional Capabilities**: Each project encompasses features for managing secrets, certificates, and encryption keys, serving as the central hub for these resources.
|
- **Functional Capabilities**: Each project encompasses features for managing secrets, certificates, and encryption keys, serving as the central hub for these resources.
|
||||||
|
|
||||||
|
<Note>Projects are isolated from one another. Secrets, certificates, and other resources cannot be shared or referenced across different projects. Each project maintains its own separate set of resources.</Note>
|
||||||
|
|
||||||
### 3. Environments
|
### 3. Environments
|
||||||
|
|
||||||
- **Purpose**: Environments are designed for organizing and compartmentalizing secrets within projects.
|
- **Purpose**: Environments are designed for organizing and compartmentalizing secrets within projects.
|
||||||
@@ -40,8 +42,9 @@ Infisical is designed to provide comprehensive, centralized, and efficient manag
|
|||||||
|
|
||||||
### 5. Imports
|
### 5. Imports
|
||||||
|
|
||||||
- **Purpose and Benefits**: To promote reusability and avoid redundancy, Infisical supports the use of imports. This allows secrets, folders, or entire environments to be referenced across multiple projects as needed.
|
- **Purpose and Benefits**: To promote reusability and avoid redundancy within a project, Infisical supports the use of imports and references. This allows secrets, folders, or entire environments to be referenced within the same project as needed.
|
||||||
- **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead.
|
- **Project Isolation**: Imports and references only work within a single project. Secrets cannot be imported or referenced across different projects, as projects are isolated from one another.
|
||||||
|
- **Best Practice**: Utilizing [secret imports](/documentation/platform/secret-reference#secret-imports) or [references](/documentation/platform/secret-reference#secret-referencing) ensures consistency and minimizes manual overhead when managing secrets within a project.
|
||||||
|
|
||||||
### 6. Approval Workflows
|
### 6. Approval Workflows
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: "Session Recording"
|
||||||
|
sidebarTitle: "Session Recording"
|
||||||
|
description: "Learn how Infisical records and stores session activity for auditing and monitoring."
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical's Privileged Access Management (PAM) provides robust session recording capabilities to help you audit and monitor user activity across your infrastructure.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
When a user initiates a session through the Infisical Gateway, a recording of the session begins. The gateway securely caches all recording data in temporary encrypted files on its local system.
|
||||||
|
|
||||||
|
Once the session concludes, the gateway transmits the complete recording to the Infisical platform for long-term, centralized storage. This asynchronous process ensures that sessions remain operational even if the connection to the Infisical platform is temporarily lost. After the upload is complete, administrators can search and review the session logs in the Infisical UI.
|
||||||
|
|
||||||
|
## What's Captured
|
||||||
|
|
||||||
|
The content captured during a session depends on the type of resource being accessed.
|
||||||
|
|
||||||
|
### Database Sessions
|
||||||
|
|
||||||
|
For database connections, Infisical captures all queries executed and their corresponding responses.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Support for additional resource types like SSH and RDP is coming soon.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Viewing Recordings
|
||||||
|
|
||||||
|
To review session recordings:
|
||||||
|
|
||||||
|
1. Navigate to the **PAM Sessions** page in your project.
|
||||||
|
2. Click on a session from the list to view its details.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The session details page provides key information, including the complete session logs, connection status, the user who initiated it, and more.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Searching Logs
|
||||||
|
|
||||||
|
You can use the search bar to quickly find relevant information:
|
||||||
|
|
||||||
|
- **On the main Sessions page:** Search across all session logs to locate specific queries or outputs.
|
||||||
|
- **On an individual session page:** Search within that specific session's logs to pinpoint activity.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Are session recordings encrypted?">
|
||||||
|
Yes. All session recordings are encrypted at rest by default, ensuring your audit data is always secure.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why aren't recordings streamed in real-time?">
|
||||||
|
Currently, Infisical uses an asynchronous approach where the gateway records the entire session locally before uploading it. This design makes your PAM sessions more resilient, as they don't depend on a constant, active connection to the Infisical platform. We may introduce live streaming capabilities in a future release.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
@@ -28,46 +28,54 @@ This section walks you through the complete end-to-end process of setting up Azu
|
|||||||
**Certificate Authority** to access the external CAs page. 
|
Page](/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png)
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Create New Azure ADCS Certificate Service CA">
|
|
||||||
Click **Create CA** and configure: - **Type**: Choose **Azure AD Certificate
|
<Step title="Create New Azure ADCS Certificate Service CA">
|
||||||
Service** - **Name**: Friendly name for this CA (e.g., "Production ADCS CA") -
|
Click **Create CA** and configure: - **Type**: Choose **Active Directory
|
||||||
**App Connection**: Choose your ADCS connection from the dropdown 
|
"Production ADCS CA") - **App Connection**: Choose your ADCS connection from
|
||||||
</Step>
|
the dropdown 
|
||||||
Once created, your Azure ADCS Certificate Authority will appear in the list
|
</Step>
|
||||||
and be ready for use. 
|
<Step title="Certificate Authority Created">
|
||||||
</Step>
|
Once created, your Azure ADCS Certificate Authority will appear in the list
|
||||||
<Step title="Navigate to Subscribers">
|
and be ready for use. 
|
||||||
Page](/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png)
|
</Step>
|
||||||
</Step>
|
|
||||||
<Step title="Create New Subscriber">
|
<Step title="Navigate to Subscribers">
|
||||||
Click **Add Subscriber** and configure: - **Name**: Unique subscriber name
|
Go to **Subscribers** to access the subscribers page. 
|
||||||
**Common Name**: Certificate CN (e.g., "api.example.com") - **Certificate
|
</Step>
|
||||||
Template**: Select from dynamically loaded ADCS templates - **Subject
|
|
||||||
Alternative Names**: DNS names, IP addresses, or email addresses - **TTL**:
|
<Step title="Create New Subscriber">
|
||||||
Certificate validity period (e.g., "1y" for 1 year) - **Additional Subject
|
Click **Add Subscriber** and configure: - **Name**: Unique subscriber name
|
||||||
Fields**: Organization, OU, locality, state, country, email (if required by
|
(e.g., "web-server-certs") - **Certificate Authority**: Select your ADCS CA
|
||||||
template) 
|
Template**: Select from dynamically loaded ADCS templates - **Subject
|
||||||
</Step>
|
Alternative Names**: DNS names, IP addresses, or email addresses - **TTL**:
|
||||||
<Step title="Subscriber Created">
|
Certificate validity period (e.g., "1y" for 1 year) - **Additional Subject
|
||||||
Your subscriber is now created and ready to issue certificates. 
|
template) 
|
||||||
<Step title="Issue New Certificate">
|
</Step>
|
||||||
Click into your subscriber and click **Order Certificate** to generate a new
|
|
||||||
certificate using your ADCS template. 
|
Your subscriber is now created and ready to issue certificates. 
|
||||||
<Step title="Certificate Created">
|
</Step>
|
||||||
Your certificate has been successfully issued by the ADCS server and is ready
|
|
||||||
for use. 
|
Click into your subscriber and click **Order Certificate** to generate a new
|
||||||
</Step>
|
certificate using your ADCS template. 
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Certificate Created">
|
||||||
|
Your certificate has been successfully issued by the ADCS server and is
|
||||||
|
ready for use. 
|
||||||
|
</Step>
|
||||||
|
|
||||||
<Step title="View Certificate Details">
|
<Step title="View Certificate Details">
|
||||||
Navigate to **Certificates** to view detailed information about all issued
|
Navigate to **Certificates** to view detailed information about all issued
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user