mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 00:26:24 +00:00
misc: addressed comments
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+2
-2
@@ -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`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-61
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-3
@@ -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;
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
+26
-17
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>({
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+85
-29
@@ -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>
|
||||||
|
|||||||
+25
-9
@@ -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
|
||||||
|
|||||||
+42
-18
@@ -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} />}
|
||||||
|
|||||||
+11
-6
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user