misc: addressed comments

This commit is contained in:
Sheen Capadngan
2025-05-17 03:42:49 +08:00
parent 60ea4bb579
commit edefa7698c
25 changed files with 744 additions and 303 deletions
@@ -225,6 +225,7 @@ export enum EventType {
REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group", REMOVE_HOST_FROM_SSH_HOST_GROUP = "remove-host-from-ssh-host-group",
CREATE_CA = "create-certificate-authority", CREATE_CA = "create-certificate-authority",
GET_CA = "get-certificate-authority", GET_CA = "get-certificate-authority",
GET_CAS = "get-certificate-authorities",
UPDATE_CA = "update-certificate-authority", UPDATE_CA = "update-certificate-authority",
DELETE_CA = "delete-certificate-authority", DELETE_CA = "delete-certificate-authority",
RENEW_CA = "renew-certificate-authority", RENEW_CA = "renew-certificate-authority",
@@ -1718,7 +1719,7 @@ interface CreateCa {
type: EventType.CREATE_CA; type: EventType.CREATE_CA;
metadata: { metadata: {
caId: string; caId: string;
dn: string; dn?: string;
}; };
} }
@@ -1726,7 +1727,14 @@ interface GetCa {
type: EventType.GET_CA; type: EventType.GET_CA;
metadata: { metadata: {
caId: string; caId: string;
dn: string; dn?: string;
};
}
interface GetCAs {
type: EventType.GET_CAS;
metadata: {
caIds: string[];
}; };
} }
@@ -1734,7 +1742,7 @@ interface UpdateCa {
type: EventType.UPDATE_CA; type: EventType.UPDATE_CA;
metadata: { metadata: {
caId: string; caId: string;
dn: string; dn?: string;
status: CaStatus; status: CaStatus;
}; };
} }
@@ -1743,7 +1751,7 @@ interface DeleteCa {
type: EventType.DELETE_CA; type: EventType.DELETE_CA;
metadata: { metadata: {
caId: string; caId: string;
dn: string; dn?: string;
}; };
} }
@@ -2031,7 +2039,7 @@ interface IssuePkiSubscriberCert {
metadata: { metadata: {
subscriberId: string; subscriberId: string;
name: string; name: string;
serialNumber: string; serialNumber?: string;
}; };
} }
@@ -2987,6 +2995,7 @@ export type Event =
| IssueSshHostHostCert | IssueSshHostHostCert
| CreateCa | CreateCa
| GetCa | GetCa
| GetCAs
| UpdateCa | UpdateCa
| DeleteCa | DeleteCa
| RenewCa | RenewCa
@@ -39,7 +39,7 @@ export const certificateAuthorityCrlServiceFactory = ({
if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` }); if (!caCrl) throw new NotFoundError({ message: `CRL with ID '${crlId}' not found` });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caCrl.caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caCrl.caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caCrl.caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caCrl.caId}' not found` });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -67,7 +67,7 @@ export const certificateAuthorityCrlServiceFactory = ({
*/ */
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => { const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -228,9 +228,9 @@ export const certificateEstServiceFactory = ({
} }
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certTemplate.caId);
if (!ca?.internalCa) { if (!ca?.internalCa?.id) {
throw new NotFoundError({ throw new NotFoundError({
message: `Certificate Authority with ID '${certTemplate.caId}' not found` message: `Internal Certificate Authority with ID '${certTemplate.caId}' not found`
}); });
} }
@@ -5,7 +5,7 @@ import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } 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 { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { import {
TCertificateAuthority, TCertificateAuthority,
TCertificateAuthorityInput TCertificateAuthorityInput
@@ -26,11 +26,13 @@ export const registerCertificateAuthorityEndpoints = <
createSchema: z.ZodType<{ createSchema: z.ZodType<{
name: string; name: string;
projectId: string; projectId: string;
status: CaStatus;
configuration: I["configuration"]; configuration: I["configuration"];
disableDirectIssuance: boolean; disableDirectIssuance: boolean;
}>; }>;
updateSchema: z.ZodType<{ updateSchema: z.ZodType<{
name?: string; name?: string;
status?: CaStatus;
configuration?: I["configuration"]; configuration?: I["configuration"];
disableDirectIssuance?: boolean; disableDirectIssuance?: boolean;
}>; }>;
@@ -63,18 +65,16 @@ export const registerCertificateAuthorityEndpoints = <
req.permission req.permission
)) as T[]; )) as T[];
// await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo, ...req.auditLogInfo,
// projectId, projectId,
// event: { event: {
// type: EventType.GET_SECRET_SYNCS, type: EventType.GET_CAS,
// metadata: { metadata: {
// destination, caIds: certificateAuthorities.map((ca) => ca.id)
// count: secretSyncs.length, }
// syncIds: secretSyncs.map((connection) => connection.id) }
// } });
// }
// });
return { certificateAuthorities }; return { certificateAuthorities };
} }
@@ -105,17 +105,16 @@ export const registerCertificateAuthorityEndpoints = <
req.permission req.permission
)) as T; )) as T;
// await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo, ...req.auditLogInfo,
// projectId: secretSync.projectId, projectId: certificateAuthority.projectId,
// event: { event: {
// type: EventType.GET_SECRET_SYNC, type: EventType.GET_CA,
// metadata: { metadata: {
// syncId, caId: certificateAuthority.id
// destination }
// } }
// } });
// });
return { certificateAuthority }; return { certificateAuthority };
} }
@@ -142,18 +141,16 @@ export const registerCertificateAuthorityEndpoints = <
req.permission req.permission
)) as T; )) as T;
// await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo, ...req.auditLogInfo,
// projectId: secretSync.projectId, projectId: certificateAuthority.projectId,
// event: { event: {
// type: EventType.CREATE_SECRET_SYNC, type: EventType.CREATE_CA,
// metadata: { metadata: {
// syncId: secretSync.id, caId: certificateAuthority.id
// destination, }
// ...req.body }
// } });
// }
// });
return { certificateAuthority }; return { certificateAuthority };
} }
@@ -181,22 +178,25 @@ export const registerCertificateAuthorityEndpoints = <
const { certificateAuthorityId } = req.params; const { certificateAuthorityId } = req.params;
const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority( const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority(
{ ...req.body, id: certificateAuthorityId, type: caType }, {
...req.body,
id: certificateAuthorityId,
type: caType
},
req.permission req.permission
)) as T; )) as T;
// await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo, ...req.auditLogInfo,
// projectId: certificateAuthority.projectId, projectId: certificateAuthority.projectId,
// event: { event: {
// type: EventType.UPDATE_SECRET_SYNC, type: EventType.UPDATE_CA,
// metadata: { metadata: {
// syncId, caId: certificateAuthority.id,
// destination, status: certificateAuthority.status
// ...req.body }
// } }
// } });
// });
return { certificateAuthority }; return { certificateAuthority };
} }
@@ -227,18 +227,16 @@ export const registerCertificateAuthorityEndpoints = <
req.permission req.permission
)) as T; )) as T;
// await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo, ...req.auditLogInfo,
// orgId: req.permission.orgId, projectId: certificateAuthority.projectId,
// event: { event: {
// type: EventType.DELETE_SECRET_SYNC, type: EventType.DELETE_CA,
// metadata: { metadata: {
// destination, caId: certificateAuthority.id
// syncId, }
// removeSecrets }
// } });
// }
// });
return { certificateAuthority }; return { certificateAuthority };
} }
@@ -311,28 +311,27 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
// await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo, ...req.auditLogInfo,
// projectId: subscriber.projectId, projectId: subscriber.projectId,
// event: { event: {
// type: EventType.ISSUE_PKI_SUBSCRIBER_CERT, type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
// metadata: { metadata: {
// subscriberId: subscriber.id, subscriberId: subscriber.id,
// name: subscriber.name, name: subscriber.name
// serialNumber }
// } }
// } });
// });
// await server.services.telemetry.sendPostHogEvents({ await server.services.telemetry.sendPostHogEvents({
// event: PostHogEventTypes.IssueCert, event: PostHogEventTypes.IssueCert,
// distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
// properties: { properties: {
// subscriberId: subscriber.id, subscriberId: subscriber.id,
// commonName: subscriber.commonName, commonName: subscriber.commonName,
// ...req.auditLogInfo ...req.auditLogInfo
// } }
// }); });
return { return {
message: "Successfully placed order for certificate" message: "Successfully placed order for certificate"
@@ -29,7 +29,6 @@ import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaStatus, CaType } from "../certificate-authority-enums"; import { CaStatus, CaType } from "../certificate-authority-enums";
import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns";
import { TCertificateAuthority } from "../certificate-authority-types";
import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal"; import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal";
import { AcmeDnsProvider } from "./acme-certificate-authority-enums"; import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas"; import { AcmeCertificateAuthorityCredentialsSchema } from "./acme-certificate-authority-schemas";
@@ -62,12 +61,13 @@ type DBConfigurationColumn = {
dnsProvider: string; dnsProvider: string;
directoryUrl: string; directoryUrl: string;
accountEmail: string; accountEmail: string;
hostedZoneId: string;
}; };
export const castDbEntryToAcmeCertificateAuthority = ( export const castDbEntryToAcmeCertificateAuthority = (
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>> ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
): TAcmeCertificateAuthority & { credentials: unknown } => { ): TAcmeCertificateAuthority & { credentials: unknown } => {
if (!ca.externalCa) { if (!ca.externalCa?.id) {
throw new BadRequestError({ message: "Malformed ACME certificate authority" }); throw new BadRequestError({ message: "Malformed ACME certificate authority" });
} }
@@ -82,7 +82,10 @@ export const castDbEntryToAcmeCertificateAuthority = (
credentials: ca.externalCa.credentials, credentials: ca.externalCa.credentials,
configuration: { configuration: {
dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string, dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string,
dnsProvider: dbConfigurationCol.dnsProvider as AcmeDnsProvider, dnsProviderConfig: {
provider: dbConfigurationCol.dnsProvider as AcmeDnsProvider,
hostedZoneId: dbConfigurationCol.hostedZoneId
},
directoryUrl: dbConfigurationCol.directoryUrl, directoryUrl: dbConfigurationCol.directoryUrl,
accountEmail: dbConfigurationCol.accountEmail accountEmail: dbConfigurationCol.accountEmail
}, },
@@ -90,7 +93,12 @@ export const castDbEntryToAcmeCertificateAuthority = (
}; };
}; };
export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, domain: string, value: string) => { export const route53InsertTxtRecord = async (
connection: TAwsConnectionConfig,
hostedZoneId: string,
domain: string,
value: string
) => {
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
const route53Client = new Route53Client({ const route53Client = new Route53Client({
credentials: config.credentials!, credentials: config.credentials!,
@@ -98,7 +106,7 @@ export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, d
}); });
const command = new ChangeResourceRecordSetsCommand({ const command = new ChangeResourceRecordSetsCommand({
HostedZoneId: "Z040441124N1GOOMCQYX1", // SHEEN TODO: Get this from user input HostedZoneId: hostedZoneId,
ChangeBatch: { ChangeBatch: {
Comment: "Set ACME challenge TXT record", Comment: "Set ACME challenge TXT record",
Changes: [ Changes: [
@@ -118,7 +126,12 @@ export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, d
await route53Client.send(command); await route53Client.send(command);
}; };
export const route53DeleteTxtRecord = async (connection: TAwsConnectionConfig, domain: string, value: string) => { export const route53DeleteTxtRecord = async (
connection: TAwsConnectionConfig,
hostedZoneId: string,
domain: string,
value: string
) => {
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
const route53Client = new Route53Client({ const route53Client = new Route53Client({
credentials: config.credentials!, credentials: config.credentials!,
@@ -126,7 +139,7 @@ export const route53DeleteTxtRecord = async (connection: TAwsConnectionConfig, d
}); });
const command = new ChangeResourceRecordSetsCommand({ const command = new ChangeResourceRecordSetsCommand({
HostedZoneId: "Z040441124N1GOOMCQYX1", // SHEEN TODO: same here HostedZoneId: hostedZoneId,
ChangeBatch: { ChangeBatch: {
Comment: "Delete ACME challenge TXT record", Comment: "Delete ACME challenge TXT record",
Changes: [ Changes: [
@@ -173,14 +186,14 @@ export const AcmeCertificateAuthorityFns = ({
disableDirectIssuance: boolean; disableDirectIssuance: boolean;
actor: OrgServiceActor; actor: OrgServiceActor;
}) => { }) => {
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProvider } = configuration; const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId); const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!appConnection) { if (!appConnection) {
throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` }); throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` });
} }
if (dnsProvider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) { if (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
throw new BadRequestError({ throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection` message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
}); });
@@ -207,7 +220,8 @@ export const AcmeCertificateAuthorityFns = ({
configuration: { configuration: {
directoryUrl, directoryUrl,
accountEmail, accountEmail,
dnsProvider dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
}, },
status status
}, },
@@ -217,19 +231,11 @@ export const AcmeCertificateAuthorityFns = ({
return certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); return certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx);
}); });
if (!caEntity.externalCa) { if (!caEntity.externalCa?.id) {
throw new BadRequestError({ message: "Failed to create external certificate authority" }); throw new BadRequestError({ message: "Failed to create external certificate authority" });
} }
return { return castDbEntryToAcmeCertificateAuthority(caEntity);
id: caEntity.id,
type: CaType.ACME,
disableDirectIssuance: caEntity.disableDirectIssuance,
name: caEntity.externalCa.name,
projectId,
status,
configuration: caEntity.externalCa.configuration
} as TCertificateAuthority;
}; };
const updateCertificateAuthority = async ({ const updateCertificateAuthority = async ({
@@ -237,24 +243,26 @@ export const AcmeCertificateAuthorityFns = ({
status, status,
configuration, configuration,
disableDirectIssuance, disableDirectIssuance,
actor actor,
name
}: { }: {
id: string; id: string;
status?: CaStatus; status?: CaStatus;
configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"]; configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"];
disableDirectIssuance?: boolean; disableDirectIssuance?: boolean;
actor: OrgServiceActor; actor: OrgServiceActor;
name?: string;
}) => { }) => {
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
if (configuration) { if (configuration) {
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProvider } = configuration; const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId); const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
if (!appConnection) { if (!appConnection) {
throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` }); throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` });
} }
if (dnsProvider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) { if (dnsProviderConfig.provider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) {
throw new BadRequestError({ throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection` message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection`
}); });
@@ -273,24 +281,29 @@ export const AcmeCertificateAuthorityFns = ({
type: CaType.ACME type: CaType.ACME
}, },
{ {
configuration: { directoryUrl, accountEmail, dnsProvider, dnsAppConnectionId } dnsAppConnectionId,
configuration: {
directoryUrl,
accountEmail,
dnsProvider: dnsProviderConfig.provider,
hostedZoneId: dnsProviderConfig.hostedZoneId
}
}, },
tx tx
); );
} }
if (status) { await externalCertificateAuthorityDAL.update(
await externalCertificateAuthorityDAL.update( {
{ certificateAuthorityId: id,
certificateAuthorityId: id, type: CaType.ACME
type: CaType.ACME },
}, {
{ name,
status status
}, },
tx tx
); );
}
if (disableDirectIssuance !== undefined) { if (disableDirectIssuance !== undefined) {
await certificateAuthorityDAL.updateById( await certificateAuthorityDAL.updateById(
@@ -305,19 +318,11 @@ export const AcmeCertificateAuthorityFns = ({
return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx); return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx);
}); });
if (!updatedCa.externalCa) { if (!updatedCa.externalCa?.id) {
throw new BadRequestError({ message: "Failed to update external certificate authority" }); throw new BadRequestError({ message: "Failed to update external certificate authority" });
} }
return { return castDbEntryToAcmeCertificateAuthority(updatedCa);
id: updatedCa.id,
type: CaType.ACME,
disableDirectIssuance: updatedCa.disableDirectIssuance,
name: updatedCa.externalCa.name,
projectId: updatedCa.projectId,
status: updatedCa.externalCa.status,
configuration: updatedCa.externalCa.configuration
};
}; };
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
@@ -329,7 +334,7 @@ export const AcmeCertificateAuthorityFns = ({
return cas.map(castDbEntryToAcmeCertificateAuthority); return cas.map(castDbEntryToAcmeCertificateAuthority);
}; };
const orderCertificate = async (subscriberId: string) => { const orderSubscriberCertificate = async (subscriberId: string) => {
const subscriber = await pkiSubscriberDAL.findById(subscriberId); const subscriber = await pkiSubscriberDAL.findById(subscriberId);
if (!subscriber.caId) { if (!subscriber.caId) {
throw new BadRequestError({ message: "Subscriber does not have a CA" }); throw new BadRequestError({ message: "Subscriber does not have a CA" });
@@ -341,6 +346,9 @@ export const AcmeCertificateAuthorityFns = ({
} }
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca); const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -421,16 +429,26 @@ export const AcmeCertificateAuthorityFns = ({
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com" const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted const recordValue = `"${keyAuthorization}"`; // must be double quoted
if (acmeCa.configuration.dnsProvider === AcmeDnsProvider.Route53) { if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
await route53InsertTxtRecord(connection as TAwsConnection, recordName, recordValue); await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
} }
}, },
challengeRemoveFn: async (authz, challenge, keyAuthorization) => { challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com" const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted const recordValue = `"${keyAuthorization}"`; // must be double quoted
if (acmeCa.configuration.dnsProvider === AcmeDnsProvider.Route53) { if (acmeCa.configuration.dnsProviderConfig.provider === AcmeDnsProvider.Route53) {
await route53DeleteTxtRecord(connection as TAwsConnection, recordName, recordValue); await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
} }
} }
}); });
@@ -466,7 +484,7 @@ export const AcmeCertificateAuthorityFns = ({
notAfter: certObj.notAfter, notAfter: certObj.notAfter,
keyUsages: subscriber.keyUsages as CertKeyUsage[], keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[], extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
caCertId: "s" // SHEEN TODO: merge Andrey's PR and then remove this projectId: ca.projectId
}, },
tx tx
); );
@@ -494,6 +512,6 @@ export const AcmeCertificateAuthorityFns = ({
createCertificateAuthority, createCertificateAuthority,
updateCertificateAuthority, updateCertificateAuthority,
listCertificateAuthorities, listCertificateAuthorities,
orderCertificate orderSubscriberCertificate
}; };
}; };
@@ -10,9 +10,13 @@ import { AcmeDnsProvider } from "./acme-certificate-authority-enums";
export const AcmeCertificateAuthorityConfigurationSchema = z.object({ export const AcmeCertificateAuthorityConfigurationSchema = z.object({
dnsAppConnectionId: z.string().trim(), dnsAppConnectionId: z.string().trim(),
dnsProvider: z.nativeEnum(AcmeDnsProvider), // soon, differentiate via the provider property
directoryUrl: z.string().trim(), dnsProviderConfig: z.object({
accountEmail: z.string().trim() provider: z.nativeEnum(AcmeDnsProvider),
hostedZoneId: z.string().trim().min(1)
}),
directoryUrl: z.string().trim().min(1),
accountEmail: z.string().trim().min(1)
}); });
export const AcmeCertificateAuthorityCredentialsSchema = z.object({ export const AcmeCertificateAuthorityCredentialsSchema = z.object({
@@ -114,7 +114,7 @@ export const getCaCredentials = async ({
kmsService kmsService
}: TGetCaCredentialsDTO) => { }: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId });
if (!caSecret) throw new NotFoundError({ message: `CA secret for CA with ID '${caId}' not found` }); if (!caSecret) throw new NotFoundError({ message: `CA secret for CA with ID '${caId}' not found` });
@@ -257,7 +257,7 @@ export const rebuildCaCrl = async ({
kmsService kmsService
}: TRebuildCaCrlDTO) => { }: TRebuildCaCrlDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id }); const caSecret = await certificateAuthoritySecretDAL.findOne({ caId: ca.id });
@@ -4,7 +4,7 @@ import crypto from "crypto";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
@@ -114,6 +114,11 @@ export const certificateAuthorityQueueFactory = ({
}; };
const orderCertificateForSubscriber = async ({ subscriberId, caType }: TOrderCertificateForSubscriberDTO) => { const orderCertificateForSubscriber = async ({ subscriberId, caType }: TOrderCertificateForSubscriberDTO) => {
const entry = await keyStore.getItem(KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId));
if (entry) {
throw new BadRequestError({ message: `Certificate order already in progress for subscriber ${subscriberId}` });
}
await queueService.queue( await queueService.queue(
QueueName.CaLifecycle, QueueName.CaLifecycle,
QueueJobs.CaOrderCertificateForSubscriber, QueueJobs.CaOrderCertificateForSubscriber,
@@ -137,8 +142,6 @@ export const certificateAuthorityQueueFactory = ({
try { try {
lock = await keyStore.acquireLock( lock = await keyStore.acquireLock(
[KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId)], [KeyStorePrefixes.CaOrderCertificateForSubscriberLock(subscriberId)],
// scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked,
// but should always release below...
5 * 60 * 1000 5 * 60 * 1000
); );
} catch (e) { } catch (e) {
@@ -148,7 +151,7 @@ export const certificateAuthorityQueueFactory = ({
try { try {
if (caType === CaType.ACME) { if (caType === CaType.ACME) {
await acmeFns.orderCertificate(subscriberId); await acmeFns.orderSubscriberCertificate(subscriberId);
} }
} catch (e) { } catch (e) {
logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`); logger.error(e, `CaOrderCertificate Failed [subscriberId=${subscriberId}] [job=${job.name}]`);
@@ -14,7 +14,10 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; import {
AcmeCertificateAuthorityFns,
castDbEntryToAcmeCertificateAuthority
} from "./acme/acme-certificate-authority-fns";
import { import {
TCreateAcmeCertificateAuthorityDTO, TCreateAcmeCertificateAuthorityDTO,
TUpdateAcmeCertificateAuthorityDTO TUpdateAcmeCertificateAuthorityDTO
@@ -153,6 +156,8 @@ export const certificateAuthorityServiceFactory = ({
actor actor
}); });
} }
throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
const findCertificateAuthorityById = async ( const findCertificateAuthorityById = async (
@@ -181,9 +186,9 @@ export const certificateAuthorityServiceFactory = ({
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa) { if (!certificateAuthority.internalCa?.id) {
throw new NotFoundError({ throw new NotFoundError({
message: `Could not find internal certificate authority with ID "${certificateAuthorityId}"` message: `Internal certificate authority with ID "${certificateAuthorityId}" not found`
}); });
} }
@@ -204,14 +209,11 @@ export const certificateAuthorityServiceFactory = ({
}); });
} }
return { if (type === CaType.ACME) {
id: certificateAuthority.id, return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
type, }
disableDirectIssuance: certificateAuthority.disableDirectIssuance,
name: certificateAuthority.externalCa.name, throw new BadRequestError({ message: "Invalid certificate authority type" });
projectId: certificateAuthority.projectId,
configuration: certificateAuthority.externalCa.configuration
} as TCertificateAuthority;
}; };
const listCertificateAuthoritiesByProjectId = async ( const listCertificateAuthoritiesByProjectId = async (
@@ -264,10 +266,12 @@ export const certificateAuthorityServiceFactory = ({
if (type === CaType.ACME) { if (type === CaType.ACME) {
return acmeFns.listCertificateAuthorities({ projectId: finalProjectId }); return acmeFns.listCertificateAuthorities({ projectId: finalProjectId });
} }
throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
const updateCertificateAuthority = async ( const updateCertificateAuthority = async (
{ id, type, configuration, disableDirectIssuance, status }: TUpdateCertificateAuthorityDTO, { id, type, configuration, disableDirectIssuance, status, name }: TUpdateCertificateAuthorityDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id);
@@ -292,9 +296,9 @@ export const certificateAuthorityServiceFactory = ({
); );
if (type === CaType.INTERNAL) { if (type === CaType.INTERNAL) {
if (!certificateAuthority.internalCa) { if (!certificateAuthority.internalCa?.id) {
throw new NotFoundError({ throw new NotFoundError({
message: `Could not find internal certificate authority with ID "${id}"` message: `Internal certificate authority with ID "${id}" not found`
}); });
} }
@@ -328,7 +332,8 @@ export const certificateAuthorityServiceFactory = ({
configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"], configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"],
disableDirectIssuance, disableDirectIssuance,
actor, actor,
status status,
name
}); });
} }
@@ -357,15 +362,15 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
if (!certificateAuthority.internalCa && type === CaType.INTERNAL) { if (!certificateAuthority.internalCa?.id && type === CaType.INTERNAL) {
throw new BadRequestError({ throw new BadRequestError({
message: "Certificate authority cannot be deleted due to mismatching type" message: "Internal certificate authority cannot be deleted"
}); });
} }
if (certificateAuthority.externalCa && certificateAuthority.externalCa.type !== type) { if (certificateAuthority.externalCa?.id && certificateAuthority.externalCa.type !== type) {
throw new BadRequestError({ throw new BadRequestError({
message: "Certificate authority cannot be deleted due to mismatching type" message: "External certificate authority cannot be deleted"
}); });
} }
@@ -383,15 +388,11 @@ export const certificateAuthorityServiceFactory = ({
} as TCertificateAuthority; } as TCertificateAuthority;
} }
return { if (type === CaType.ACME) {
id: certificateAuthority.id, return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
type, }
disableDirectIssuance: certificateAuthority.disableDirectIssuance,
name: certificateAuthority.externalCa?.name, throw new BadRequestError({ message: "Invalid certificate authority type" });
projectId: certificateAuthority.projectId,
configuration: certificateAuthority.externalCa?.configuration,
status: certificateAuthority.externalCa?.status
} as TCertificateAuthority;
}; };
return { return {
@@ -9,9 +9,7 @@ export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertifi
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput; export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "type" | "id"> & { export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
type: CaType;
};
export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & { export type TUpdateCertificateAuthorityDTO = Partial<Omit<TCreateCertificateAuthorityDTO, "projectId">> & {
type: CaType; type: CaType;
@@ -225,7 +225,8 @@ export const InternalCertificateAuthorityFns = ({
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
keyUsages: selectedKeyUsages, keyUsages: selectedKeyUsages,
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
projectId: ca.projectId
}, },
tx tx
); );
@@ -1209,8 +1209,8 @@ export const internalCertificateAuthorityServiceFactory = ({
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId);
} }
if (!ca?.internalCa) { if (!ca?.internalCa?.id) {
throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
} }
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@@ -1475,7 +1475,8 @@ export const internalCertificateAuthorityServiceFactory = ({
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
keyUsages: selectedKeyUsages, keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId
}, },
tx tx
); );
@@ -1560,8 +1561,8 @@ export const internalCertificateAuthorityServiceFactory = ({
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId); ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId);
} }
if (!ca?.internalCa) { if (!ca?.internalCa?.id) {
throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
} }
if (!dto.isInternal) { if (!dto.isInternal) {
@@ -1854,6 +1855,20 @@ export const internalCertificateAuthorityServiceFactory = ({
plainText: Buffer.from(new Uint8Array(leafCert.rawData)) plainText: Buffer.from(new Uint8Array(leafCert.rawData))
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
await certificateDAL.transaction(async (tx) => { await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create( const cert = await certificateDAL.create(
{ {
@@ -1868,7 +1883,8 @@ export const internalCertificateAuthorityServiceFactory = ({
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate,
keyUsages: selectedKeyUsages, keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages extendedKeyUsages: selectedExtendedKeyUsages,
projectId: ca.projectId
}, },
tx tx
); );
@@ -1876,7 +1892,8 @@ export const internalCertificateAuthorityServiceFactory = ({
await certificateBodyDAL.create( await certificateBodyDAL.create(
{ {
certId: cert.id, certId: cert.id,
encryptedCertificate encryptedCertificate,
encryptedCertificateChain
}, },
tx tx
); );
@@ -1894,17 +1911,9 @@ export const internalCertificateAuthorityServiceFactory = ({
return cert; return cert;
}); });
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return { return {
certificate: leafCert, certificate: leafCert,
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), certificateChain: certificateChainPem,
issuingCaCertificate, issuingCaCertificate,
serialNumber, serialNumber,
ca: expandInternalCa(ca), ca: expandInternalCa(ca),
@@ -1923,7 +1932,7 @@ export const internalCertificateAuthorityServiceFactory = ({
actorOrgId actorOrgId
}: TGetCaCertificateTemplatesDTO) => { }: TGetCaCertificateTemplatesDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -1,6 +1,7 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import RE2 from "re2";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -52,8 +53,11 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[])
.join("\n") .join("\n")
.trim(); .trim();
export const splitPemChain = (pemText: string) => export const splitPemChain = (pemText: string) => {
pemText.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) || []; const re2Pattern = new RE2("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----", "g");
return re2Pattern.match(pemText) || [];
};
/** /**
* Return the public and private key of certificate * Return the public and private key of certificate
@@ -22,7 +22,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns"; import { expandInternalCa, getCaCertChain, rebuildCaCrl } from "../certificate-authority/certificate-authority-fns";
import { buildCertificateChain, getCertificateCredentials, revocationReasonToCrlCode } from "./certificate-fns"; import { getCertificateCredentials, revocationReasonToCrlCode, splitPemChain } from "./certificate-fns";
import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
import { import {
CertExtendedKeyUsage, CertExtendedKeyUsage,
@@ -39,9 +39,9 @@ import {
} from "./certificate-types"; } from "./certificate-types";
type TCertificateServiceFactoryDep = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find" | "transaction" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne">; certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "update">;
@@ -77,7 +77,6 @@ export const certificateServiceFactory = ({
*/ */
const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => { const getCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -94,8 +93,7 @@ export const certificateServiceFactory = ({
); );
return { return {
cert, cert
ca: expandInternalCa(ca)
}; };
}; };
@@ -110,7 +108,6 @@ export const certificateServiceFactory = ({
actorOrgId actorOrgId
}: TGetCertPrivateKeyDTO) => { }: TGetCertPrivateKeyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -135,7 +132,6 @@ export const certificateServiceFactory = ({
}); });
return { return {
ca: expandInternalCa(ca),
cert, cert,
certPrivateKey certPrivateKey
}; };
@@ -146,7 +142,6 @@ export const certificateServiceFactory = ({
*/ */
const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => { const deleteCert = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -165,8 +160,7 @@ export const certificateServiceFactory = ({
const deletedCert = await certificateDAL.deleteById(cert.id); const deletedCert = await certificateDAL.deleteById(cert.id);
return { return {
deletedCert, deletedCert
ca: expandInternalCa(ca)
}; };
}; };
@@ -184,8 +178,21 @@ export const certificateServiceFactory = ({
actorOrgId actorOrgId
}: TRevokeCertDTO) => { }: TRevokeCertDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = await certificateDAL.findOne({ serialNumber });
if (!cert.caId) {
throw new BadRequestError({
message: "Cannot revoke imported certificates"
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
if (ca.externalCa?.id) {
throw new BadRequestError({
message: "Cannot revoke external certificates"
});
}
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -234,7 +241,6 @@ export const certificateServiceFactory = ({
*/ */
const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => { const getCertBody = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBodyDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -292,8 +298,234 @@ export const certificateServiceFactory = ({
certificate: certObj.toString("pem"), certificate: certObj.toString("pem"),
certificateChain, certificateChain,
serialNumber: certObj.serialNumber, serialNumber: certObj.serialNumber,
cert, cert
ca: expandInternalCa(ca) };
};
/**
* Import certificate
*/
const importCert = async ({
projectSlug,
pkiCollectionId,
actorId,
actorAuthMethod,
actor,
actorOrgId,
friendlyName,
certificatePem,
chainPem,
privateKeyPem
}: TImportCertDTO) => {
const collectionId = pkiCollectionId;
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
let projectId = project.id;
const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId(
projectId,
ProjectType.CertificateManager
);
if (certManagerProjectFromSplit) {
projectId = certManagerProjectFromSplit.id;
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
// Check PKI collection
if (collectionId) {
const pkiCollection = await pkiCollectionDAL.findById(collectionId);
if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" });
if (pkiCollection.projectId !== projectId) throw new BadRequestError({ message: "Invalid PKI collection" });
}
const leafCert = new x509.X509Certificate(certificatePem);
// Verify the certificate chain
const chainCerts = splitPemChain(chainPem).map((pem) => new x509.X509Certificate(pem));
// Remove leaf cert from the chain if it's present
if (chainCerts[0].equal(leafCert)) {
chainCerts.splice(0, 1);
}
if (chainCerts.length === 0) {
throw new BadRequestError({
message: "Certificate chain must contain at least one issuer certificate"
});
}
// Verify leaf certificate is signed by the first certificate in the chain
const isLeafVerified = await leafCert.verify({ publicKey: chainCerts[0].publicKey }).catch(() => false);
if (!isLeafVerified) {
throw new BadRequestError({ message: "Leaf certificate verification against chain failed" });
}
// Verify the entire chain of trust
const verificationPromises = chainCerts.slice(0, -1).map(async (currentCert, index) => {
const issuerCert = chainCerts[index + 1];
return currentCert.verify({ publicKey: issuerCert.publicKey }).catch(() => false);
});
const verificationResults = await Promise.all(verificationPromises);
if (verificationResults.some((result) => !result)) {
throw new BadRequestError({
message: "Certificate chain verification failed: broken trust chain"
});
}
// Verify private key matches the certificate
let privateKey;
try {
privateKey = createPrivateKey(privateKeyPem);
} catch (err) {
throw new BadRequestError({ message: "Invalid private key format" });
}
try {
const message = Buffer.from(Buffer.alloc(32));
const publicKey = createPublicKey(certificatePem);
const signature = sign(null, message, privateKey);
const isValid = verify(null, message, publicKey, signature);
if (!isValid) {
throw new BadRequestError({ message: "Private key does not match certificate" });
}
} catch (err) {
if (err instanceof BadRequestError) {
throw err;
}
throw new BadRequestError({ message: "Error verifying private key against certificate" });
}
// Get certificate attributes
const commonName = Array.from(leafCert.subjectName.getField("CN")?.values() || [])[0] || "";
let altNames: undefined | string;
const sanExtension = leafCert.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
altNames = sanNames.items.map((name) => name.value).join(", ");
}
const { serialNumber, notBefore, notAfter } = leafCert;
// Encrypt certificate for storage
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKeyId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(certificatePem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(privateKeyPem)
});
// Extract Key Usage
const keyUsagesExt = leafCert.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
let keyUsages: CertKeyUsage[] = [];
if (keyUsagesExt) {
keyUsages = Object.values(CertKeyUsage).filter(
// eslint-disable-next-line no-bitwise
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & keyUsagesExt.usages) !== 0
);
}
// Extract Extended Key Usage
const extKeyUsageExt = leafCert.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
let extendedKeyUsages: CertExtendedKeyUsage[] = [];
if (extKeyUsageExt) {
extendedKeyUsages = extKeyUsageExt.usages.map((ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]);
}
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(chainPem)
});
const cert = await certificateDAL.transaction(async (tx) => {
try {
const txCert = await certificateDAL.create(
{
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
altNames,
serialNumber,
notBefore,
notAfter,
projectId,
keyUsages,
extendedKeyUsages
},
tx
);
await certificateBodyDAL.create(
{
certId: txCert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: txCert.id,
encryptedPrivateKey
},
tx
);
if (collectionId) {
await pkiCollectionItemDAL.create(
{
pkiCollectionId: collectionId,
certId: txCert.id
},
tx
);
}
return txCert;
} catch (error) {
// @ts-expect-error We're expecting a database error
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.error?.code === "23505") {
throw new BadRequestError({ message: "Certificate serial already exists in your project" });
}
throw error;
}
});
return {
certificate: certificatePem,
certificateChain: chainPem,
privateKey: privateKeyPem,
serialNumber,
cert
}; };
}; };
@@ -303,7 +535,6 @@ export const certificateServiceFactory = ({
*/ */
const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => { const getCertBundle = async ({ serialNumber, actorId, actorAuthMethod, actor, actorOrgId }: TGetCertBundleDTO) => {
const cert = await certificateDAL.findOne({ serialNumber }); const cert = await certificateDAL.findOne({ serialNumber });
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -375,8 +606,7 @@ export const certificateServiceFactory = ({
certificateChain, certificateChain,
privateKey: certPrivateKey, privateKey: certPrivateKey,
serialNumber, serialNumber,
cert, cert
ca: expandInternalCa(ca)
}; };
}; };
@@ -25,19 +25,19 @@ export const pkiAlertDALFactory = (db: TDbClient) => {
recipientEmails: string; recipientEmails: string;
}; };
// SHEEN TODO: FIX REGRESION HERE
// gets CAs and certificates as part of PKI collection items // gets CAs and certificates as part of PKI collection items
const combinedQuery = db const combinedQuery = db
.replicaNode() .replicaNode()
.select( .select(
db.raw("? as type", [PkiItemType.CA]), db.raw("? as type", [PkiItemType.CA]),
`${PkiItemType.CA}.id`, `${PkiItemType.CA}.id`,
`${PkiItemType.CA}.notAfter as expiryDate`, "ic.notAfter as expiryDate",
`${PkiItemType.CA}.serialNumber`, "ic.serialNumber",
`${PkiItemType.CA}.friendlyName`, "ic.friendlyName",
"pci.pkiCollectionId" "pci.pkiCollectionId"
) )
.from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`) .from(`${TableName.CertificateAuthority} as ${PkiItemType.CA}`)
.join(`${TableName.InternalCertificateAuthority} as ic`, `${PkiItemType.CA}.id`, "ic.certificateAuthorityId")
.join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId") .join(`${TableName.PkiCollectionItem} as pci`, `${PkiItemType.CA}.id`, "pci.caId")
.unionAll((qb) => { .unionAll((qb) => {
void qb void qb
@@ -320,6 +320,10 @@ export const pkiSubscriberServiceFactory = ({
throw new BadRequestError({ message: "CA does not support ordering of certificates" }); throw new BadRequestError({ message: "CA does not support ordering of certificates" });
} }
if (ca.externalCa?.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) { if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
await certificateAuthorityQueue.orderCertificateForSubscriber({ await certificateAuthorityQueue.orderCertificateForSubscriber({
subscriberId: subscriber.id, subscriberId: subscriber.id,
+3 -1
View File
@@ -4,10 +4,12 @@ export {
useCreateCertificate, useCreateCertificate,
useCreateUnifiedCa, useCreateUnifiedCa,
useDeleteCa, useDeleteCa,
useDeleteUnifiedCa,
useImportCaCertificate, useImportCaCertificate,
useRenewCa, useRenewCa,
useSignIntermediate, useSignIntermediate,
useUpdateCa useUpdateCa,
useUpdateUnifiedCa
} from "./mutations"; } from "./mutations";
export { export {
useGetCaById, useGetCaById,
+43 -1
View File
@@ -11,6 +11,7 @@ import {
TCreateCertificateResponse, TCreateCertificateResponse,
TCreateUnifiedCertificateAuthorityDTO, TCreateUnifiedCertificateAuthorityDTO,
TDeleteCaDTO, TDeleteCaDTO,
TDeleteUnifiedCertificateAuthorityDTO,
TImportCaCertificateDTO, TImportCaCertificateDTO,
TImportCaCertificateResponse, TImportCaCertificateResponse,
TRenewCaDTO, TRenewCaDTO,
@@ -18,9 +19,31 @@ import {
TSignIntermediateDTO, TSignIntermediateDTO,
TSignIntermediateResponse, TSignIntermediateResponse,
TUnifiedCertificateAuthority, TUnifiedCertificateAuthority,
TUpdateCaDTO TUpdateCaDTO,
TUpdateUnifiedCertificateAuthorityDTO
} from "./types"; } from "./types";
export const useUpdateUnifiedCa = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateAuthority, object, TUpdateUnifiedCertificateAuthorityDTO>({
mutationFn: async ({ id, ...body }) => {
const {
data: { certificateAuthority }
} = await apiRequest.patch<{ certificateAuthority: TUnifiedCertificateAuthority }>(
`/api/v1/pki/ca/${body.type}/${id}`,
body
);
return certificateAuthority;
},
onSuccess: ({ projectId, type }) => {
queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
});
}
});
};
export const useCreateUnifiedCa = () => { export const useCreateUnifiedCa = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateAuthority, object, TCreateUnifiedCertificateAuthorityDTO>({ return useMutation<TUnifiedCertificateAuthority, object, TCreateUnifiedCertificateAuthorityDTO>({
@@ -39,6 +62,25 @@ export const useCreateUnifiedCa = () => {
}); });
}; };
export const useDeleteUnifiedCa = () => {
const queryClient = useQueryClient();
return useMutation<TUnifiedCertificateAuthority, object, TDeleteUnifiedCertificateAuthorityDTO>({
mutationFn: async ({ caId, type }) => {
const {
data: { certificateAuthority }
} = await apiRequest.delete<{ certificateAuthority: TUnifiedCertificateAuthority }>(
`/api/v1/pki/ca/${type}/${caId}`
);
return certificateAuthority;
},
onSuccess: (_, { type, projectId }) => {
queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
});
}
});
};
export const useCreateCa = () => { export const useCreateCa = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<TCertificateAuthority, object, TCreateCaDTO>({ return useMutation<TCertificateAuthority, object, TCreateCaDTO>({
+14 -1
View File
@@ -10,7 +10,10 @@ export type TAcmeCertificateAuthority = {
disableDirectIssuance: boolean; disableDirectIssuance: boolean;
configuration: { configuration: {
dnsAppConnectionId: string; dnsAppConnectionId: string;
dnsProvider: AcmeDnsProvider; dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53;
hostedZoneId: string;
};
directoryUrl: string; directoryUrl: string;
accountEmail: string; accountEmail: string;
}; };
@@ -48,6 +51,16 @@ export type TUnifiedCertificateAuthority =
| TInternalCertificateAuthority; | TInternalCertificateAuthority;
export type TCreateUnifiedCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">; export type TCreateUnifiedCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
export type TUpdateUnifiedCertificateAuthorityDTO = Partial<TUnifiedCertificateAuthority> & {
id: string;
type: CaType;
};
export type TDeleteUnifiedCertificateAuthorityDTO = {
caId: string;
type: CaType;
projectId: string;
};
export type TCertificateAuthority = { export type TCertificateAuthority = {
id: string; id: string;
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TReactQueryOptions } from "@app/types/reactQuery";
import { TCertificate } from "../certificates/types"; import { TCertificate } from "../certificates/types";
import { TPkiSubscriber } from "./types"; import { TPkiSubscriber } from "./types";
@@ -62,17 +63,20 @@ export const useGetPkiSubscriber = ({
}); });
}; };
export const useGetPkiSubscriberCertificates = ({ export const useGetPkiSubscriberCertificates = (
subscriberName, {
projectId, subscriberName,
offset, projectId,
limit offset,
}: { limit
subscriberName: string; }: {
projectId: string; subscriberName: string;
offset: number; projectId: string;
limit: number; offset: number;
}) => { limit: number;
},
options?: TReactQueryOptions["options"]
) => {
return useQuery({ return useQuery({
queryKey: pkiSubscriberKeys.specificPkiSubscriberCertificates({ queryKey: pkiSubscriberKeys.specificPkiSubscriberCertificates({
subscriberName, subscriberName,
@@ -97,6 +101,7 @@ export const useGetPkiSubscriberCertificates = ({
); );
return { certificates, totalCount }; return { certificates, totalCount };
}, },
enabled: Boolean(subscriberName) && Boolean(projectId) enabled: Boolean(subscriberName) && Boolean(projectId),
...options
}); });
}; };
@@ -23,23 +23,27 @@ import {
CaStatus, CaStatus,
CaType, CaType,
useCreateUnifiedCa, useCreateUnifiedCa,
useGetCaById, useGetCaByTypeAndId,
useUpdateCa useUpdateUnifiedCa
} from "@app/hooks/api/ca"; } from "@app/hooks/api/ca";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
type: z.enum([CaType.ACME]), type: z.nativeEnum(CaType),
name: z.string(), name: z.string(),
disableDirectIssuance: z.boolean(), disableDirectIssuance: z.boolean(),
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]), status: z.nativeEnum(CaStatus),
configuration: z.object({ configuration: z.object({
dnsAppConnection: z.object({ dnsAppConnection: z.object({
id: z.string(), id: z.string(),
name: z.string() name: z.string()
}), }),
dnsProvider: z.nativeEnum(AcmeDnsProvider), // currently specific to Route53 but can be extended to others by differentiating via the provider property
dnsProviderConfig: z.object({
provider: z.nativeEnum(AcmeDnsProvider),
hostedZoneId: z.string()
}),
directoryUrl: z.string(), directoryUrl: z.string(),
accountEmail: z.string() accountEmail: z.string()
}) })
@@ -58,11 +62,13 @@ const caTypes = [{ label: "ACME", value: CaType.ACME }];
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || ""); const { data: ca } = useGetCaByTypeAndId(
(popUp?.ca?.data as { type: CaType })?.type || "",
(popUp?.ca?.data as { caId: string })?.caId || ""
);
// SHEEN TODO: finish up CA management
const { mutateAsync: createMutateAsync } = useCreateUnifiedCa(); const { mutateAsync: createMutateAsync } = useCreateUnifiedCa();
const { mutateAsync: updateMutateAsync } = useUpdateCa(); const { mutateAsync: updateMutateAsync } = useUpdateUnifiedCa();
const { const {
control, control,
@@ -82,7 +88,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
id: "", id: "",
name: "" name: ""
}, },
dnsProvider: AcmeDnsProvider.ROUTE53, dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53,
hostedZoneId: ""
},
directoryUrl: "", directoryUrl: "",
accountEmail: "" accountEmail: ""
} }
@@ -90,7 +99,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
}); });
const caType = watch("type"); const caType = watch("type");
const dnsProvider = watch("configuration.dnsProvider"); const dnsProvider = watch("configuration.dnsProviderConfig.provider");
const { data: availableConnections, isPending } = useListAvailableAppConnections( const { data: availableConnections, isPending } = useListAvailableAppConnections(
AppConnection.AWS, AppConnection.AWS,
@@ -101,11 +110,30 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
useEffect(() => { useEffect(() => {
if (ca) { if (ca) {
// reset({ if (ca.type !== CaType.INTERNAL && availableConnections?.length) {
// type: ca.type, const selectedConnection = availableConnections?.find(
// name: ca.name, (connection) => connection.id === ca?.configuration.dnsAppConnectionId
// disableDirectIssuance: ca.disableDirectIssuance );
// });
reset({
type: ca.type,
name: ca.name,
status: ca.status,
disableDirectIssuance: ca.disableDirectIssuance,
configuration: {
dnsAppConnection: {
id: ca.configuration.dnsAppConnectionId,
name: selectedConnection?.name || ""
},
dnsProviderConfig: {
provider: ca.configuration.dnsProviderConfig.provider,
hostedZoneId: ca.configuration.dnsProviderConfig.hostedZoneId
},
directoryUrl: ca.configuration.directoryUrl,
accountEmail: ca.configuration.accountEmail
}
});
}
} else { } else {
reset({ reset({
type: CaType.ACME, type: CaType.ACME,
@@ -117,13 +145,16 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
id: "", id: "",
name: "" name: ""
}, },
dnsProvider: AcmeDnsProvider.ROUTE53, dnsProviderConfig: {
provider: AcmeDnsProvider.ROUTE53,
hostedZoneId: ""
},
directoryUrl: "", directoryUrl: "",
accountEmail: "" accountEmail: ""
} }
}); });
} }
}, [ca]); }, [ca, availableConnections]);
const onFormSubmit = async ({ const onFormSubmit = async ({
type, type,
@@ -135,17 +166,20 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
try { try {
if (!currentWorkspace?.slug) return; if (!currentWorkspace?.slug) return;
if (ca) { if (ca && type !== CaType.INTERNAL) {
// update await updateMutateAsync({
// await updateMutateAsync({ id: ca.id,
// projectSlug: currentWorkspace.slug, projectId: currentWorkspace.id,
// caId: ca.id, name,
// name, type,
// disableDirectIssuance, status,
// status disableDirectIssuance,
// }); configuration: {
...configuration,
dnsAppConnectionId: configuration.dnsAppConnection.id
}
});
} else { } else {
// create
await createMutateAsync({ await createMutateAsync({
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
name, name,
@@ -217,7 +251,12 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
defaultValue="" defaultValue=""
name="name" name="name"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}> <FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="my-external-ca" isDisabled={Boolean(ca)} /> <Input {...field} placeholder="my-external-ca" isDisabled={Boolean(ca)} />
</FormControl> </FormControl>
)} )}
@@ -226,7 +265,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
<> <>
<Controller <Controller
control={control} control={control}
name="configuration.dnsProvider" name="configuration.dnsProviderConfig.provider"
defaultValue={AcmeDnsProvider.ROUTE53} defaultValue={AcmeDnsProvider.ROUTE53}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
@@ -275,6 +314,21 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
control={control} control={control}
name="configuration.dnsAppConnection" name="configuration.dnsAppConnection"
/> />
<Controller
control={control}
defaultValue=""
name="configuration.dnsProviderConfig.hostedZoneId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Hosted Zone ID"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Z040441124N1GOOMCQYX1" />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""
@@ -284,6 +338,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
label="Directory URL" label="Directory URL"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
isRequired
> >
<Input <Input
{...field} {...field}
@@ -301,6 +356,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
label="Account Email" label="Account Email"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
isRequired
> >
<Input {...field} placeholder="[email protected]" /> <Input {...field} placeholder="[email protected]" />
</FormControl> </FormControl>
@@ -6,7 +6,7 @@ import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus, useDeleteCa, useUpdateCa } from "@app/hooks/api"; import { CaStatus, CaType, useDeleteUnifiedCa, useUpdateUnifiedCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { ExternalCaModal } from "./ExternalCaModal"; import { ExternalCaModal } from "./ExternalCaModal";
@@ -14,8 +14,8 @@ import { ExternalCaTable } from "./ExternalCaTable";
export const ExternalCaSection = () => { export const ExternalCaSection = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { mutateAsync: deleteCa } = useDeleteCa(); const { mutateAsync: deleteCa } = useDeleteUnifiedCa();
const { mutateAsync: updateCa } = useUpdateCa(); const { mutateAsync: updateCa } = useUpdateUnifiedCa();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"ca", "ca",
@@ -24,11 +24,11 @@ export const ExternalCaSection = () => {
"upgradePlan" "upgradePlan"
] as const); ] as const);
const onRemoveCaSubmit = async (caId: string) => { const onRemoveCaSubmit = async (caId: string, type: CaType) => {
try { try {
if (!currentWorkspace?.slug) return; if (!currentWorkspace?.slug) return;
await deleteCa({ caId, projectSlug: currentWorkspace.slug }); await deleteCa({ caId, type, projectId: currentWorkspace.id });
createNotification({ createNotification({
text: "Successfully deleted CA", text: "Successfully deleted CA",
@@ -44,11 +44,19 @@ export const ExternalCaSection = () => {
} }
}; };
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => { const onUpdateCaStatus = async ({
caId,
type,
status
}: {
caId: string;
type: CaType;
status: CaStatus;
}) => {
try { try {
if (!currentWorkspace?.slug) return; if (!currentWorkspace?.slug) return;
await updateCa({ caId, projectSlug: currentWorkspace.slug, status }); await updateCa({ id: caId, type, status });
createNotification({ createNotification({
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`, text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
@@ -96,7 +104,12 @@ export const ExternalCaSection = () => {
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy." subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)} onDeleteApproved={() =>
onRemoveCaSubmit(
(popUp?.deleteCa?.data as { caId: string })?.caId,
(popUp?.deleteCa?.data as { type: CaType })?.type
)
}
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.caStatus.isOpen} isOpen={popUp.caStatus.isOpen}
@@ -111,9 +124,12 @@ export const ExternalCaSection = () => {
: "This action will prevent the CA from issuing new certificates." : "This action will prevent the CA from issuing new certificates."
} }
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)} onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
buttonText="Proceed"
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus }) onUpdateCaStatus(
popUp?.caStatus?.data as { caId: string; type: CaType; status: CaStatus }
)
} }
/> />
<UpgradePlanModal <UpgradePlanModal
@@ -1,6 +1,11 @@
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; import {
faBan,
faCertificate,
faEllipsis,
faPencil,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
@@ -24,7 +29,6 @@ import {
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api"; import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -32,7 +36,7 @@ type Props = {
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>, popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
data?: { data?: {
caId?: string; caId?: string;
dn?: string; type?: CaType;
status?: CaStatus; status?: CaStatus;
description?: string; description?: string;
} }
@@ -40,7 +44,6 @@ type Props = {
}; };
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id); const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id);
@@ -66,15 +69,12 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
<Tr <Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700" className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`ca-${ca.id}`} key={`ca-${ca.id}`}
onClick={() => onClick={() => {
navigate({ handlePopUpOpen("ca", {
to: `/${ProjectType.CertificateManager}/$projectId/ca/$caId` as const, caId: ca.id,
params: { type: ca.type
projectId: currentWorkspace.id, });
caId: ca.id }}
}
})
}
> >
<Td>{ca.name}</Td> <Td>{ca.name}</Td>
<Td>{ca.type}</Td> <Td>{ca.type}</Td>
@@ -93,6 +93,29 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("ca", {
caId: ca.id,
type: ca.type
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit CA
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && ( {(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
@@ -108,6 +131,7 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
e.stopPropagation(); e.stopPropagation();
handlePopUpOpen("caStatus", { handlePopUpOpen("caStatus", {
caId: ca.id, caId: ca.id,
type: ca.type,
status: status:
ca.status === CaStatus.ACTIVE ca.status === CaStatus.ACTIVE
? CaStatus.DISABLED ? CaStatus.DISABLED
@@ -133,10 +157,10 @@ export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
)} )}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// handlePopUpOpen("deleteCa", { handlePopUpOpen("deleteCa", {
// caId: ca.id, caId: ca.id,
// dn: ca.dn type: ca.type
// }); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />} icon={<FontAwesomeIcon icon={faTrash} />}
@@ -48,12 +48,17 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT); const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { data, isPending } = useGetPkiSubscriberCertificates({ const { data, isPending } = useGetPkiSubscriberCertificates(
subscriberName, {
projectId, subscriberName,
offset: (page - 1) * perPage, projectId,
limit: perPage offset: (page - 1) * perPage,
}); limit: perPage
},
{
refetchInterval: 10 * 1000 // 10 seconds
}
);
const getCertStatusBadge = (status: string, notAfter: string) => { const getCertStatusBadge = (status: string, notAfter: string) => {
if (status === CertStatus.REVOKED) { if (status === CertStatus.REVOKED) {