mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 00:27:30 +00:00
Fix cert profile edit missing enrollment config fields
This commit is contained in:
@@ -1139,7 +1139,9 @@ export const registerRoutes = async (
|
|||||||
certificateTemplateV2DAL,
|
certificateTemplateV2DAL,
|
||||||
apiEnrollmentConfigDAL,
|
apiEnrollmentConfigDAL,
|
||||||
estEnrollmentConfigDAL,
|
estEnrollmentConfigDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiAlertService = pkiAlertServiceFactory({
|
const pkiAlertService = pkiAlertServiceFactory({
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
passphraseInput: z.string().min(1),
|
passphrase: z.string().min(1),
|
||||||
encryptedCaChain: z.string().optional()
|
caChain: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
apiConfig: z
|
apiConfig: z
|
||||||
@@ -137,6 +137,21 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
expiringCertificates: z.number(),
|
expiringCertificates: z.number(),
|
||||||
revokedCertificates: z.number()
|
revokedCertificates: z.number()
|
||||||
})
|
})
|
||||||
|
.optional(),
|
||||||
|
estConfig: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
disableBootstrapCaValidation: z.boolean(),
|
||||||
|
passphrase: z.string(),
|
||||||
|
caChain: z.string().optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
apiConfig: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
autoRenew: z.boolean(),
|
||||||
|
autoRenewDays: z.number().optional()
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
}).array(),
|
}).array(),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
@@ -207,8 +222,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
disableBootstrapCaValidation: z.boolean(),
|
disableBootstrapCaValidation: z.boolean(),
|
||||||
hashedPassphrase: z.string(),
|
passphrase: z.string(),
|
||||||
encryptedCaChain: z.string()
|
caChain: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
apiConfig: z
|
apiConfig: z
|
||||||
@@ -333,8 +348,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
passphraseInput: z.string().min(1),
|
passphrase: z.string().min(1).optional(),
|
||||||
encryptedCaChain: z.string()
|
caChain: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
apiConfig: z
|
apiConfig: z
|
||||||
|
|||||||
@@ -119,12 +119,12 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
if (!result) return undefined;
|
if (!result) return undefined;
|
||||||
|
|
||||||
const estConfig =
|
const estConfig =
|
||||||
result.estConfigEncryptedCaChain && result.estConfigId && result.estConfigHashedPassphrase
|
result.estConfigId && result.estConfigHashedPassphrase
|
||||||
? ({
|
? ({
|
||||||
id: result.estConfigId,
|
id: result.estConfigId,
|
||||||
disableBootstrapCaValidation: !!result.estConfigDisableBootstrapCaValidation,
|
disableBootstrapCaValidation: !!result.estConfigDisableBootstrapCaValidation,
|
||||||
hashedPassphrase: result.estConfigHashedPassphrase,
|
passphrase: "",
|
||||||
encryptedCaChain: result.estConfigEncryptedCaChain.toString("base64")
|
caChain: result.estConfigEncryptedCaChain ? result.estConfigEncryptedCaChain.toString("utf8") : ""
|
||||||
} as TCertificateProfileWithConfigs["estConfig"])
|
} as TCertificateProfileWithConfigs["estConfig"])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
expiringDays?: number;
|
expiringDays?: number;
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[]> => {
|
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
offset = 0,
|
offset = 0,
|
||||||
@@ -219,13 +219,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
expiringDays = 7
|
expiringDays = 7
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let query = (tx || db)(TableName.PkiCertificateProfile).where(
|
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
|
||||||
`${TableName.PkiCertificateProfile}.projectId`,
|
`${TableName.PkiCertificateProfile}.projectId`,
|
||||||
projectId
|
projectId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query = query.where((builder) => {
|
baseQuery = baseQuery.where((builder) => {
|
||||||
void builder.where((qb) => {
|
void builder.where((qb) => {
|
||||||
void qb
|
void qb
|
||||||
.whereILike(`${TableName.PkiCertificateProfile}.slug`, `%${search}%`)
|
.whereILike(`${TableName.PkiCertificateProfile}.slug`, `%${search}%`)
|
||||||
@@ -235,26 +235,62 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (enrollmentType) {
|
if (enrollmentType) {
|
||||||
query = query.where(`${TableName.PkiCertificateProfile}.enrollmentType`, enrollmentType);
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.enrollmentType`, enrollmentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caId) {
|
if (caId) {
|
||||||
query = query.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let query = baseQuery
|
||||||
|
.leftJoin(
|
||||||
|
TableName.PkiEstEnrollmentConfig,
|
||||||
|
`${TableName.PkiCertificateProfile}.estConfigId`,
|
||||||
|
`${TableName.PkiEstEnrollmentConfig}.id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.PkiApiEnrollmentConfig,
|
||||||
|
`${TableName.PkiCertificateProfile}.apiConfigId`,
|
||||||
|
`${TableName.PkiApiEnrollmentConfig}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.PkiCertificateProfile))
|
||||||
|
.select(
|
||||||
|
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("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays")
|
||||||
|
);
|
||||||
|
|
||||||
if (includeMetrics) {
|
if (includeMetrics) {
|
||||||
|
query = query.leftJoin(
|
||||||
|
TableName.Certificate,
|
||||||
|
`${TableName.PkiCertificateProfile}.id`,
|
||||||
|
`${TableName.Certificate}.profileId`
|
||||||
|
);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const expiringDate = new Date();
|
const expiringDate = new Date();
|
||||||
expiringDate.setDate(now.getDate() + expiringDays);
|
expiringDate.setDate(now.getDate() + expiringDays);
|
||||||
|
|
||||||
const certificateProfiles = await query
|
query = query
|
||||||
.leftJoin(
|
|
||||||
TableName.Certificate,
|
|
||||||
`${TableName.PkiCertificateProfile}.id`,
|
|
||||||
`${TableName.Certificate}.profileId`
|
|
||||||
)
|
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.PkiCertificateProfile),
|
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("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays"),
|
||||||
db.raw("COUNT(certificates.id) as total_certificates"),
|
db.raw("COUNT(certificates.id) as total_certificates"),
|
||||||
db.raw(
|
db.raw(
|
||||||
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
|
||||||
@@ -270,21 +306,66 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
),
|
),
|
||||||
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
||||||
)
|
)
|
||||||
.groupBy(`${TableName.PkiCertificateProfile}.id`)
|
.groupBy(
|
||||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
`${TableName.PkiCertificateProfile}.id`,
|
||||||
.offset(offset)
|
`${TableName.PkiEstEnrollmentConfig}.id`,
|
||||||
.limit(limit);
|
`${TableName.PkiApiEnrollmentConfig}.id`
|
||||||
|
);
|
||||||
return certificateProfiles as TCertificateProfileWithRawMetrics[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const certificateProfiles = await query
|
const results = (await query
|
||||||
.select(selectAllTableCols(TableName.PkiCertificateProfile))
|
|
||||||
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit);
|
.limit(limit)) as Record<string, unknown>[];
|
||||||
|
|
||||||
return certificateProfiles as TCertificateProfile[];
|
return results.map((result: Record<string, unknown>) => {
|
||||||
|
const estConfig =
|
||||||
|
result.estId && result.estHashedPassphrase
|
||||||
|
? {
|
||||||
|
id: result.estId as string,
|
||||||
|
disableBootstrapCaValidation: !!result.estDisableBootstrapCaValidation,
|
||||||
|
passphrase: "",
|
||||||
|
caChain: result.estEncryptedCaChain ? (result.estEncryptedCaChain as Buffer).toString("utf8") : ""
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const apiConfig = result.apiId
|
||||||
|
? {
|
||||||
|
id: result.apiId as string,
|
||||||
|
autoRenew: !!result.apiAutoRenew,
|
||||||
|
autoRenewDays: (result.apiAutoRenewDays as number) || undefined
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const baseProfile = {
|
||||||
|
id: result.id,
|
||||||
|
projectId: result.projectId,
|
||||||
|
caId: result.caId,
|
||||||
|
certificateTemplateId: result.certificateTemplateId,
|
||||||
|
slug: result.slug,
|
||||||
|
description: result.description,
|
||||||
|
enrollmentType: result.enrollmentType as EnrollmentType,
|
||||||
|
estConfigId: result.estConfigId,
|
||||||
|
apiConfigId: result.apiConfigId,
|
||||||
|
createdAt: result.createdAt,
|
||||||
|
updatedAt: result.updatedAt,
|
||||||
|
estConfig,
|
||||||
|
apiConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeMetrics) {
|
||||||
|
return {
|
||||||
|
...baseProfile,
|
||||||
|
total_certificates: result.total_certificates,
|
||||||
|
active_certificates: result.active_certificates,
|
||||||
|
expired_certificates: result.expired_certificates,
|
||||||
|
expiring_certificates: result.expiring_certificates,
|
||||||
|
revoked_certificates: result.revoked_certificates
|
||||||
|
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseProfile as TCertificateProfileWithConfigs;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find certificate profiles by project id" });
|
throw new DatabaseError({ error, name: "Find certificate profiles by project id" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { ActorType, AuthMethod } from "../auth/auth-type";
|
|||||||
import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||||
import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||||
import type { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
|
import type { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
|
||||||
|
import type { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import type { TProjectDALFactory } from "../project/project-dal";
|
||||||
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
import type { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||||
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
|
import { certificateProfileServiceFactory, TCertificateProfileServiceFactory } from "./certificate-profile-service";
|
||||||
import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types";
|
import { EnrollmentType, TCertificateProfile, TCertificateProfileWithConfigs } from "./certificate-profile-types";
|
||||||
@@ -149,6 +151,22 @@ describe("CertificateProfileService", () => {
|
|||||||
})
|
})
|
||||||
} as unknown as Pick<TPermissionServiceFactory, "getProjectPermission">;
|
} as unknown as Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
|
||||||
|
const mockKmsService = {
|
||||||
|
encryptWithKmsKey: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(() => Promise.resolve({ cipherTextBlob: Buffer.from("encrypted-data") })),
|
||||||
|
decryptWithKmsKey: vi.fn().mockResolvedValue(() => Promise.resolve(Buffer.from("decrypted-ca-chain"))),
|
||||||
|
generateKmsKey: vi.fn()
|
||||||
|
} as unknown as Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
|
|
||||||
|
const mockProjectDAL = {
|
||||||
|
findById: vi.fn(),
|
||||||
|
findOne: vi.fn(),
|
||||||
|
updateById: vi.fn(),
|
||||||
|
findProjectBySlug: vi.fn(),
|
||||||
|
transaction: vi.fn()
|
||||||
|
} as unknown as Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
||||||
throwUnlessCan: vi.fn()
|
throwUnlessCan: vi.fn()
|
||||||
@@ -165,7 +183,9 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateV2DAL: mockCertificateTemplateV2DAL,
|
certificateTemplateV2DAL: mockCertificateTemplateV2DAL,
|
||||||
apiEnrollmentConfigDAL: mockApiEnrollmentConfigDAL,
|
apiEnrollmentConfigDAL: mockApiEnrollmentConfigDAL,
|
||||||
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL,
|
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL,
|
||||||
permissionService: mockPermissionService
|
permissionService: mockPermissionService,
|
||||||
|
kmsService: mockKmsService,
|
||||||
|
projectDAL: mockProjectDAL
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -698,8 +718,9 @@ describe("CertificateProfileService", () => {
|
|||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
estConfig: {
|
estConfig: {
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
passphraseInput: "secret-passphrase",
|
passphrase: "secret-passphrase",
|
||||||
encryptedCaChain: Buffer.from("test-ca-chain-data").toString("base64")
|
caChain:
|
||||||
|
"-----BEGIN CERTIFICATE-----\nMIIC+DCCAeCgAwIBAgIUBmCvLQ7l6CmNYjGeGXqIaS9LPuUwDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEChMJSW5maXNpY2FsMB4XDTI1MTAxNzE1MjczMFoXDTM1MTAx\nNzAwMDAwMFowFDESMBAGA1UEChMJSW5maXNpY2FsMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAqRS0ZKh44Y1GHvD4/ryduaelVtfvqkdCmhxpCp7OTjIA\n/gPuVoBA31gxqMVcpDgIAk8dfqds0WFzFe2byhbBalNm3+FSYJkEKa1mdCnqM/mL\nt6O0V/dPv2dcepDluwWbHJIuFf5elH1F8eeyqZV5w6c980lOyDO0DVNqB6pjGlPq\njEVcvEdEtGSfIX3B2tmODilwUvl/lGjhnK6ghfots7i1Xno9VAY/YTqR0T+lyPx4\n23r+22gstJ7XCLA7aqfRyFyYaVKqubHPBwz2qKiBTc3Shc3ii/OHc5KjTpADNRDv\nvH7X5kOXYtdpGbMsJ1uY+MPwfbOVkxy4tg4HFejmyQIDAQABo0IwQDAPBgNVHRMB\nAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUpshrlfvvw+zkoLKf\nxNUYD92/YxIwDQYJKoZIhvcNAQELBQADggEBAAWDMNe8HnoOPHF1sIUcCvJjBeUz\neB++l5Er9P+UPpkSr7+KpD+9DQGWmaOT57Vp7nBYXd42828h+cq7KEG2w5Uf6fYD\nBuitrzj2IzNznvKwOMh/qAePC17tH4mnkSnsJCMg6cvG99GG+vQoMQW7+D6VshIH\nm5hNThNGSPznk+eNk+NlIIVzD4autRn+U5geYzDaZIWfmx95gwCPK2VVw1IDExA+\naQiZi4g1JviUB97E92rZzX+Ai4GYk+CKQTxAiZPZ2M9gRFLrjKIGbRu7FaL+9lwU\nWnax4HJZ/cdVUtVp8VgaAOy7qvl5WGZ4eLopLhMkW3RyPiFr4+M3vNJocqU=\n-----END CERTIFICATE-----"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -723,7 +744,7 @@ describe("CertificateProfileService", () => {
|
|||||||
{
|
{
|
||||||
disableBootstrapCaValidation: estProfileData.estConfig.disableBootstrapCaValidation,
|
disableBootstrapCaValidation: estProfileData.estConfig.disableBootstrapCaValidation,
|
||||||
hashedPassphrase: "mocked-hash",
|
hashedPassphrase: "mocked-hash",
|
||||||
encryptedCaChain: Buffer.from(estProfileData.estConfig.encryptedCaChain, "base64")
|
encryptedCaChain: Buffer.from("encrypted-data")
|
||||||
},
|
},
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
@@ -1091,8 +1112,8 @@ describe("CertificateProfileService", () => {
|
|||||||
estConfig: {
|
estConfig: {
|
||||||
id: "est-config-123",
|
id: "est-config-123",
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
hashedPassphrase: "hashed-passphrase",
|
passphrase: "",
|
||||||
encryptedCaChain: Buffer.from("mock-ca-chain").toString("base64")
|
caChain: "mock-ca-chain"
|
||||||
}
|
}
|
||||||
} as TCertificateProfileWithConfigs;
|
} as TCertificateProfileWithConfigs;
|
||||||
|
|
||||||
@@ -1103,9 +1124,9 @@ describe("CertificateProfileService", () => {
|
|||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
orgId: "project-123",
|
orgId: "project-123",
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
caChain: "bW9jay1jYS1jaGFpbg==", // base64 encoded
|
caChain: "mock-ca-chain",
|
||||||
disableBootstrapCertValidation: false,
|
disableBootstrapCertValidation: false,
|
||||||
hashedPassphrase: "hashed-passphrase"
|
hashedPassphrase: ""
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1125,8 +1146,8 @@ describe("CertificateProfileService", () => {
|
|||||||
estConfig: {
|
estConfig: {
|
||||||
id: "est-config-123",
|
id: "est-config-123",
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
hashedPassphrase: "hashed-passphrase",
|
passphrase: "",
|
||||||
encryptedCaChain: Buffer.from("mock-ca-chain").toString("base64")
|
caChain: "mock-ca-chain"
|
||||||
}
|
}
|
||||||
} as TCertificateProfileWithConfigs;
|
} as TCertificateProfileWithConfigs;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
@@ -6,15 +7,20 @@ import {
|
|||||||
ProjectPermissionCertificateProfileActions,
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||||
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||||
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||||
import { TApiConfigData, TEstConfigData } from "../enrollment-config/enrollment-config-types";
|
import { TApiConfigData, TEstConfigData } from "../enrollment-config/enrollment-config-types";
|
||||||
import { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
|
import { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
|
||||||
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||||
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
|
||||||
import {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
@@ -27,18 +33,67 @@ import {
|
|||||||
TCertificateProfileWithRawMetrics
|
TCertificateProfileWithRawMetrics
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
const validateAndEncodeBase64CaChain = (caChain: unknown) => {
|
const validateAndEncryptPemCaChain = async (
|
||||||
|
caChain: string,
|
||||||
|
projectId: string,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
if (typeof caChain !== "string") {
|
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||||
throw new BadRequestError({ message: "CA chain must be a string" });
|
|
||||||
|
if (!certificates || certificates.length === 0) {
|
||||||
|
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||||
}
|
}
|
||||||
const buffer = Buffer.from(caChain, "base64");
|
|
||||||
if (buffer.toString("base64") !== caChain) {
|
if (!(await isCertChainValid(certificates))) {
|
||||||
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
|
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||||
}
|
}
|
||||||
return { encryptedCaChain: buffer };
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(caChain)
|
||||||
|
});
|
||||||
|
|
||||||
|
return { encryptedCaChain: cipherTextBlob };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
|
throw new BadRequestError({ message: `Failed to process certificate chain: ${(error as Error).message}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decryptCaChain = async (
|
||||||
|
encryptedCaChain: Buffer,
|
||||||
|
projectId: string,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey">,
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaChain = await kmsDecryptor({
|
||||||
|
cipherTextBlob: encryptedCaChain
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptedCaChain.toString();
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestError({ message: `Failed to decrypt certificate chain: ${(error as Error).message}` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,6 +108,8 @@ type TCertificateProfileServiceFactoryDep = {
|
|||||||
apiEnrollmentConfigDAL: TApiEnrollmentConfigDALFactory;
|
apiEnrollmentConfigDAL: TApiEnrollmentConfigDALFactory;
|
||||||
estEnrollmentConfigDAL: TEstEnrollmentConfigDALFactory;
|
estEnrollmentConfigDAL: TEstEnrollmentConfigDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileServiceFactory = ReturnType<typeof certificateProfileServiceFactory>;
|
export type TCertificateProfileServiceFactory = ReturnType<typeof certificateProfileServiceFactory>;
|
||||||
@@ -69,7 +126,9 @@ export const certificateProfileServiceFactory = ({
|
|||||||
certificateTemplateV2DAL,
|
certificateTemplateV2DAL,
|
||||||
apiEnrollmentConfigDAL,
|
apiEnrollmentConfigDAL,
|
||||||
estEnrollmentConfigDAL,
|
estEnrollmentConfigDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
}: TCertificateProfileServiceFactoryDep) => {
|
}: TCertificateProfileServiceFactoryDep) => {
|
||||||
const createProfile = async ({
|
const createProfile = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -140,21 +199,17 @@ export const certificateProfileServiceFactory = ({
|
|||||||
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
// Hash the passphrase
|
// Hash the passphrase
|
||||||
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphraseInput, appCfg.SALT_ROUNDS);
|
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS);
|
||||||
|
|
||||||
let encryptedCaChainBuffer: Buffer | null = null;
|
let encryptedCaChainBuffer: Buffer | null = null;
|
||||||
if (!data.estConfig.disableBootstrapCaValidation) {
|
if (!data.estConfig.disableBootstrapCaValidation && data.estConfig.caChain) {
|
||||||
try {
|
const { encryptedCaChain } = await validateAndEncryptPemCaChain(
|
||||||
if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") {
|
data.estConfig.caChain,
|
||||||
throw new BadRequestError({ message: "Invalid or missing CA chain data" });
|
projectId,
|
||||||
}
|
kmsService,
|
||||||
encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64");
|
projectDAL
|
||||||
if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) {
|
);
|
||||||
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
|
encryptedCaChainBuffer = encryptedCaChain;
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.create(
|
const estConfig = await estEnrollmentConfigDAL.create(
|
||||||
@@ -256,17 +311,31 @@ export const certificateProfileServiceFactory = ({
|
|||||||
|
|
||||||
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
||||||
if (estConfig && existingProfile.estConfigId) {
|
if (estConfig && existingProfile.estConfigId) {
|
||||||
await estEnrollmentConfigDAL.updateById(
|
const updateData: {
|
||||||
existingProfile.estConfigId,
|
disableBootstrapCaValidation: boolean;
|
||||||
{
|
hashedPassphrase?: string;
|
||||||
disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation,
|
encryptedCaChain?: Buffer;
|
||||||
...(estConfig.passphraseInput && {
|
} = {
|
||||||
hashedPassphrase: await crypto.hashing().createHash(estConfig.passphraseInput, getConfig().SALT_ROUNDS)
|
disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation ?? false
|
||||||
}),
|
};
|
||||||
...(estConfig.caChain && validateAndEncodeBase64CaChain(estConfig.caChain))
|
|
||||||
},
|
if (estConfig.passphrase) {
|
||||||
tx
|
updateData.hashedPassphrase = await crypto
|
||||||
);
|
.hashing()
|
||||||
|
.createHash(estConfig.passphrase, getConfig().SALT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estConfig.caChain) {
|
||||||
|
const { encryptedCaChain } = await validateAndEncryptPemCaChain(
|
||||||
|
estConfig.caChain,
|
||||||
|
existingProfile.projectId,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
|
);
|
||||||
|
updateData.encryptedCaChain = encryptedCaChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
await estEnrollmentConfigDAL.updateById(existingProfile.estConfigId, updateData, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiConfig && existingProfile.apiConfigId) {
|
if (apiConfig && existingProfile.apiConfigId) {
|
||||||
@@ -366,6 +435,25 @@ export const certificateProfileServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateProfiles
|
ProjectPermissionSub.CertificateProfiles
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (profile.estConfig && profile.estConfig.caChain) {
|
||||||
|
try {
|
||||||
|
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId!);
|
||||||
|
if (estConfig && estConfig.encryptedCaChain) {
|
||||||
|
const decryptedCaChain = await decryptCaChain(
|
||||||
|
estConfig.encryptedCaChain,
|
||||||
|
profile.projectId,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
|
);
|
||||||
|
profile.estConfig.caChain = decryptedCaChain;
|
||||||
|
} else {
|
||||||
|
profile.estConfig.caChain = "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
profile.estConfig.caChain = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...profile,
|
...profile,
|
||||||
enrollmentType: profile.enrollmentType as EnrollmentType
|
enrollmentType: profile.enrollmentType as EnrollmentType
|
||||||
@@ -435,7 +523,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
includeMetrics?: boolean;
|
includeMetrics?: boolean;
|
||||||
expiringDays?: number;
|
expiringDays?: number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
profiles: (TCertificateProfile & { metrics?: TCertificateProfileMetrics })[];
|
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}> => {
|
}> => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
@@ -467,24 +555,66 @@ export const certificateProfileServiceFactory = ({
|
|||||||
caId
|
caId
|
||||||
});
|
});
|
||||||
|
|
||||||
const convertedProfiles = profiles.map((profile) => {
|
const convertedProfiles = await Promise.all(
|
||||||
const converted = convertDalToService(profile);
|
profiles.map(async (profile) => {
|
||||||
if (includeMetrics) {
|
const profileWithConfigs = profile as TCertificateProfileWithConfigs;
|
||||||
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
|
|
||||||
return {
|
let decryptedEstConfig = profileWithConfigs.estConfig;
|
||||||
...converted,
|
if (decryptedEstConfig && profileWithConfigs.estConfigId) {
|
||||||
metrics: {
|
try {
|
||||||
profileId: converted.id,
|
const estConfig = await estEnrollmentConfigDAL.findById(profileWithConfigs.estConfigId);
|
||||||
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
|
if (estConfig && estConfig.encryptedCaChain) {
|
||||||
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
|
const decryptedCaChain = await decryptCaChain(
|
||||||
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
|
estConfig.encryptedCaChain,
|
||||||
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
|
projectId,
|
||||||
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
|
kmsService,
|
||||||
|
projectDAL
|
||||||
|
);
|
||||||
|
decryptedEstConfig = {
|
||||||
|
...decryptedEstConfig,
|
||||||
|
caChain: decryptedCaChain
|
||||||
|
};
|
||||||
|
} else if (decryptedEstConfig) {
|
||||||
|
decryptedEstConfig = {
|
||||||
|
...decryptedEstConfig,
|
||||||
|
caChain: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (decryptedEstConfig) {
|
||||||
|
decryptedEstConfig = {
|
||||||
|
...decryptedEstConfig,
|
||||||
|
caChain: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const converted = convertDalToService(profileWithConfigs);
|
||||||
|
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
|
||||||
|
...converted,
|
||||||
|
estConfig: decryptedEstConfig,
|
||||||
|
apiConfig: profileWithConfigs.apiConfig
|
||||||
};
|
};
|
||||||
}
|
|
||||||
return converted;
|
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 {
|
return {
|
||||||
profiles: convertedProfiles,
|
profiles: convertedProfiles,
|
||||||
@@ -673,9 +803,9 @@ export const certificateProfileServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
orgId: profile.projectId,
|
orgId: profile.projectId,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
caChain: profile.estConfig.encryptedCaChain,
|
caChain: profile.estConfig.caChain,
|
||||||
disableBootstrapCertValidation: profile.estConfig.disableBootstrapCaValidation,
|
disableBootstrapCertValidation: profile.estConfig.disableBootstrapCaValidation,
|
||||||
hashedPassphrase: profile.estConfig.hashedPassphrase
|
hashedPassphrase: ""
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enr
|
|||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphraseInput?: string;
|
passphrase?: string;
|
||||||
caChain?: string;
|
caChain?: string;
|
||||||
};
|
};
|
||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
@@ -46,8 +46,8 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
|||||||
estConfig?: {
|
estConfig?: {
|
||||||
id: string;
|
id: string;
|
||||||
disableBootstrapCaValidation: boolean;
|
disableBootstrapCaValidation: boolean;
|
||||||
hashedPassphrase: string;
|
passphrase: string;
|
||||||
encryptedCaChain: string;
|
caChain: string;
|
||||||
};
|
};
|
||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate;
|
|||||||
|
|
||||||
export interface TEstConfigData {
|
export interface TEstConfigData {
|
||||||
disableBootstrapCaValidation: boolean;
|
disableBootstrapCaValidation: boolean;
|
||||||
passphraseInput: string;
|
passphrase: string;
|
||||||
encryptedCaChain?: string;
|
caChain?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TApiConfigData {
|
export interface TApiConfigData {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const certificateProfileKeys = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
includeMetrics?: boolean;
|
includeMetrics?: boolean;
|
||||||
|
includeConfigs?: boolean;
|
||||||
expiringDays?: number;
|
expiringDays?: number;
|
||||||
}) => ["certificate-profiles", "list", params],
|
}) => ["certificate-profiles", "list", params],
|
||||||
getById: (profileId: string) => ["certificate-profiles", "get-by-id", profileId],
|
getById: (profileId: string) => ["certificate-profiles", "get-by-id", profileId],
|
||||||
@@ -50,6 +51,7 @@ export const useListCertificateProfiles = ({
|
|||||||
offset = 0,
|
offset = 0,
|
||||||
search,
|
search,
|
||||||
includeMetrics = false,
|
includeMetrics = false,
|
||||||
|
includeConfigs = false,
|
||||||
expiringDays = 7
|
expiringDays = 7
|
||||||
}: TListCertificateProfilesDTO) => {
|
}: TListCertificateProfilesDTO) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -59,6 +61,7 @@ export const useListCertificateProfiles = ({
|
|||||||
offset,
|
offset,
|
||||||
search,
|
search,
|
||||||
includeMetrics,
|
includeMetrics,
|
||||||
|
includeConfigs,
|
||||||
expiringDays
|
expiringDays
|
||||||
}),
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -72,6 +75,7 @@ export const useListCertificateProfiles = ({
|
|||||||
offset,
|
offset,
|
||||||
search,
|
search,
|
||||||
includeMetrics,
|
includeMetrics,
|
||||||
|
includeConfigs,
|
||||||
expiringDays
|
expiringDays
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
|
|||||||
certificateTemplate?: {
|
certificateTemplate?: {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
slug: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
id: string;
|
id: string;
|
||||||
disableBootstrapCaValidation: boolean;
|
disableBootstrapCaValidation: boolean;
|
||||||
hashedPassphrase: string;
|
passphrase: string;
|
||||||
encryptedCaChain: string;
|
caChain: string;
|
||||||
};
|
};
|
||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -48,7 +48,7 @@ export type TCreateCertificateProfileDTO = {
|
|||||||
enrollmentType: "api" | "est";
|
enrollmentType: "api" | "est";
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
passphraseInput: string;
|
passphrase: string;
|
||||||
caChain?: string;
|
caChain?: string;
|
||||||
};
|
};
|
||||||
apiConfig?: {
|
apiConfig?: {
|
||||||
@@ -82,6 +82,7 @@ export type TListCertificateProfilesDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
includeMetrics?: boolean;
|
includeMetrics?: boolean;
|
||||||
|
includeConfigs?: boolean;
|
||||||
expiringDays?: number;
|
expiringDays?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -10,7 +10,7 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import {
|
import {
|
||||||
TCertificateProfile,
|
TCertificateProfileWithDetails,
|
||||||
useDeleteCertificateProfile
|
useDeleteCertificateProfile
|
||||||
} from "@app/hooks/api/certificateProfiles";
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
|
|
||||||
@@ -23,7 +23,9 @@ export const CertificateProfilesTab = () => {
|
|||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [selectedProfile, setSelectedProfile] = useState<TCertificateProfile | null>(null);
|
const [selectedProfile, setSelectedProfile] = useState<TCertificateProfileWithDetails | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const deleteProfile = useDeleteCertificateProfile();
|
const deleteProfile = useDeleteCertificateProfile();
|
||||||
|
|
||||||
@@ -36,12 +38,12 @@ export const CertificateProfilesTab = () => {
|
|||||||
setIsCreateModalOpen(true);
|
setIsCreateModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProfile = (profile: TCertificateProfile) => {
|
const handleEditProfile = (profile: TCertificateProfileWithDetails) => {
|
||||||
setSelectedProfile(profile);
|
setSelectedProfile(profile);
|
||||||
setIsEditModalOpen(true);
|
setIsEditModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProfile = (profile: TCertificateProfile) => {
|
const handleDeleteProfile = (profile: TCertificateProfileWithDetails) => {
|
||||||
setSelectedProfile(profile);
|
setSelectedProfile(profile);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -111,7 +113,9 @@ export const CertificateProfilesTab = () => {
|
|||||||
title={`Delete Certificate Profile ${selectedProfile.slug}?`}
|
title={`Delete Certificate Profile ${selectedProfile.slug}?`}
|
||||||
onChange={(isOpen) => {
|
onChange={(isOpen) => {
|
||||||
setIsDeleteModalOpen(isOpen);
|
setIsDeleteModalOpen(isOpen);
|
||||||
if (!isOpen) setSelectedProfile(null);
|
if (!isOpen) {
|
||||||
|
setSelectedProfile(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
deleteKey={selectedProfile.slug}
|
deleteKey={selectedProfile.slug}
|
||||||
onDeleteApproved={handleDeleteConfirm}
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
|
|||||||
+37
-9
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -47,7 +48,7 @@ const createSchema = z
|
|||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().optional(),
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
passphraseInput: z.string().min(1, "EST passphrase is required"),
|
passphrase: z.string().min(1, "EST passphrase is required"),
|
||||||
caChain: z.string().min(1, "EST CA chain is required").optional()
|
caChain: z.string().min(1, "EST CA chain is required").optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
@@ -107,7 +108,7 @@ const editSchema = z
|
|||||||
estConfig: z
|
estConfig: z
|
||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().optional(),
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
passphraseInput: z.string().optional(),
|
passphrase: z.string().optional(),
|
||||||
caChain: z.string().optional()
|
caChain: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -174,8 +175,8 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
? {
|
? {
|
||||||
disableBootstrapCaValidation:
|
disableBootstrapCaValidation:
|
||||||
profile.estConfig?.disableBootstrapCaValidation || false,
|
profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
passphraseInput: "",
|
passphrase: profile.estConfig?.passphrase || "",
|
||||||
caChain: profile.estConfig?.encryptedCaChain || ""
|
caChain: profile.estConfig?.caChain || ""
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
apiConfig:
|
apiConfig:
|
||||||
@@ -203,6 +204,34 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
||||||
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEdit && profile) {
|
||||||
|
reset({
|
||||||
|
slug: profile.slug,
|
||||||
|
description: profile.description || "",
|
||||||
|
enrollmentType: profile.enrollmentType,
|
||||||
|
certificateAuthorityId: profile.caId,
|
||||||
|
certificateTemplateId: profile.certificateTemplateId,
|
||||||
|
estConfig:
|
||||||
|
profile.enrollmentType === "est"
|
||||||
|
? {
|
||||||
|
disableBootstrapCaValidation:
|
||||||
|
profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
|
passphrase: profile.estConfig?.passphrase || "",
|
||||||
|
caChain: profile.estConfig?.caChain || ""
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
apiConfig:
|
||||||
|
profile.enrollmentType === "api"
|
||||||
|
? {
|
||||||
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||||
|
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isEdit, profile, reset]);
|
||||||
|
|
||||||
const onFormSubmit = async (data: FormData) => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentProject?.id && !isEdit) return;
|
if (!currentProject?.id && !isEdit) return;
|
||||||
@@ -237,7 +266,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
|
|
||||||
if (data.enrollmentType === "est" && data.estConfig) {
|
if (data.enrollmentType === "est" && data.estConfig) {
|
||||||
createData.estConfig = {
|
createData.estConfig = {
|
||||||
passphraseInput: data.estConfig.passphraseInput,
|
passphrase: data.estConfig.passphrase,
|
||||||
caChain: data.estConfig.caChain || undefined,
|
caChain: data.estConfig.caChain || undefined,
|
||||||
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation
|
||||||
};
|
};
|
||||||
@@ -354,7 +383,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
if (watchedEnrollmentType === "est") {
|
if (watchedEnrollmentType === "est") {
|
||||||
setValue("estConfig", {
|
setValue("estConfig", {
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
passphraseInput: ""
|
passphrase: ""
|
||||||
});
|
});
|
||||||
setValue("apiConfig", undefined);
|
setValue("apiConfig", undefined);
|
||||||
} else {
|
} else {
|
||||||
@@ -398,7 +427,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
setValue("apiConfig", undefined);
|
setValue("apiConfig", undefined);
|
||||||
setValue("estConfig", {
|
setValue("estConfig", {
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
passphraseInput: ""
|
passphrase: ""
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setValue("estConfig", undefined);
|
setValue("estConfig", undefined);
|
||||||
@@ -450,7 +479,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="estConfig.passphraseInput"
|
name="estConfig.passphrase"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="EST Passphrase"
|
label="EST Passphrase"
|
||||||
@@ -543,7 +572,6 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 365) {
|
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 365) {
|
||||||
field.onChange(parsed);
|
field.onChange(parsed);
|
||||||
} else {
|
} else {
|
||||||
// Preserve the original field value instead of defaulting to 30
|
|
||||||
field.onChange(field.value || "");
|
field.onChange(field.value || "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -11,15 +11,15 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import {
|
import {
|
||||||
TCertificateProfile,
|
TCertificateProfileWithDetails,
|
||||||
useListCertificateProfiles
|
useListCertificateProfiles
|
||||||
} from "@app/hooks/api/certificateProfiles";
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
|
|
||||||
import { ProfileRow } from "./ProfileRow";
|
import { ProfileRow } from "./ProfileRow";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onEditProfile: (profile: TCertificateProfile) => void;
|
onEditProfile: (profile: TCertificateProfileWithDetails) => void;
|
||||||
onDeleteProfile: (profile: TCertificateProfile) => void;
|
onDeleteProfile: (profile: TCertificateProfileWithDetails) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||||
@@ -29,6 +29,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
projectId: currentProject?.id || "",
|
projectId: currentProject?.id || "",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
includeConfigs: true,
|
||||||
includeMetrics: true
|
includeMetrics: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user