mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 08:28:52 +00:00
Merge branch 'main' into ENG-4111
This commit is contained in:
@@ -371,6 +371,7 @@ export enum EventType {
|
||||
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
||||
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
|
||||
RENEW_CERTIFICATE = "renew-certificate",
|
||||
GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle",
|
||||
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
|
||||
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
|
||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||
@@ -2752,6 +2753,17 @@ interface OrderCertificateFromProfile {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateProfileLatestActiveBundle {
|
||||
type: EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE;
|
||||
metadata: {
|
||||
certificateProfileId: string;
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
profileName: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RenewCertificate {
|
||||
type: EventType.RENEW_CERTIFICATE;
|
||||
metadata: {
|
||||
@@ -4282,6 +4294,7 @@ export type Event =
|
||||
| DeleteCertificateProfile
|
||||
| GetCertificateProfile
|
||||
| ListCertificateProfiles
|
||||
| GetCertificateProfileLatestActiveBundle
|
||||
| IssueCertificateFromProfile
|
||||
| SignCertificateFromProfile
|
||||
| OrderCertificateFromProfile
|
||||
|
||||
@@ -1178,6 +1178,10 @@ export const registerRoutes = async (
|
||||
apiEnrollmentConfigDAL,
|
||||
estEnrollmentConfigDAL,
|
||||
acmeEnrollmentConfigDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL
|
||||
|
||||
@@ -498,6 +498,71 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id/certificates/latest-active-bundle",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||
description: "Get latest active certificate bundle for a profile",
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().nullable(),
|
||||
certificateChain: z.string().nullable(),
|
||||
privateKey: z.string().nullable(),
|
||||
serialNumber: z.string().nullable()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const response = await server.services.certificateProfile.getLatestActiveCertificateBundle({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.params.id
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
certificate: null,
|
||||
certificateChain: null,
|
||||
privateKey: null,
|
||||
serialNumber: null
|
||||
};
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: response.certObj.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE,
|
||||
metadata: {
|
||||
certificateProfileId: response.profile.id,
|
||||
certificateId: response.certObj.id,
|
||||
commonName: response.certObj.commonName,
|
||||
profileName: response.profile.slug,
|
||||
serialNumber: response.certObj.serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: response.certificate,
|
||||
certificateChain: response.certificateChain,
|
||||
privateKey: response.privateKey,
|
||||
serialNumber: response.certObj.serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id/acme/eab-secret/reveal",
|
||||
|
||||
@@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerUserRouter, { prefix: "/users" });
|
||||
await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" });
|
||||
await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
|
||||
await server.register(registerCertificatesRouter, { prefix: "/certificates" });
|
||||
await server.register(registerCertificatesRouter, { prefix: "/pki/certificates" });
|
||||
};
|
||||
|
||||
@@ -462,6 +462,24 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getLatestActiveCertificateForProfile = async (profileId: string, tx?: Knex) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
const certificate = await (tx || db)(TableName.Certificate)
|
||||
.where("profileId", profileId)
|
||||
.where("status", "active")
|
||||
.where("notAfter", ">", now)
|
||||
.whereNull("revokedAt")
|
||||
.orderBy("createdAt", "desc")
|
||||
.first();
|
||||
|
||||
return certificate;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Get latest active certificate by profile" });
|
||||
}
|
||||
};
|
||||
|
||||
const isProfileInUse = async (profileId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
|
||||
@@ -485,6 +503,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
countByProjectId,
|
||||
findByNameAndProjectId,
|
||||
getCertificatesByProfile,
|
||||
getLatestActiveCertificateForProfile,
|
||||
isProfileInUse
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,6 +9,10 @@ import type { TPermissionServiceFactory } from "@app/ee/services/permission/perm
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||
import type { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||
import type { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import type { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
||||
import type { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import type { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
|
||||
import type { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||
@@ -178,6 +182,54 @@ describe("CertificateProfileService", () => {
|
||||
transaction: vi.fn()
|
||||
} as unknown as Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
|
||||
const mockCertificateBodyDAL = {
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
deleteById: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
find: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
} as unknown as TCertificateBodyDALFactory;
|
||||
|
||||
const mockCertificateSecretDAL = {
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
deleteById: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
find: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
} as unknown as TCertificateSecretDALFactory;
|
||||
|
||||
const mockCertificateAuthorityDAL = {
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
deleteById: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
find: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
} as unknown as TCertificateAuthorityDALFactory;
|
||||
|
||||
const mockCertificateAuthorityCertDAL = {
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
deleteById: vi.fn(),
|
||||
transaction: vi.fn(),
|
||||
find: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
} as unknown as TCertificateAuthorityCertDALFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(ForbiddenError, "from").mockReturnValue({
|
||||
throwUnlessCan: vi.fn()
|
||||
@@ -195,6 +247,10 @@ describe("CertificateProfileService", () => {
|
||||
apiEnrollmentConfigDAL: mockApiEnrollmentConfigDAL,
|
||||
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL,
|
||||
acmeEnrollmentConfigDAL: mockAcmeEnrollmentConfigDAL,
|
||||
certificateBodyDAL: mockCertificateBodyDAL,
|
||||
certificateSecretDAL: mockCertificateSecretDAL,
|
||||
certificateAuthorityDAL: mockCertificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
|
||||
permissionService: mockPermissionService,
|
||||
kmsService: mockKmsService,
|
||||
projectDAL: mockProjectDAL
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as x509 from "@peculiar/x509";
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
@@ -15,6 +16,11 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal";
|
||||
import { getCertificateCredentials, isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
|
||||
import { TAcmeEnrollmentConfigDALFactory } from "../enrollment-config/acme-enrollment-config-dal";
|
||||
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
|
||||
@@ -142,6 +148,10 @@ type TCertificateProfileServiceFactoryDep = {
|
||||
apiEnrollmentConfigDAL: TApiEnrollmentConfigDALFactory;
|
||||
estEnrollmentConfigDAL: TEstEnrollmentConfigDALFactory;
|
||||
acmeEnrollmentConfigDAL: TAcmeEnrollmentConfigDALFactory;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
@@ -162,6 +172,8 @@ export const certificateProfileServiceFactory = ({
|
||||
apiEnrollmentConfigDAL,
|
||||
estEnrollmentConfigDAL,
|
||||
acmeEnrollmentConfigDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL
|
||||
@@ -729,6 +741,106 @@ export const certificateProfileServiceFactory = ({
|
||||
return certificates;
|
||||
};
|
||||
|
||||
const getLatestActiveCertificateBundle = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
profileId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
}) => {
|
||||
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
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
const cert = await certificateProfileDAL.getLatestActiveCertificateForProfile(profileId);
|
||||
|
||||
if (!cert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: cert.id });
|
||||
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: cert.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKeyId
|
||||
});
|
||||
const decryptedCert = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificate
|
||||
});
|
||||
|
||||
const certObj = new x509.X509Certificate(decryptedCert);
|
||||
const certificate = certObj.toString("pem");
|
||||
|
||||
const decryptedCertChain = await kmsDecryptor({
|
||||
cipherTextBlob: certBody.encryptedCertificateChain!
|
||||
});
|
||||
|
||||
const certificateChain = decryptedCertChain.toString();
|
||||
|
||||
let privateKey = null;
|
||||
try {
|
||||
const { certPrivateKey } = await getCertificateCredentials({
|
||||
certId: cert.id,
|
||||
projectId: cert.projectId,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
privateKey = certPrivateKey;
|
||||
} catch (error) {
|
||||
// Private key might not exist for ACME certificates or other external workflows
|
||||
// where the key is generated client-side
|
||||
if (error instanceof NotFoundError) {
|
||||
privateKey = null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
profile,
|
||||
certObj: cert
|
||||
};
|
||||
};
|
||||
|
||||
const getEstConfigurationByProfile = async (
|
||||
params:
|
||||
| {
|
||||
@@ -854,6 +966,7 @@ export const certificateProfileServiceFactory = ({
|
||||
listProfiles,
|
||||
deleteProfile,
|
||||
getProfileCertificates,
|
||||
getLatestActiveCertificateBundle,
|
||||
getEstConfigurationByProfile,
|
||||
revealAcmeEabSecret
|
||||
};
|
||||
|
||||
@@ -675,7 +675,7 @@ export const certificateV3ServiceFactory = ({
|
||||
status: CertificateOrderStatus.VALID
|
||||
})),
|
||||
authorizations: [],
|
||||
finalize: `/api/v3/certificates/orders/${orderId}/completed`,
|
||||
finalize: `/api/v3/pki/certificates/orders/${orderId}/completed`,
|
||||
certificate: certificateResult.certificate,
|
||||
projectId: certificateResult.projectId,
|
||||
profileName: certificateResult.profileName
|
||||
|
||||
Reference in New Issue
Block a user