mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 08:28:22 +00:00
feat: POC for ACME done
This commit is contained in:
Generated
+1877
File diff suppressed because it is too large
Load Diff
@@ -131,6 +131,7 @@
|
|||||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||||
"@aws-sdk/client-iam": "^3.525.0",
|
"@aws-sdk/client-iam": "^3.525.0",
|
||||||
"@aws-sdk/client-kms": "^3.609.0",
|
"@aws-sdk/client-kms": "^3.609.0",
|
||||||
|
"@aws-sdk/client-route-53": "^3.810.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||||
"@aws-sdk/client-sts": "^3.600.0",
|
"@aws-sdk/client-sts": "^3.600.0",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
@@ -174,6 +175,7 @@
|
|||||||
"@slack/oauth": "^3.0.2",
|
"@slack/oauth": "^3.0.2",
|
||||||
"@slack/web-api": "^7.8.0",
|
"@slack/web-api": "^7.8.0",
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
"acme-client": "^5.4.0",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"argon2": "^0.31.2",
|
"argon2": "^0.31.2",
|
||||||
"aws-sdk": "^2.1553.0",
|
"aws-sdk": "^2.1553.0",
|
||||||
|
|||||||
@@ -971,20 +971,6 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
|
||||||
pkiSubscriberDAL,
|
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateAuthorityCertDAL,
|
|
||||||
certificateAuthoritySecretDAL,
|
|
||||||
certificateAuthorityCrlDAL,
|
|
||||||
certificateDAL,
|
|
||||||
certificateBodyDAL,
|
|
||||||
certificateSecretDAL,
|
|
||||||
projectDAL,
|
|
||||||
kmsService,
|
|
||||||
permissionService
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectTemplateService = projectTemplateServiceFactory({
|
const projectTemplateService = projectTemplateServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -1690,6 +1676,23 @@ export const registerRoutes = async (
|
|||||||
internalCertificateAuthorityService
|
internalCertificateAuthorityService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||||
|
pkiSubscriberDAL,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
permissionService,
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
externalCertificateAuthorityDAL
|
||||||
|
});
|
||||||
|
|
||||||
await secretRotationV2QueueServiceFactory({
|
await secretRotationV2QueueServiceFactory({
|
||||||
secretRotationV2Service,
|
secretRotationV2Service,
|
||||||
secretRotationV2DAL,
|
secretRotationV2DAL,
|
||||||
|
|||||||
@@ -278,6 +278,76 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:subscriberName/order-certificate",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiSubscribers],
|
||||||
|
description: "Issue certificate",
|
||||||
|
params: z.object({
|
||||||
|
subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificate),
|
||||||
|
issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.issuingCaCertificate),
|
||||||
|
certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificateChain),
|
||||||
|
privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.privateKey),
|
||||||
|
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.pkiSubscriber.issueSubscriberCert({
|
||||||
|
subscriberName: req.params.subscriberName,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: subscriber.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
|
||||||
|
metadata: {
|
||||||
|
subscriberId: subscriber.id,
|
||||||
|
name: subscriber.name,
|
||||||
|
serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.IssueCert,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
subscriberId: subscriber.id,
|
||||||
|
commonName: subscriber.commonName,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate,
|
||||||
|
certificateChain,
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey,
|
||||||
|
serialNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:subscriberName/issue-certificate",
|
url: "/:subscriberName/issue-certificate",
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
|
||||||
|
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
|
import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
|
||||||
|
|
||||||
|
const CertificateAuthoritySchema = z.discriminatedUnion("type", [
|
||||||
|
InternalCertificateAuthoritySchema,
|
||||||
|
AcmeCertificateAuthoritySchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||||
|
description: "Get Certificate Authorities",
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
certificateAuthorities: CertificateAuthoritySchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const internalCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||||
|
{
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
type: CaType.INTERNAL
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
const acmeCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||||
|
{
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
type: CaType.ACME
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
// await server.services.auditLog.createAuditLog({
|
||||||
|
// ...req.auditLogInfo,
|
||||||
|
// projectId: ca.projectId,
|
||||||
|
// event: {
|
||||||
|
// type: EventType.GET_CA,
|
||||||
|
// metadata: {
|
||||||
|
// caId: ca.id,
|
||||||
|
// dn: ca.dn
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerCaRouter } from "./certificate-authority-router";
|
||||||
import { registerGroupProjectRouter } from "./group-project-router";
|
import { registerGroupProjectRouter } from "./group-project-router";
|
||||||
import { registerIdentityOrgRouter } from "./identity-org-router";
|
import { registerIdentityOrgRouter } from "./identity-org-router";
|
||||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||||
@@ -14,6 +15,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerUserRouter, { prefix: "/users" });
|
await server.register(registerUserRouter, { prefix: "/users" });
|
||||||
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
|
||||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
|
await server.register(registerCaRouter, { prefix: "/pki/ca" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (orgRouter) => {
|
async (orgRouter) => {
|
||||||
await orgRouter.register(registerOrgRouter);
|
await orgRouter.register(registerOrgRouter);
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
|
import { ChangeResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import acme from "acme-client";
|
||||||
|
import { KeyObject } from "crypto";
|
||||||
|
|
||||||
|
import { TableName, TPkiSubscribers } from "@app/db/schemas";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
|
import { TAwsConnection, TAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsage,
|
||||||
|
CertKeyAlgorithm,
|
||||||
|
CertKeyUsage,
|
||||||
|
CertStatus
|
||||||
|
} from "@app/services/certificate/certificate-types";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
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 { TCertificateAuthority } from "../certificate-authority-types";
|
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 {
|
import {
|
||||||
|
TAcmeCertificateAuthority,
|
||||||
TCreateAcmeCertificateAuthorityDTO,
|
TCreateAcmeCertificateAuthorityDTO,
|
||||||
TUpdateAcmeCertificateAuthorityDTO
|
TUpdateAcmeCertificateAuthorityDTO
|
||||||
} from "./acme-certificate-authority-types";
|
} from "./acme-certificate-authority-types";
|
||||||
@@ -19,16 +41,113 @@ type TAcmeCertificateAuthorityFnsDeps = {
|
|||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
certificateAuthorityDAL: Pick<
|
certificateAuthorityDAL: Pick<
|
||||||
TCertificateAuthorityDALFactory,
|
TCertificateAuthorityDALFactory,
|
||||||
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById"
|
"create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa"
|
||||||
>;
|
>;
|
||||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "encryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DBConfigurationColumn = {
|
||||||
|
dnsProvider: string;
|
||||||
|
directoryUrl: string;
|
||||||
|
accountEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const castDbEntryToAcmeCertificateAuthority = (
|
||||||
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||||
|
): TAcmeCertificateAuthority => {
|
||||||
|
if (!ca.externalCa) {
|
||||||
|
throw new BadRequestError({ message: "Malformed ACME certificate authority" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbConfigurationCol = ca.externalCa.configuration as DBConfigurationColumn;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: ca.id,
|
||||||
|
type: CaType.ACME,
|
||||||
|
disableDirectIssuance: ca.disableDirectIssuance,
|
||||||
|
name: ca.externalCa.name,
|
||||||
|
projectId: ca.projectId,
|
||||||
|
configuration: {
|
||||||
|
dnsAppConnectionId: ca.externalCa.dnsAppConnectionId as string,
|
||||||
|
dnsProvider: dbConfigurationCol.dnsProvider as AcmeDnsProvider,
|
||||||
|
directoryUrl: dbConfigurationCol.directoryUrl,
|
||||||
|
accountEmail: dbConfigurationCol.accountEmail
|
||||||
|
},
|
||||||
|
status: ca.externalCa.status as CaStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const route53InsertTxtRecord = async (connection: TAwsConnectionConfig, domain: string, value: string) => {
|
||||||
|
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||||
|
const route53Client = new Route53Client({
|
||||||
|
credentials: config.credentials!,
|
||||||
|
region: config.region
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new ChangeResourceRecordSetsCommand({
|
||||||
|
HostedZoneId: "Z040441124N1GOOMCQYX1", // SHEEN TODO: Get this from user input
|
||||||
|
ChangeBatch: {
|
||||||
|
Comment: "Set ACME challenge TXT record",
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: "UPSERT",
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: domain,
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: 30,
|
||||||
|
ResourceRecords: [{ Value: value }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await route53Client.send(command);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const route53DeleteTxtRecord = async (connection: TAwsConnectionConfig, domain: string, value: string) => {
|
||||||
|
const config = await getAwsConnectionConfig(connection, AWSRegion.US_WEST_1); // REGION is irrelevant because Route53 is global
|
||||||
|
const route53Client = new Route53Client({
|
||||||
|
credentials: config.credentials!,
|
||||||
|
region: config.region
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new ChangeResourceRecordSetsCommand({
|
||||||
|
HostedZoneId: "Z040441124N1GOOMCQYX1", // SHEEN TODO: same here
|
||||||
|
ChangeBatch: {
|
||||||
|
Comment: "Delete ACME challenge TXT record",
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: "DELETE",
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: domain,
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: 30,
|
||||||
|
ResourceRecords: [{ Value: value }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await route53Client.send(command);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AcmeCertificateAuthorityFns = ({
|
export const AcmeCertificateAuthorityFns = ({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
externalCertificateAuthorityDAL
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
}: TAcmeCertificateAuthorityFnsDeps) => {
|
}: TAcmeCertificateAuthorityFnsDeps) => {
|
||||||
const createCertificateAuthority = async ({
|
const createCertificateAuthority = async ({
|
||||||
name,
|
name,
|
||||||
@@ -192,8 +311,150 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => {
|
||||||
|
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
||||||
|
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||||
|
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.ACME
|
||||||
|
});
|
||||||
|
|
||||||
|
return cas.map(castDbEntryToAcmeCertificateAuthority);
|
||||||
|
};
|
||||||
|
|
||||||
|
// SHEEN TODO: need to execute this from a job
|
||||||
|
const orderCertificate = async (
|
||||||
|
subscriber: TPkiSubscribers,
|
||||||
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
|
||||||
|
|
||||||
|
// SHEEN TODO: need to save this in credentials field and reuse
|
||||||
|
const privateRsaKey = await acme.crypto.createPrivateRsaKey();
|
||||||
|
|
||||||
|
const acmeClient = new acme.Client({
|
||||||
|
directoryUrl: acmeCa.configuration.directoryUrl,
|
||||||
|
accountKey: privateRsaKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
|
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
const [, certificateCsr] = await acme.crypto.createCsr(
|
||||||
|
{
|
||||||
|
altNames: subscriber.subjectAlternativeNames,
|
||||||
|
commonName: subscriber.commonName
|
||||||
|
},
|
||||||
|
skLeaf
|
||||||
|
);
|
||||||
|
|
||||||
|
// SHEEN TODO: need to update this to remove dependence on ACTOR
|
||||||
|
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
|
||||||
|
const connection = await appConnectionService.connectAppConnectionById(
|
||||||
|
appConnection.app as AppConnection,
|
||||||
|
acmeCa.configuration.dnsAppConnectionId,
|
||||||
|
actor
|
||||||
|
);
|
||||||
|
|
||||||
|
const pem = await acmeClient.auto({
|
||||||
|
csr: certificateCsr,
|
||||||
|
email: acmeCa.configuration.accountEmail,
|
||||||
|
challengePriority: ["dns-01"],
|
||||||
|
termsOfServiceAgreed: true,
|
||||||
|
|
||||||
|
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
||||||
|
if (challenge.type !== "dns-01") {
|
||||||
|
throw new Error("Unsupported challenge type");
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||||
|
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||||
|
|
||||||
|
if (acmeCa.configuration.dnsProvider === AcmeDnsProvider.Route53) {
|
||||||
|
await route53InsertTxtRecord(connection as TAwsConnection, recordName, recordValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||||
|
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||||
|
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||||
|
|
||||||
|
if (acmeCa.configuration.dnsProvider === AcmeDnsProvider.Route53) {
|
||||||
|
await route53DeleteTxtRecord(connection as TAwsConnection, recordName, recordValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("PEM IS", pem);
|
||||||
|
|
||||||
|
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
|
||||||
|
const certObj = new x509.X509Certificate(leafCert);
|
||||||
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(certObj.rawData))
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChainPem = parentCert.trim();
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(certificateChainPem)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(skLeaf)
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
pkiSubscriberId: subscriber.id,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: subscriber.commonName,
|
||||||
|
commonName: subscriber.commonName,
|
||||||
|
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||||
|
serialNumber: certObj.serialNumber,
|
||||||
|
notBefore: certObj.notBefore,
|
||||||
|
notAfter: certObj.notAfter,
|
||||||
|
keyUsages: subscriber.keyUsages as CertKeyUsage[],
|
||||||
|
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
|
||||||
|
caCertId: "s" // SHEEN TODO: merge Andrey's PR and then remove this
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedPrivateKey
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createCertificateAuthority,
|
createCertificateAuthority,
|
||||||
updateCertificateAuthority
|
updateCertificateAuthority,
|
||||||
|
listCertificateAuthorities,
|
||||||
|
orderCertificate
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas";
|
import { CertificateAuthoritiesSchema, TableName, TCertificateAuthorities } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
export type TCertificateAuthorityDALFactory = ReturnType<typeof certificateAuthorityDALFactory>;
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findWithAssociatedCa = async (
|
const findWithAssociatedCa = async (
|
||||||
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string },
|
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
|
||||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
|
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
@@ -158,7 +158,8 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.CertificateAuthority}.id`,
|
`${TableName.CertificateAuthority}.id`,
|
||||||
`${TableName.ExternalCertificateAuthority}.certificateAuthorityId`
|
`${TableName.ExternalCertificateAuthority}.certificateAuthorityId`
|
||||||
)
|
)
|
||||||
.where(filter)
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter(filter))
|
||||||
.select(selectAllTableCols(TableName.CertificateAuthority))
|
.select(selectAllTableCols(TableName.CertificateAuthority))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
|
db.ref("id").withSchema(TableName.InternalCertificateAuthority).as("internalCaId"),
|
||||||
|
|||||||
@@ -218,17 +218,12 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateAuthorities
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
|
||||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: finalProjectId,
|
|
||||||
...(type === CaType.INTERNAL && {
|
|
||||||
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"]
|
|
||||||
}),
|
|
||||||
...(type !== CaType.INTERNAL && {
|
|
||||||
[`${TableName.ExternalCertificateAuthority}.type` as "type"]: type
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (type === CaType.INTERNAL) {
|
if (type === CaType.INTERNAL) {
|
||||||
|
const cas = await certificateAuthorityDAL.findWithAssociatedCa({
|
||||||
|
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: finalProjectId,
|
||||||
|
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"]
|
||||||
|
});
|
||||||
|
|
||||||
return cas
|
return cas
|
||||||
.filter((ca): ca is typeof ca & { internalCa: NonNullable<typeof ca.internalCa> } => Boolean(ca.internalCa))
|
.filter((ca): ca is typeof ca & { internalCa: NonNullable<typeof ca.internalCa> } => Boolean(ca.internalCa))
|
||||||
.map((ca) => ({
|
.map((ca) => ({
|
||||||
@@ -242,17 +237,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
})) as TCertificateAuthority[];
|
})) as TCertificateAuthority[];
|
||||||
}
|
}
|
||||||
|
|
||||||
return cas
|
if (type === CaType.ACME) {
|
||||||
.filter((ca): ca is typeof ca & { externalCa: NonNullable<typeof ca.externalCa> } => Boolean(ca.externalCa))
|
return acmeFns.listCertificateAuthorities({ projectId: finalProjectId });
|
||||||
.map((ca) => ({
|
}
|
||||||
id: ca.id,
|
|
||||||
type,
|
|
||||||
disableDirectIssuance: ca.disableDirectIssuance,
|
|
||||||
name: ca.externalCa.name,
|
|
||||||
projectId: ca.projectId,
|
|
||||||
configuration: ca.externalCa.configuration,
|
|
||||||
status: ca.externalCa.status
|
|
||||||
})) as TCertificateAuthority[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCertificateAuthority = async (
|
const updateCertificateAuthority = async (
|
||||||
|
|||||||
+265
@@ -0,0 +1,265 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
import { KeyObject } from "crypto";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TPkiSubscribers } from "@app/db/schemas";
|
||||||
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsage,
|
||||||
|
CertKeyAlgorithm,
|
||||||
|
CertKeyUsage,
|
||||||
|
CertStatus
|
||||||
|
} from "@app/services/certificate/certificate-types";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
|
||||||
|
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
|
||||||
|
import { CaStatus } from "../certificate-authority-enums";
|
||||||
|
import {
|
||||||
|
createSerialNumber,
|
||||||
|
getCaCertChain,
|
||||||
|
getCaCredentials,
|
||||||
|
keyAlgorithmToAlgCfg
|
||||||
|
} from "../certificate-authority-fns";
|
||||||
|
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
||||||
|
|
||||||
|
type TInternalCertificateAuthorityFnsDeps = {
|
||||||
|
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
||||||
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findById" | "transaction" | "findOne" | "updateById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InternalCertificateAuthorityFns = ({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL
|
||||||
|
}: TInternalCertificateAuthorityFnsDeps) => {
|
||||||
|
const issueCertificate = async (
|
||||||
|
subscriber: TPkiSubscribers,
|
||||||
|
ca: Awaited<ReturnType<TCertificateAuthorityDALFactory["findByIdWithAssociatedCa"]>>
|
||||||
|
) => {
|
||||||
|
if (ca.internalCa?.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||||
|
if (!ca.internalCa?.activeCaCertId)
|
||||||
|
throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||||
|
if (ca.disableDirectIssuance) {
|
||||||
|
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
|
||||||
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: ca.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedCaCert = await kmsDecryptor({
|
||||||
|
cipherTextBlob: caCert.encryptedCertificate
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||||
|
const notBeforeDate = new Date();
|
||||||
|
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
||||||
|
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||||
|
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||||
|
|
||||||
|
// check not before constraint
|
||||||
|
if (notBeforeDate < caCertNotBeforeDate) {
|
||||||
|
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// check not after constraint
|
||||||
|
if (notAfterDate > caCertNotAfterDate) {
|
||||||
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
|
name: `CN=${subscriber.commonName}`,
|
||||||
|
keys: leafKeys,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions: [
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||||
|
],
|
||||||
|
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||||
|
caId: ca.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||||
|
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||||
|
|
||||||
|
const extensions: x509.Extension[] = [
|
||||||
|
new x509.BasicConstraintsExtension(false),
|
||||||
|
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||||
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||||
|
new x509.AuthorityInfoAccessExtension({
|
||||||
|
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||||
|
}),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||||
|
if (keyUsagesBitValue) {
|
||||||
|
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriber.extendedKeyUsages.length) {
|
||||||
|
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
|
||||||
|
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
extensions.push(extendedKeyUsagesExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
||||||
|
|
||||||
|
if (subscriber.subjectAlternativeNames?.length) {
|
||||||
|
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
||||||
|
if (z.string().email().safeParse(altName).success) {
|
||||||
|
return { type: "email", value: altName };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||||
|
return { type: "dns", value: altName };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||||
|
extensions.push(altNamesExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialNumber = createSerialNumber();
|
||||||
|
const leafCert = await x509.X509CertificateGenerator.create({
|
||||||
|
serialNumber,
|
||||||
|
subject: csrObj.subject,
|
||||||
|
issuer: caCertObj.subject,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
signingKey: caPrivateKey,
|
||||||
|
publicKey: csrObj.publicKey,
|
||||||
|
signingAlgorithm: alg,
|
||||||
|
extensions
|
||||||
|
});
|
||||||
|
|
||||||
|
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||||
|
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||||
|
|
||||||
|
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||||
|
});
|
||||||
|
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(skLeaf)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||||
|
caCertId: caCert.id,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||||
|
|
||||||
|
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||||
|
plainText: Buffer.from(certificateChainPem)
|
||||||
|
});
|
||||||
|
|
||||||
|
await certificateDAL.transaction(async (tx) => {
|
||||||
|
const cert = await certificateDAL.create(
|
||||||
|
{
|
||||||
|
caId: ca.id,
|
||||||
|
caCertId: caCert.id,
|
||||||
|
pkiSubscriberId: subscriber.id,
|
||||||
|
status: CertStatus.ACTIVE,
|
||||||
|
friendlyName: subscriber.commonName,
|
||||||
|
commonName: subscriber.commonName,
|
||||||
|
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||||
|
serialNumber,
|
||||||
|
notBefore: notBeforeDate,
|
||||||
|
notAfter: notAfterDate,
|
||||||
|
keyUsages: selectedKeyUsages,
|
||||||
|
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateBodyDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedCertificate,
|
||||||
|
encryptedCertificateChain
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await certificateSecretDAL.create(
|
||||||
|
{
|
||||||
|
certId: cert.id,
|
||||||
|
encryptedPrivateKey
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
certificate: leafCert.toString("pem"),
|
||||||
|
certificateChain: certificateChainPem,
|
||||||
|
issuingCaCertificate,
|
||||||
|
privateKey: skLeaf,
|
||||||
|
serialNumber,
|
||||||
|
ca,
|
||||||
|
subscriber
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueCertificate
|
||||||
|
};
|
||||||
|
};
|
||||||
+24
-3
@@ -10,13 +10,34 @@ import {
|
|||||||
} from "../certificate-authority-schemas";
|
} from "../certificate-authority-schemas";
|
||||||
import { validateCaDateField } from "../certificate-authority-validators";
|
import { validateCaDateField } from "../certificate-authority-validators";
|
||||||
|
|
||||||
const InternalCertificateAuthorityConfigurationSchema = z
|
const InternalCertificateAuthorityConfigurationSchema = z.object({
|
||||||
|
type: z.nativeEnum(InternalCaType),
|
||||||
|
friendlyName: z.string().optional(),
|
||||||
|
commonName: z.string().trim(),
|
||||||
|
organization: z.string().trim(),
|
||||||
|
ou: z.string().trim(),
|
||||||
|
dn: z.string().trim(),
|
||||||
|
parentCaId: z.string().uuid().nullable(),
|
||||||
|
serialNumber: z.string().trim(),
|
||||||
|
activeCaCertId: z.string().uuid().nullable(),
|
||||||
|
country: z.string().trim(),
|
||||||
|
province: z.string().trim(),
|
||||||
|
locality: z.string().trim(),
|
||||||
|
notBefore: z.date().optional(),
|
||||||
|
notAfter: z.date().optional(),
|
||||||
|
maxPathLength: z.number().min(-1),
|
||||||
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
|
});
|
||||||
|
|
||||||
|
const CreateInternalCertificateAuthorityConfigurationSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.nativeEnum(InternalCaType),
|
type: z.nativeEnum(InternalCaType),
|
||||||
friendlyName: z.string().optional(),
|
friendlyName: z.string().optional(),
|
||||||
commonName: z.string().trim(),
|
commonName: z.string().trim(),
|
||||||
organization: z.string().trim(),
|
organization: z.string().trim(),
|
||||||
ou: z.string().trim(),
|
ou: z.string().trim(),
|
||||||
|
dn: z.string().trim(),
|
||||||
|
parentCaId: z.string().uuid().optional(),
|
||||||
country: z.string().trim(),
|
country: z.string().trim(),
|
||||||
province: z.string().trim(),
|
province: z.string().trim(),
|
||||||
locality: z.string().trim(),
|
locality: z.string().trim(),
|
||||||
@@ -48,11 +69,11 @@ export const InternalCertificateAuthoritySchema = BaseCertificateAuthoritySchema
|
|||||||
export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
|
export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
|
||||||
CaType.INTERNAL
|
CaType.INTERNAL
|
||||||
).extend({
|
).extend({
|
||||||
configuration: InternalCertificateAuthorityConfigurationSchema
|
configuration: CreateInternalCertificateAuthorityConfigurationSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
|
export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
|
||||||
CaType.INTERNAL
|
CaType.INTERNAL
|
||||||
).extend({
|
).extend({
|
||||||
configuration: InternalCertificateAuthorityConfigurationSchema.optional()
|
configuration: CreateInternalCertificateAuthorityConfigurationSchema.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import * as x509 from "@peculiar/x509";
|
import * as x509 from "@peculiar/x509";
|
||||||
import crypto, { KeyObject } from "crypto";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
@@ -14,10 +12,8 @@ import {
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
|
||||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
|
||||||
import {
|
import {
|
||||||
CertExtendedKeyUsage,
|
CertExtendedKeyUsage,
|
||||||
CertExtendedKeyUsageOIDToName,
|
CertExtendedKeyUsageOIDToName,
|
||||||
@@ -27,7 +23,7 @@ import {
|
|||||||
} from "@app/services/certificate/certificate-types";
|
} from "@app/services/certificate/certificate-types";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums";
|
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||||
import {
|
import {
|
||||||
createSerialNumber,
|
createSerialNumber,
|
||||||
expandInternalCa,
|
expandInternalCa,
|
||||||
@@ -42,6 +38,12 @@ import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subsc
|
|||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||||
|
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
|
||||||
|
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||||
|
import { AcmeCertificateAuthorityFns } from "../certificate-authority/acme/acme-certificate-authority-fns";
|
||||||
|
import { TExternalCertificateAuthorityDALFactory } from "../certificate-authority/external-certificate-authority-dal";
|
||||||
|
import { InternalCertificateAuthorityFns } from "../certificate-authority/internal/internal-certificate-authority-fns";
|
||||||
import {
|
import {
|
||||||
PkiSubscriberStatus,
|
PkiSubscriberStatus,
|
||||||
TCreatePkiSubscriberDTO,
|
TCreatePkiSubscriberDTO,
|
||||||
@@ -49,22 +51,29 @@ import {
|
|||||||
TGetPkiSubscriberDTO,
|
TGetPkiSubscriberDTO,
|
||||||
TIssuePkiSubscriberCertDTO,
|
TIssuePkiSubscriberCertDTO,
|
||||||
TListPkiSubscriberCertsDTO,
|
TListPkiSubscriberCertsDTO,
|
||||||
|
TOrderPkiSubscriberCertDTO,
|
||||||
TSignPkiSubscriberCertDTO,
|
TSignPkiSubscriberCertDTO,
|
||||||
TUpdatePkiSubscriberDTO
|
TUpdatePkiSubscriberDTO
|
||||||
} from "./pki-subscriber-types";
|
} from "./pki-subscriber-types";
|
||||||
|
|
||||||
type TPkiSubscriberServiceFactoryDep = {
|
type TPkiSubscriberServiceFactoryDep = {
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||||
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
|
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||||
pkiSubscriberDAL: Pick<
|
pkiSubscriberDAL: Pick<
|
||||||
TPkiSubscriberDALFactory,
|
TPkiSubscriberDALFactory,
|
||||||
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
|
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
|
||||||
>;
|
>;
|
||||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
certificateAuthorityDAL: Pick<
|
||||||
|
TCertificateAuthorityDALFactory,
|
||||||
|
"findByIdWithAssociatedCa" | "findById" | "transaction" | "create" | "updateById" | "findWithAssociatedCa"
|
||||||
|
>;
|
||||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
|
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
|
||||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
|
||||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||||
|
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
@@ -79,12 +88,39 @@ export const pkiSubscriberServiceFactory = ({
|
|||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
certificateAuthorityCrlDAL,
|
certificateAuthorityCrlDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
certificateBodyDAL,
|
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
externalCertificateAuthorityDAL
|
||||||
}: TPkiSubscriberServiceFactoryDep) => {
|
}: TPkiSubscriberServiceFactoryDep) => {
|
||||||
|
const internalCaFns = InternalCertificateAuthorityFns({
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
certificateAuthorityCertDAL,
|
||||||
|
certificateAuthoritySecretDAL,
|
||||||
|
certificateAuthorityCrlDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const acmeCaFns = AcmeCertificateAuthorityFns({
|
||||||
|
appConnectionDAL,
|
||||||
|
appConnectionService,
|
||||||
|
certificateAuthorityDAL,
|
||||||
|
externalCertificateAuthorityDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateBodyDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectDAL
|
||||||
|
});
|
||||||
|
|
||||||
const createSubscriber = async ({
|
const createSubscriber = async ({
|
||||||
name,
|
name,
|
||||||
commonName,
|
commonName,
|
||||||
@@ -252,28 +288,26 @@ export const pkiSubscriberServiceFactory = ({
|
|||||||
return subscriber;
|
return subscriber;
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueSubscriberCert = async ({
|
const orderSubscriberCert = async ({
|
||||||
subscriberName,
|
subscriberName,
|
||||||
projectId,
|
projectId,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
}: TIssuePkiSubscriberCertDTO) => {
|
}: TOrderPkiSubscriberCertDTO) => {
|
||||||
const subscriber = await pkiSubscriberDAL.findOne({
|
const subscriber = await pkiSubscriberDAL.findOne({
|
||||||
name: subscriberName,
|
name: subscriberName,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||||
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||||
|
|
||||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
|
||||||
if (!ca?.internalCa) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
|
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: ca.projectId,
|
projectId: subscriber.projectId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.CertificateManager
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
@@ -288,201 +322,74 @@ export const pkiSubscriberServiceFactory = ({
|
|||||||
|
|
||||||
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||||
throw new BadRequestError({ message: "Subscriber is not active" });
|
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||||
if (ca.internalCa?.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
|
||||||
if (!ca.internalCa?.activeCaCertId)
|
|
||||||
throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
|
||||||
if (ca.disableDirectIssuance) {
|
|
||||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
|
||||||
}
|
|
||||||
const caCert = await certificateAuthorityCertDAL.findById(ca.internalCa.activeCaCertId);
|
|
||||||
|
|
||||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||||
projectId: ca.projectId,
|
if (ca.internalCa?.id) {
|
||||||
projectDAL,
|
throw new BadRequestError({ message: "CA does not support ordering certificates" });
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
|
||||||
kmsId: certificateManagerKmsId
|
|
||||||
});
|
|
||||||
|
|
||||||
const decryptedCaCert = await kmsDecryptor({
|
|
||||||
cipherTextBlob: caCert.encryptedCertificate
|
|
||||||
});
|
|
||||||
|
|
||||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
|
||||||
const notBeforeDate = new Date();
|
|
||||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
|
||||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
|
||||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
|
||||||
|
|
||||||
// check not before constraint
|
|
||||||
if (notBeforeDate < caCertNotBeforeDate) {
|
|
||||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check not after constraint
|
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
|
||||||
if (notAfterDate > caCertNotAfterDate) {
|
return acmeCaFns.orderCertificate(subscriber, ca, {
|
||||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
type: actor,
|
||||||
}
|
id: actorId,
|
||||||
|
authMethod: actorAuthMethod,
|
||||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
orgId: actorOrgId
|
||||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
|
||||||
|
|
||||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
|
||||||
name: `CN=${subscriber.commonName}`,
|
|
||||||
keys: leafKeys,
|
|
||||||
signingAlgorithm: alg,
|
|
||||||
extensions: [
|
|
||||||
// eslint-disable-next-line no-bitwise
|
|
||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
|
||||||
],
|
|
||||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
|
||||||
});
|
|
||||||
|
|
||||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
|
||||||
caId: ca.id,
|
|
||||||
certificateAuthorityDAL,
|
|
||||||
certificateAuthoritySecretDAL,
|
|
||||||
projectDAL,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
|
||||||
const appCfg = getConfig();
|
|
||||||
|
|
||||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
|
||||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
|
||||||
|
|
||||||
const extensions: x509.Extension[] = [
|
|
||||||
new x509.BasicConstraintsExtension(false),
|
|
||||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
|
||||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
|
||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
|
||||||
new x509.AuthorityInfoAccessExtension({
|
|
||||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
|
||||||
}),
|
|
||||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
|
||||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
|
||||||
if (keyUsagesBitValue) {
|
|
||||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscriber.extendedKeyUsages.length) {
|
|
||||||
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
|
|
||||||
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
extensions.push(extendedKeyUsagesExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
|
||||||
|
|
||||||
if (subscriber.subjectAlternativeNames?.length) {
|
|
||||||
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
|
||||||
if (z.string().email().safeParse(altName).success) {
|
|
||||||
return { type: "email", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
|
||||||
return { type: "dns", value: altName };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
|
||||||
extensions.push(altNamesExtension);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialNumber = createSerialNumber();
|
throw new BadRequestError({ message: "Unsupported CA type" });
|
||||||
const leafCert = await x509.X509CertificateGenerator.create({
|
};
|
||||||
serialNumber,
|
|
||||||
subject: csrObj.subject,
|
const issueSubscriberCert = async ({
|
||||||
issuer: caCertObj.subject,
|
subscriberName,
|
||||||
notBefore: notBeforeDate,
|
projectId,
|
||||||
notAfter: notAfterDate,
|
actorId,
|
||||||
signingKey: caPrivateKey,
|
actorAuthMethod,
|
||||||
publicKey: csrObj.publicKey,
|
actor,
|
||||||
signingAlgorithm: alg,
|
actorOrgId
|
||||||
extensions
|
}: TIssuePkiSubscriberCertDTO) => {
|
||||||
|
const subscriber = await pkiSubscriberDAL.findOne({
|
||||||
|
name: subscriberName,
|
||||||
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||||
|
|
||||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
kmsId: certificateManagerKmsId
|
actor,
|
||||||
});
|
actorId,
|
||||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
projectId: subscriber.projectId,
|
||||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
actorAuthMethod,
|
||||||
});
|
actorOrgId,
|
||||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
plainText: Buffer.from(skLeaf)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
caCertId: caCert.id,
|
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||||
certificateAuthorityDAL,
|
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||||
certificateAuthorityCertDAL,
|
name: subscriber.name
|
||||||
projectDAL,
|
})
|
||||||
kmsService
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||||
|
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||||
|
|
||||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
|
||||||
plainText: Buffer.from(certificateChainPem)
|
if (ca.internalCa?.id) {
|
||||||
});
|
return internalCaFns.issueCertificate(subscriber, ca);
|
||||||
|
}
|
||||||
|
|
||||||
await certificateDAL.transaction(async (tx) => {
|
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
|
||||||
const cert = await certificateDAL.create(
|
return acmeCaFns.orderCertificate(subscriber, ca, {
|
||||||
{
|
type: actor,
|
||||||
caId: ca.id,
|
id: actorId,
|
||||||
caCertId: caCert.id,
|
authMethod: actorAuthMethod,
|
||||||
pkiSubscriberId: subscriber.id,
|
orgId: actorOrgId
|
||||||
status: CertStatus.ACTIVE,
|
});
|
||||||
friendlyName: subscriber.commonName,
|
}
|
||||||
commonName: subscriber.commonName,
|
|
||||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
|
||||||
serialNumber,
|
|
||||||
notBefore: notBeforeDate,
|
|
||||||
notAfter: notAfterDate,
|
|
||||||
keyUsages: selectedKeyUsages,
|
|
||||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
await certificateBodyDAL.create(
|
throw new BadRequestError({ message: "CA does not support immediate issuance of certificates" });
|
||||||
{
|
|
||||||
certId: cert.id,
|
|
||||||
encryptedCertificate,
|
|
||||||
encryptedCertificateChain
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
await certificateSecretDAL.create(
|
|
||||||
{
|
|
||||||
certId: cert.id,
|
|
||||||
encryptedPrivateKey
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
certificate: leafCert.toString("pem"),
|
|
||||||
certificateChain: certificateChainPem,
|
|
||||||
issuingCaCertificate,
|
|
||||||
privateKey: skLeaf,
|
|
||||||
serialNumber,
|
|
||||||
ca,
|
|
||||||
subscriber
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const signSubscriberCert = async ({
|
const signSubscriberCert = async ({
|
||||||
@@ -803,6 +710,7 @@ export const pkiSubscriberServiceFactory = ({
|
|||||||
deleteSubscriber,
|
deleteSubscriber,
|
||||||
issueSubscriberCert,
|
issueSubscriberCert,
|
||||||
signSubscriberCert,
|
signSubscriberCert,
|
||||||
listSubscriberCerts
|
listSubscriberCerts,
|
||||||
|
orderSubscriberCert
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export type TIssuePkiSubscriberCertDTO = {
|
|||||||
subscriberName: string;
|
subscriberName: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TOrderPkiSubscriberCertDTO = {
|
||||||
|
subscriberName: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TSignPkiSubscriberCertDTO = {
|
export type TSignPkiSubscriberCertDTO = {
|
||||||
subscriberName: string;
|
subscriberName: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
|
|||||||
@@ -918,6 +918,7 @@ export const projectServiceFactory = ({
|
|||||||
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
|
const cas = await certificateAuthorityDAL.findWithAssociatedCa(
|
||||||
{
|
{
|
||||||
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId,
|
||||||
|
$notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"],
|
||||||
...(status && { [`${TableName.InternalCertificateAuthority}.status` as "status"]: status }),
|
...(status && { [`${TableName.InternalCertificateAuthority}.status` as "status"]: status }),
|
||||||
...(friendlyName && {
|
...(friendlyName && {
|
||||||
[`${TableName.InternalCertificateAuthority}.friendlyName` as "friendlyName"]: friendlyName
|
[`${TableName.InternalCertificateAuthority}.friendlyName` as "friendlyName"]: friendlyName
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { SshCaStatus } from "../sshCa";
|
import { SshCaStatus } from "../sshCa";
|
||||||
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
|
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
|
||||||
import { CaStatus, CaType } from "./enums";
|
import { CaStatus, InternalCaType } from "./enums";
|
||||||
|
|
||||||
export const caTypeToNameMap: { [K in CaType]: string } = {
|
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
|
||||||
[CaType.ROOT]: "Root",
|
[InternalCaType.ROOT]: "Root",
|
||||||
[CaType.INTERMEDIATE]: "Intermediate"
|
[InternalCaType.INTERMEDIATE]: "Intermediate"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export enum CaType {
|
export enum CaType {
|
||||||
|
INTERNAL = "internal",
|
||||||
|
ACME = "acme"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum InternalCaType {
|
||||||
ROOT = "root",
|
ROOT = "root",
|
||||||
INTERMEDIATE = "intermediate"
|
INTERMEDIATE = "intermediate"
|
||||||
}
|
}
|
||||||
@@ -12,3 +17,7 @@ export enum CaStatus {
|
|||||||
export enum CaRenewalType {
|
export enum CaRenewalType {
|
||||||
EXISTING = "existing"
|
EXISTING = "existing"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AcmeDnsProvider {
|
||||||
|
ROUTE53 = "route53"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export { CaRenewalType, CaStatus, CaType } from "./enums";
|
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||||
export {
|
export {
|
||||||
useCreateCa,
|
useCreateCa,
|
||||||
useCreateCertificate,
|
useCreateCertificate,
|
||||||
|
useCreateUnifiedCa,
|
||||||
useDeleteCa,
|
useDeleteCa,
|
||||||
useImportCaCertificate,
|
useImportCaCertificate,
|
||||||
useRenewCa,
|
useRenewCa,
|
||||||
@@ -10,9 +11,12 @@ export {
|
|||||||
} from "./mutations";
|
} from "./mutations";
|
||||||
export {
|
export {
|
||||||
useGetCaById,
|
useGetCaById,
|
||||||
|
useGetCaByTypeAndId,
|
||||||
useGetCaCert,
|
useGetCaCert,
|
||||||
useGetCaCerts,
|
useGetCaCerts,
|
||||||
useGetCaCertTemplates,
|
useGetCaCertTemplates,
|
||||||
useGetCaCrls,
|
useGetCaCrls,
|
||||||
useGetCaCsr
|
useGetCaCsr,
|
||||||
|
useListCasByProjectId,
|
||||||
|
useListCasByTypeAndProjectId
|
||||||
} from "./queries";
|
} from "./queries";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
TCreateCaDTO,
|
TCreateCaDTO,
|
||||||
TCreateCertificateDTO,
|
TCreateCertificateDTO,
|
||||||
TCreateCertificateResponse,
|
TCreateCertificateResponse,
|
||||||
|
TCreateUnifiedCertificateAuthorityDTO,
|
||||||
TDeleteCaDTO,
|
TDeleteCaDTO,
|
||||||
TImportCaCertificateDTO,
|
TImportCaCertificateDTO,
|
||||||
TImportCaCertificateResponse,
|
TImportCaCertificateResponse,
|
||||||
@@ -16,9 +17,28 @@ import {
|
|||||||
TRenewCaResponse,
|
TRenewCaResponse,
|
||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
TSignIntermediateResponse,
|
TSignIntermediateResponse,
|
||||||
|
TUnifiedCertificateAuthority,
|
||||||
TUpdateCaDTO
|
TUpdateCaDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateUnifiedCa = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TUnifiedCertificateAuthority, object, TCreateUnifiedCertificateAuthorityDTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const { data } = await apiRequest.post<TUnifiedCertificateAuthority>(
|
||||||
|
`/api/v1/pki/ca/${body.type}`,
|
||||||
|
body
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
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>({
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||||
import { TCertificateAuthority } from "./types";
|
import { CaType } from "./enums";
|
||||||
|
import { TCertificateAuthority, TUnifiedCertificateAuthority } from "./types";
|
||||||
|
|
||||||
export const caKeys = {
|
export const caKeys = {
|
||||||
getCaById: (caId: string) => [{ caId }, "ca"],
|
getCaById: (caId: string) => [{ caId }, "ca"],
|
||||||
|
getCaByTypeAndId: (type: CaType, caId: string) => [{ type, caId }, "ca"],
|
||||||
|
listCasByTypeAndProjectId: (type: CaType, projectId: string) => [{ type, projectId }, "cas"],
|
||||||
|
listCasByProjectId: (projectId: string) => [{ projectId }, "cas"],
|
||||||
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
|
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
|
||||||
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
|
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
|
||||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||||
@@ -16,6 +20,47 @@ export const caKeys = {
|
|||||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetCaByTypeAndId = (type: CaType, caId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: caKeys.getCaByTypeAndId(type, caId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { certificateAuthority }
|
||||||
|
} = await apiRequest.get<{ certificateAuthority: TUnifiedCertificateAuthority }>(
|
||||||
|
`/api/v1/pki/ca/${type}/${caId}`
|
||||||
|
);
|
||||||
|
return certificateAuthority;
|
||||||
|
},
|
||||||
|
enabled: Boolean(caId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListCasByTypeAndProjectId = (type: CaType, projectId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateAuthorities: TUnifiedCertificateAuthority[];
|
||||||
|
}>(`/api/v1/pki/ca/${type}?projectId=${projectId}`);
|
||||||
|
|
||||||
|
return data.certificateAuthorities;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListCasByProjectId = (projectId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: caKeys.listCasByProjectId(projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateAuthorities: TUnifiedCertificateAuthority[];
|
||||||
|
}>(`/api/v2/pki/ca?projectId=${projectId}`);
|
||||||
|
|
||||||
|
return data.certificateAuthorities;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetCaById = (caId: string) => {
|
export const useGetCaById = (caId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: caKeys.getCaById(caId),
|
queryKey: caKeys.getCaById(caId),
|
||||||
|
|||||||
@@ -1,11 +1,59 @@
|
|||||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
|
||||||
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
import { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||||
|
|
||||||
|
export type TAcmeCertificateAuthority = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
type: CaType.ACME;
|
||||||
|
status: CaStatus;
|
||||||
|
name: string;
|
||||||
|
disableDirectIssuance: boolean;
|
||||||
|
configuration: {
|
||||||
|
dnsAppConnectionId: string;
|
||||||
|
dnsProvider: AcmeDnsProvider;
|
||||||
|
directoryUrl: string;
|
||||||
|
accountEmail: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TInternalCertificateAuthority = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
type: CaType.INTERNAL;
|
||||||
|
status: CaStatus;
|
||||||
|
name: string;
|
||||||
|
disableDirectIssuance: boolean;
|
||||||
|
configuration: {
|
||||||
|
type: InternalCaType;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName: string;
|
||||||
|
organization: string;
|
||||||
|
ou: string;
|
||||||
|
country: string;
|
||||||
|
province: string;
|
||||||
|
locality: string;
|
||||||
|
maxPathLength: number;
|
||||||
|
keyAlgorithm: CertKeyAlgorithm;
|
||||||
|
notAfter?: string;
|
||||||
|
notBefore?: string;
|
||||||
|
dn: string;
|
||||||
|
parentCaId?: string;
|
||||||
|
serialNumber: string;
|
||||||
|
activeCaCertId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUnifiedCertificateAuthority =
|
||||||
|
| TAcmeCertificateAuthority
|
||||||
|
| TInternalCertificateAuthority;
|
||||||
|
|
||||||
|
export type TCreateUnifiedCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
|
||||||
|
|
||||||
export type TCertificateAuthority = {
|
export type TCertificateAuthority = {
|
||||||
id: string;
|
id: string;
|
||||||
parentCaId?: string;
|
parentCaId?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
type: CaType;
|
type: InternalCaType;
|
||||||
status: CaStatus;
|
status: CaStatus;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
|
|||||||
+3
-3
@@ -6,7 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
|||||||
import { Button, IconButton, Tooltip } from "@app/components/v2";
|
import { Button, IconButton, Tooltip } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { CaStatus, CaType, useGetCaById } from "@app/hooks/api";
|
import { CaStatus, InternalCaType, useGetCaById } from "@app/hooks/api";
|
||||||
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
|
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -82,7 +82,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
{ca.type === InternalCaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
|
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
|
||||||
<div className="group flex align-top">
|
<div className="group flex align-top">
|
||||||
@@ -156,7 +156,7 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
|
|||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (ca.type === CaType.INTERMEDIATE && !ca.parentCaId) {
|
if (ca.type === InternalCaType.INTERMEDIATE && !ca.parentCaId) {
|
||||||
// intermediate CA with external parent CA
|
// intermediate CA with external parent CA
|
||||||
handlePopUpOpen("installCaCert", {
|
handlePopUpOpen("installCaCert", {
|
||||||
caId,
|
caId,
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
|
|||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
import { ExternalCaSection } from "./components/ExternalCaSection";
|
||||||
import { CaSection } from "./components";
|
import { CaSection } from "./components";
|
||||||
|
|
||||||
export const CertificateAuthoritiesPage = () => {
|
export const CertificateAuthoritiesPage = () => {
|
||||||
@@ -25,6 +26,7 @@ export const CertificateAuthoritiesPage = () => {
|
|||||||
a={ProjectPermissionSub.CertificateAuthorities}
|
a={ProjectPermissionSub.CertificateAuthorities}
|
||||||
>
|
>
|
||||||
<CaSection />
|
<CaSection />
|
||||||
|
<ExternalCaSection />
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
// DatePicker
|
// DatePicker
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { CaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
import { InternalCaType, useCreateCa, useGetCaById, useUpdateCa } from "@app/hooks/api/ca";
|
||||||
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
|
||||||
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -35,7 +35,7 @@ const getDateTenYearsFromToday = () => {
|
|||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]),
|
type: z.enum([InternalCaType.ROOT, InternalCaType.INTERMEDIATE]),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
organization: z.string(),
|
organization: z.string(),
|
||||||
ou: z.string(),
|
ou: z.string(),
|
||||||
@@ -63,8 +63,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const caTypes = [
|
const caTypes = [
|
||||||
{ label: "Root", value: CaType.ROOT },
|
{ label: "Root", value: InternalCaType.ROOT },
|
||||||
{ label: "Intermediate", value: CaType.INTERMEDIATE }
|
{ label: "Intermediate", value: InternalCaType.INTERMEDIATE }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
@@ -85,7 +85,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: CaType.ROOT,
|
type: InternalCaType.ROOT,
|
||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
organization: "",
|
organization: "",
|
||||||
ou: "",
|
ou: "",
|
||||||
@@ -119,7 +119,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
type: CaType.ROOT,
|
type: InternalCaType.ROOT,
|
||||||
friendlyName: "",
|
friendlyName: "",
|
||||||
organization: "",
|
organization: "",
|
||||||
ou: "",
|
ou: "",
|
||||||
@@ -212,7 +212,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="type"
|
name="type"
|
||||||
defaultValue={CaType.ROOT}
|
defaultValue={InternalCaType.ROOT}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
<FormControl label="CA Type" errorText={error?.message} isError={Boolean(error)}>
|
||||||
<Select
|
<Select
|
||||||
@@ -231,7 +231,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{caType === CaType.ROOT && (
|
{caType === InternalCaType.ROOT && (
|
||||||
<>
|
<>
|
||||||
{/* <Controller
|
{/* <Controller
|
||||||
name="notAfter"
|
name="notAfter"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const CaSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Authorities</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Internal Certificate Authorities</p>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.CertificateAuthorities}
|
a={ProjectPermissionSub.CertificateAuthorities}
|
||||||
|
|||||||
+350
@@ -0,0 +1,350 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
|
||||||
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
import {
|
||||||
|
AcmeDnsProvider,
|
||||||
|
CaStatus,
|
||||||
|
CaType,
|
||||||
|
useCreateUnifiedCa,
|
||||||
|
useGetCaById,
|
||||||
|
useUpdateCa
|
||||||
|
} from "@app/hooks/api/ca";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
type: z.enum([CaType.ACME]),
|
||||||
|
name: z.string(),
|
||||||
|
disableDirectIssuance: z.boolean(),
|
||||||
|
status: z.enum([CaStatus.ACTIVE, CaStatus.DISABLED]),
|
||||||
|
configuration: z.object({
|
||||||
|
dnsAppConnection: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
}),
|
||||||
|
dnsProvider: z.nativeEnum(AcmeDnsProvider),
|
||||||
|
directoryUrl: z.string(),
|
||||||
|
accountEmail: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["ca"]>;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const caTypes = [{ label: "ACME", value: CaType.ACME }];
|
||||||
|
|
||||||
|
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const { data: ca } = useGetCaById((popUp?.ca?.data as { caId: string })?.caId || "");
|
||||||
|
|
||||||
|
// SHEEN TODO: finish up CA management
|
||||||
|
const { mutateAsync: createMutateAsync } = useCreateUnifiedCa();
|
||||||
|
const { mutateAsync: updateMutateAsync } = useUpdateCa();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
watch
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
type: CaType.ACME,
|
||||||
|
name: "",
|
||||||
|
status: CaStatus.ACTIVE,
|
||||||
|
disableDirectIssuance: false,
|
||||||
|
configuration: {
|
||||||
|
dnsAppConnection: {
|
||||||
|
id: "",
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
dnsProvider: AcmeDnsProvider.ROUTE53,
|
||||||
|
directoryUrl: "",
|
||||||
|
accountEmail: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const caType = watch("type");
|
||||||
|
const dnsProvider = watch("configuration.dnsProvider");
|
||||||
|
|
||||||
|
const { data: availableConnections, isPending } = useListAvailableAppConnections(
|
||||||
|
AppConnection.AWS,
|
||||||
|
{
|
||||||
|
enabled: dnsProvider === AcmeDnsProvider.ROUTE53
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ca) {
|
||||||
|
// reset({
|
||||||
|
// type: ca.type,
|
||||||
|
// name: ca.name,
|
||||||
|
// disableDirectIssuance: ca.disableDirectIssuance
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
reset({
|
||||||
|
type: CaType.ACME,
|
||||||
|
name: "",
|
||||||
|
status: CaStatus.ACTIVE,
|
||||||
|
disableDirectIssuance: false,
|
||||||
|
configuration: {
|
||||||
|
dnsAppConnection: {
|
||||||
|
id: "",
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
dnsProvider: AcmeDnsProvider.ROUTE53,
|
||||||
|
directoryUrl: "",
|
||||||
|
accountEmail: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ca]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
disableDirectIssuance,
|
||||||
|
status,
|
||||||
|
configuration
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
|
if (ca) {
|
||||||
|
// update
|
||||||
|
// await updateMutateAsync({
|
||||||
|
// projectSlug: currentWorkspace.slug,
|
||||||
|
// caId: ca.id,
|
||||||
|
// name,
|
||||||
|
// disableDirectIssuance,
|
||||||
|
// status
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
// create
|
||||||
|
await createMutateAsync({
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
disableDirectIssuance,
|
||||||
|
configuration: {
|
||||||
|
...configuration,
|
||||||
|
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("ca", false);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${ca ? "updated" : "created"} CA`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create CA",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.ca?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("ca", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`${ca ? "View" : "Create"} External CA`}>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
{ca && (
|
||||||
|
<FormControl label="CA ID">
|
||||||
|
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="type"
|
||||||
|
defaultValue={CaType.ACME}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={Boolean(ca)}
|
||||||
|
>
|
||||||
|
{caTypes.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="my-external-ca" isDisabled={Boolean(ca)} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{caType === CaType.ACME && (
|
||||||
|
<>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="configuration.dnsProvider"
|
||||||
|
defaultValue={AcmeDnsProvider.ROUTE53}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="DNS Provider"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={Boolean(ca)}
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={String(AcmeDnsProvider.ROUTE53)}
|
||||||
|
key={AcmeDnsProvider.ROUTE53}
|
||||||
|
>
|
||||||
|
Route53
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText={`${dnsProvider === AcmeDnsProvider.ROUTE53 ? "Route53" : ""} requires an AWS App Connection. This can be created from the Organization Settings page.`}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="DNS App Connection"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
isLoading={isPending}
|
||||||
|
options={availableConnections}
|
||||||
|
placeholder="Select connection..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="configuration.dnsAppConnection"
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="configuration.directoryUrl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Directory URL"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="configuration.accountEmail"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Account Email"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="[email protected]" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="disableDirectIssuance"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||||
|
<Switch
|
||||||
|
id="is-active"
|
||||||
|
onCheckedChange={(value) => field.onChange(value)}
|
||||||
|
isChecked={field.value}
|
||||||
|
>
|
||||||
|
<p className="w-full">Disable Direct Issuance</p>
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{popUp?.ca?.data ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("ca", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { CaStatus, useDeleteCa, useUpdateCa } from "@app/hooks/api";
|
||||||
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { ExternalCaModal } from "./ExternalCaModal";
|
||||||
|
import { ExternalCaTable } from "./ExternalCaTable";
|
||||||
|
|
||||||
|
export const ExternalCaSection = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { mutateAsync: deleteCa } = useDeleteCa();
|
||||||
|
const { mutateAsync: updateCa } = useUpdateCa();
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"ca",
|
||||||
|
"deleteCa",
|
||||||
|
"caStatus", // enable / disable
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const onRemoveCaSubmit = async (caId: string) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
|
await deleteCa({ caId, projectSlug: currentWorkspace.slug });
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted CA",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteCa");
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete CA",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateCaStatus = async ({ caId, status }: { caId: string; status: CaStatus }) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.slug) return;
|
||||||
|
|
||||||
|
await updateCa({ caId, projectSlug: currentWorkspace.slug, status });
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("caStatus");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">External Certificate Authorities</p>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.CertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("ca")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create CA
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<ExternalCaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<ExternalCaTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteCa.isOpen}
|
||||||
|
title={`Are you sure want to remove the CA ${
|
||||||
|
(popUp?.deleteCa?.data as { dn: string })?.dn || ""
|
||||||
|
} from the project?`}
|
||||||
|
subTitle="This action will delete other CAs and certificates below it in your CA hierarchy."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteCa", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => onRemoveCaSubmit((popUp?.deleteCa?.data as { caId: string })?.caId)}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.caStatus.isOpen}
|
||||||
|
title={`Are you sure want to ${
|
||||||
|
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||||
|
? "enable"
|
||||||
|
: "disable"
|
||||||
|
} the CA ${(popUp?.caStatus?.data as { dn: string })?.dn || ""} from the project?`}
|
||||||
|
subTitle={
|
||||||
|
(popUp?.caStatus?.data as { status: string })?.status === CaStatus.ACTIVE
|
||||||
|
? "This action will allow the CA to start issuing certificates again."
|
||||||
|
: "This action will prevent the CA from issuing new certificates."
|
||||||
|
}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("caStatus", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onUpdateCaStatus(popUp?.caStatus?.data as { caId: string; status: CaStatus })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
|
||||||
|
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["ca", "deleteCa", "caStatus", "upgradePlan"]>,
|
||||||
|
data?: {
|
||||||
|
caId?: string;
|
||||||
|
dn?: string;
|
||||||
|
status?: CaStatus;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Type</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={3} innerKey="project-cas" />}
|
||||||
|
{!isPending &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map((ca) => {
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
key={`ca-${ca.id}`}
|
||||||
|
onClick={() =>
|
||||||
|
navigate({
|
||||||
|
to: `/${ProjectType.CertificateManager}/$projectId/ca/$caId` as const,
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
caId: ca.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Td>{ca.name}</Td>
|
||||||
|
<Td>{ca.type}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={getCaStatusBadgeVariant(ca.status)}>
|
||||||
|
{caStatusToNameMap[ca.status]}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td className="flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
{(ca.status === CaStatus.ACTIVE || ca.status === CaStatus.DISABLED) && (
|
||||||
|
<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("caStatus", {
|
||||||
|
caId: ca.id,
|
||||||
|
status:
|
||||||
|
ca.status === CaStatus.ACTIVE
|
||||||
|
? CaStatus.DISABLED
|
||||||
|
: CaStatus.ACTIVE
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faBan} />}
|
||||||
|
>
|
||||||
|
{`${ca.status === CaStatus.ACTIVE ? "Disable" : "Enable"} CA`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.CertificateAuthorities}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// handlePopUpOpen("deleteCa", {
|
||||||
|
// caId: ca.id,
|
||||||
|
// dn: ca.dn
|
||||||
|
// });
|
||||||
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete CA
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending && data?.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title="No external certificate authorities have been created"
|
||||||
|
icon={faCertificate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+13
-11
@@ -20,10 +20,10 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
CaStatus,
|
CaType,
|
||||||
useCreatePkiSubscriber,
|
useCreatePkiSubscriber,
|
||||||
useGetPkiSubscriber,
|
useGetPkiSubscriber,
|
||||||
useListWorkspaceCas,
|
useListCasByProjectId,
|
||||||
useListWorkspacePkiSubscribers,
|
useListWorkspacePkiSubscribers,
|
||||||
useUpdatePkiSubscriber
|
useUpdatePkiSubscriber
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -74,10 +74,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const projectId = currentWorkspace.id;
|
const projectId = currentWorkspace.id;
|
||||||
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
|
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
|
||||||
const { data: cas } = useListWorkspaceCas({
|
const { data: cas } = useListCasByProjectId(projectId);
|
||||||
projectSlug: currentWorkspace?.slug ?? "",
|
|
||||||
status: CaStatus.ACTIVE
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
const { data: pkiSubscriber } = useGetPkiSubscriber({
|
||||||
subscriberName:
|
subscriberName:
|
||||||
@@ -276,11 +273,16 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
onValueChange={(e) => onChange(e)}
|
onValueChange={(e) => onChange(e)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{(cas || []).map(({ id, dn }) => (
|
{(cas || []).map(({ id, name, type, configuration }) => {
|
||||||
<SelectItem value={id} key={`ca-${id}`}>
|
const displayName =
|
||||||
{dn}
|
type === CaType.INTERNAL ? `${name} (${configuration.dn})` : name;
|
||||||
</SelectItem>
|
|
||||||
))}
|
return (
|
||||||
|
<SelectItem value={id} key={`ca-${id}`}>
|
||||||
|
{displayName}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user