diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index e6faf12f7..74c7a7072 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -20,6 +20,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { ActorType } from "@app/services/auth/auth-type"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; @@ -31,7 +32,6 @@ import { TUpdateSecretSyncDTO } from "@app/services/secret-sync/secret-sync-types"; import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types"; -import { CertKeyUsage, CertExtendedKeyUsage } from "@app/services/certificate/certificate-types"; import { KmipPermission } from "../kmip/kmip-enum"; import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types"; @@ -242,6 +242,8 @@ export enum EventType { UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber", DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber", GET_PKI_SUBSCRIBER = "get-pki-subscriber", + ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert", + SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert", CREATE_KMS = "create-kms", UPDATE_KMS = "update-kms", DELETE_KMS = "delete-kms", @@ -1946,6 +1948,22 @@ interface GetPkiSubscriber { }; } +interface IssuePkiSubscriberCert { + type: EventType.ISSUE_PKI_SUBSCRIBER_CERT; + metadata: { + subscriberId: string; + serialNumber: string; + }; +} + +interface SignPkiSubscriberCert { + type: EventType.SIGN_PKI_SUBSCRIBER_CERT; + metadata: { + subscriberId: string; + serialNumber: string; + }; +} + interface CreateKmsEvent { type: EventType.CREATE_KMS; metadata: { @@ -2908,6 +2926,8 @@ export type Event = | UpdatePkiSubscriber | DeletePkiSubscriber | GetPkiSubscriber + | IssuePkiSubscriberCert + | SignPkiSubscriberCert | CreateKmsEvent | UpdateKmsEvent | DeleteKmsEvent diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index b7ae6f7ee..04bbcec2d 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -54,7 +54,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ projectTemplates: false, kmip: false, gateway: false, - sshHostGroups: false + sshHostGroups: true }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts index 751116895..97fea1ce8 100644 --- a/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts +++ b/backend/src/ee/services/ssh-host-group/ssh-host-group-service.ts @@ -179,13 +179,41 @@ export const sshHostGroupServiceFactory = ({ }); const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => { - await sshHostGroupDAL.updateById( - sshHostGroupId, - { - name - }, - tx - ); + if (name) { + // (dangtony98): room to optimize check to ensure that + // the SSH host group name is unique across the whole org + const project = await projectDAL.findById(sshHostGroup.projectId, tx); + if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` }); + const projects = await projectDAL.find( + { + orgId: project.orgId + }, + { tx } + ); + + const existingSshHostGroup = await sshHostGroupDAL.find( + { + name, + $in: { + projectId: projects.map((p) => p.id) + } + }, + { tx } + ); + + if (existingSshHostGroup.length) { + throw new BadRequestError({ + message: `SSH host group with name '${name}' already exists in the organization` + }); + } + await sshHostGroupDAL.updateById( + sshHostGroupId, + { + name + }, + tx + ); + } if (loginMappings) { await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx); if (loginMappings.length) { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bc5969bf1..f3cac5ab8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -954,6 +954,15 @@ export const registerRoutes = async ( const pkiSubscriberService = pkiSubscriberServiceFactory({ pkiSubscriberDAL, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, + certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + projectDAL, + kmsService, permissionService }); diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts index fcd152381..02a1bc681 100644 --- a/backend/src/server/routes/v1/pki-subscriber-router.ts +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -5,11 +5,13 @@ import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { validateAltNameField } from "@app/services/certificate-authority/certificate-authority-validators"; import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => { server.route({ @@ -31,7 +33,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const subscriber = await server.services.pkiSubscriber.getPkiSubscriberById({ + const subscriber = await server.services.pkiSubscriber.getSubscriberById({ subscriberId: req.params.subscriberId, actor: req.permission.type, actorId: req.permission.id, @@ -103,7 +105,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const subscriber = await server.services.pkiSubscriber.createPkiSubscriber({ + const subscriber = await server.services.pkiSubscriber.createSubscriber({ ...req.body, actor: req.permission.type, actorId: req.permission.id, @@ -185,7 +187,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - const subscriber = await server.services.pkiSubscriber.updatePkiSubscriber({ + const subscriber = await server.services.pkiSubscriber.updateSubscriber({ subscriberId: req.params.subscriberId, actor: req.permission.type, actorId: req.permission.id, @@ -235,7 +237,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const subscriber = await server.services.pkiSubscriber.deletePkiSubscriber({ + const subscriber = await server.services.pkiSubscriber.deleteSubscriber({ subscriberId: req.params.subscriberId, actor: req.permission.type, actorId: req.permission.id, @@ -264,7 +266,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { hide: false, tags: [ApiDocsTags.PkiSubscribers], @@ -283,37 +285,43 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - // TODO: reuse issueCertFromCa fn (or not since we are adding support for external CAs?) - // const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } = - // await server.services.pkiSubscriber.issuePkiSubscriberCertificate({ - // subscriberId: req.params.subscriberId, - // actor: req.permission.type, - // actorId: req.permission.id, - // actorAuthMethod: req.permission.authMethod, - // actorOrgId: req.permission.orgId - // }); + const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, subscriber } = + await server.services.pkiSubscriber.issueSubscriberCert({ + subscriberId: req.params.subscriberId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, event: { - type: EventType.ISSUE_SSH_HOST_USER_CERT, + type: EventType.ISSUE_PKI_SUBSCRIBER_CERT, metadata: { - sshHostId: req.params.sshHostId, - hostname: host.hostname, - loginUser: req.body.loginUser, - principals, - ttl: host.userCertTtl + subscriberId: subscriber.id, + serialNumber } } }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.IssueCert, + distinctId: getTelemetryDistinctId(req), + properties: { + subscriberId: subscriber.id, + commonName: subscriber.commonName, + ...req.auditLogInfo + } + }); + return { - serialNumber, - signedKey: signedPublicKey, + certificate, + certificateChain, + issuingCaCertificate, privateKey, - publicKey, - keyAlgorithm + serialNumber }; } }); @@ -324,7 +332,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => config: { rateLimit: writeLimit }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { hide: false, tags: [ApiDocsTags.PkiSubscribers], @@ -333,7 +341,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => subscriberId: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberId) }), body: z.object({ - csr: z.string().trim().min(1). + csr: z.string().trim().min(1) }), response: { 200: z.object({ @@ -345,36 +353,43 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => } }, handler: async (req) => { - // TODO: reuse issueCertFromCa fn (or not since we are adding support for external CAs?) - const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } = - await server.services.pkiSubscriber.issuePkiSubscriberCertificate({ + const { certificate, certificateChain, issuingCaCertificate, serialNumber, subscriber } = + await server.services.pkiSubscriber.signSubscriberCert({ subscriberId: req.params.subscriberId, + csr: req.body.csr, actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId }); - // await server.services.auditLog.createAuditLog({ - // ...req.auditLogInfo, - // orgId: req.permission.orgId, - // event: { - // type: EventType.ISSUE_SSH_HOST_USER_CERT, - // metadata: { - // sshHostId: req.params.sshHostId, - // hostname: host.hostname, - // loginUser: req.body.loginUser, - // principals, - // ttl: host.userCertTtl - // } - // } - // }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.SIGN_PKI_SUBSCRIBER_CERT, + metadata: { + subscriberId: subscriber.id, + serialNumber + } + } + }); + + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.SignCert, + distinctId: getTelemetryDistinctId(req), + properties: { + subscriberId: subscriber.id, + commonName: subscriber.commonName, + ...req.auditLogInfo + } + }); return { - serialNumber, - signedKey: signedPublicKey, - publicKey, - keyAlgorithm + certificate: certificate.toString("pem"), + certificateChain, + issuingCaCertificate, + serialNumber }; } }); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 2b389518b..aaff6b01b 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -1,5 +1,7 @@ +/* eslint-disable no-bitwise */ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; import { ActionProjectType } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; @@ -11,10 +13,26 @@ import { import { getConfig } from "@app/lib/config/env"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; -import { CertKeyAlgorithm } from "@app/services/certificate/certificate-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, + CertExtendedKeyUsageOIDToName, + CertKeyAlgorithm, + CertKeyUsage, + CertStatus +} from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; -import { getCaCredentials, keyAlgorithmToAlgCfg } from "@app/services/certificate-authority/certificate-authority-fns"; +import { + createSerialNumber, + getCaCertChain, + getCaCredentials, + keyAlgorithmToAlgCfg, + parseDistinguishedName +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; @@ -31,32 +49,41 @@ import { } from "./pki-subscriber-types"; type TPkiSubscriberServiceFactoryDep = { - pkiSubscriberDAL: Pick; + pkiSubscriberDAL: Pick< + TPkiSubscriberDALFactory, + "create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" + >; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; + certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; - projectDAL: Pick; - kmsService: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; permissionService: Pick; }; export type TPkiSubscriberServiceFactory = ReturnType; -// TODO: bind subscribers to CA - export const pkiSubscriberServiceFactory = ({ pkiSubscriberDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, + certificateAuthoritySecretDAL, certificateAuthorityCrlDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, projectDAL, kmsService, permissionService }: TPkiSubscriberServiceFactoryDep) => { - const createPkiSubscriber = async ({ + const createSubscriber = async ({ name, commonName, - caId, // (dangtony98) consider by CA name instead (newly-introduced field) + caId, ttl, subjectAlternativeNames, keyUsages, @@ -82,21 +109,55 @@ export const pkiSubscriberServiceFactory = ({ ProjectPermissionSub.PkiSubscribers ); - const newSubscriber = await pkiSubscriberDAL.create({ - caId, - projectId, - name, - commonName, - ttl, - subjectAlternativeNames, - keyUsages, - extendedKeyUsages + const newSubscriber = await pkiSubscriberDAL.transaction(async (tx) => { + // (dangtony98): room to optimize check to ensure that + // the PKI subscriber name is unique across the whole org + const project = await projectDAL.findById(projectId, tx); + if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); + const projects = await projectDAL.find( + { + orgId: project.orgId + }, + { tx } + ); + + const existingPkiSubscriber = await pkiSubscriberDAL.find( + { + name, + $in: { + projectId: projects.map((p) => p.id) + } + }, + { tx } + ); + + if (existingPkiSubscriber.length) { + throw new BadRequestError({ + message: `PKI subscriber with name '${name}' already exists in the organization` + }); + } + + const subscriber = await pkiSubscriberDAL.create( + { + caId, + projectId, + name, + commonName, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages + }, + tx + ); + + return subscriber; }); return newSubscriber; }; - const getPkiSubscriberById = async ({ + const getSubscriberById = async ({ subscriberId, actorId, actorAuthMethod, @@ -124,7 +185,7 @@ export const pkiSubscriberServiceFactory = ({ return subscriber; }; - const updatePkiSubscriber = async ({ + const updateSubscriber = async ({ subscriberId, name, commonName, @@ -138,13 +199,13 @@ export const pkiSubscriberServiceFactory = ({ actor, actorOrgId }: TUpdatePkiSubscriberDTO) => { - const subscriber = await pkiSubscriberDAL.findById(subscriberId); - if (!subscriber) throw new NotFoundError({ message: `PKI subscriber with ID '${subscriberId}' not found` }); + const foundSubscriber = await pkiSubscriberDAL.findById(subscriberId); + if (!foundSubscriber) throw new NotFoundError({ message: `PKI subscriber with ID '${subscriberId}' not found` }); const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId: subscriber.projectId, + projectId: foundSubscriber.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager @@ -156,20 +217,57 @@ export const pkiSubscriberServiceFactory = ({ ProjectPermissionSub.PkiSubscribers ); - const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriberId, { - caId, - name, - commonName, - ttl, - subjectAlternativeNames, - keyUsages, - extendedKeyUsages + const updatedSubscriber = await pkiSubscriberDAL.transaction(async (tx) => { + if (name) { + // (dangtony98): room to optimize check to ensure that + // the PKI subscriber name is unique across the whole org + const project = await projectDAL.findById(foundSubscriber.projectId, tx); + if (!project) throw new NotFoundError({ message: `Project with ID '${foundSubscriber.projectId}' not found` }); + const projects = await projectDAL.find( + { + orgId: project.orgId + }, + { tx } + ); + + const existingPkiSubscriber = await pkiSubscriberDAL.find( + { + name, + $in: { + projectId: projects.map((p) => p.id) + } + }, + { tx } + ); + + if (existingPkiSubscriber.length) { + throw new BadRequestError({ + message: `PKI subscriber with name '${name}' already exists in the organization` + }); + } + } + + const subscriber = await pkiSubscriberDAL.updateById( + subscriberId, + { + caId, + name, + commonName, + ttl, + subjectAlternativeNames, + keyUsages, + extendedKeyUsages + }, + tx + ); + + return subscriber; }); return updatedSubscriber; }; - const deletePkiSubscriber = async ({ + const deleteSubscriber = async ({ subscriberId, actorId, actorAuthMethod, @@ -199,7 +297,7 @@ export const pkiSubscriberServiceFactory = ({ return subscriber; }; - const issuePkiSubscriberCert = async ({ + const issueSubscriberCert = async ({ subscriberId, actorId, actorAuthMethod, @@ -300,15 +398,111 @@ export const pkiSubscriberServiceFactory = ({ }), 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)); + } + + const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[]; + + 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, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages + }, + 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 + }; }; - const signPkiSubscriberCert = async ({ + const signSubscriberCert = async ({ subscriberId, + csr, actorId, actorAuthMethod, actor, actorOrgId }: TSignPkiSubscriberCertDTO) => { + const appCfg = getConfig(); const subscriber = await pkiSubscriberDAL.findById(subscriberId); if (!subscriber) throw new NotFoundError({ message: `PKI subscriber with ID '${subscriberId}' not found` }); const ca = await certificateAuthorityDAL.findById(subscriber.caId); @@ -328,14 +522,237 @@ export const pkiSubscriberServiceFactory = ({ ProjectPermissionPkiSubscriberActions.SignCert, ProjectPermissionSub.PkiSubscribers ); + + if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" }); + if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); + if (ca.requireTemplateForIssuance) { + throw new BadRequestError({ message: "Certificate template is required for issuance" }); + } + const caCert = await certificateAuthorityCertDAL.findById(ca.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.keyAlgorithm as CertKeyAlgorithm); + + const csrObj = new x509.Pkcs10CertificateRequest(csr); + + const dn = parseDistinguishedName(csrObj.subject); + const cn = dn.commonName; + if (cn !== subscriber.commonName) { + throw new BadRequestError({ message: "Common name (CN) in the CSR does not match the subscriber's common name" }); + } + + const { caPrivateKey, caSecret } = await getCaCredentials({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthoritySecretDAL, + projectDAL, + kmsService + }); + + const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); + 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), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), + new x509.CRLDistributionPointsExtension([distributionPointUrl]), + new x509.AuthorityInfoAccessExtension({ + caIssuers: new x509.GeneralName("url", caIssuerUrl) + }), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy + ]; + + // handle key usages + const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension; + let csrKeyUsages: CertKeyUsage[] = []; + if (csrKeyUsageExtension) { + csrKeyUsages = Object.values(CertKeyUsage).filter( + (keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0 + ); + } + + const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[]; + + if (csrKeyUsages.some((keyUsage) => !selectedKeyUsages.includes(keyUsage))) { + throw new BadRequestError({ + message: "Invalid key usage value based on subscriber's specified key usages" + }); + } + + const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0); + if (keyUsagesBitValue) { + extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true)); + } + + // handle extended key usages + const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension; + let csrExtendedKeyUsages: CertExtendedKeyUsage[] = []; + if (csrExtendedKeyUsageExtension) { + csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map( + (ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string] + ); + } + + const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[]; + if (csrExtendedKeyUsages.some((eku) => !selectedExtendedKeyUsages.includes(eku))) { + throw new BadRequestError({ + message: "Invalid extended key usage value based on subscriber's specified extended key usages" + }); + } + + if (selectedExtendedKeyUsages.length) { + extensions.push( + new x509.ExtendedKeyUsageExtension( + selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]), + true + ) + ); + } + + // attempt to read from CSR if altNames is not explicitly provided + let altNamesArray: { + type: "email" | "dns"; + value: string; + }[] = []; + + const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); + if (sanExtension) { + const sanNames = new x509.GeneralNames(sanExtension.value); + + altNamesArray = sanNames.items + .filter((value) => value.type === "email" || value.type === "dns") + .map((name) => ({ + type: name.type as "email" | "dns", + value: name.value + })); + } + + if ( + altNamesArray + .map((altName) => altName.value) + .some((altName) => !subscriber.subjectAlternativeNames.includes(altName)) + ) { + throw new BadRequestError({ + message: "Invalid subject alternative name based on subscriber's specified subject alternative names" + }); + } + + if (altNamesArray.length) { + 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 kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(leafCert.rawData)) + }); + + const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({ + caCertId: ca.activeCaCertId, + certificateAuthorityDAL, + certificateAuthorityCertDAL, + projectDAL, + kmsService + }); + + const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim(); + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + caCertId: caCert.id, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber, + notBefore: notBeforeDate, + notAfter: notAfterDate, + keyUsages: selectedKeyUsages, + extendedKeyUsages: selectedExtendedKeyUsages + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + return cert; + }); + + return { + certificate: leafCert, + certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), + issuingCaCertificate, + serialNumber, + ca, + commonName: subscriber.commonName, + subscriber + }; }; return { - createPkiSubscriber, - getPkiSubscriberById, - updatePkiSubscriber, - deletePkiSubscriber, - issuePkiSubscriberCert, - signPkiSubscriberCert + createSubscriber, + getSubscriberById, + updateSubscriber, + deleteSubscriber, + issueSubscriberCert, + signSubscriberCert }; }; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts index 3f5735d14..b149f93ee 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-types.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -37,4 +37,5 @@ export type TIssuePkiSubscriberCertDTO = { export type TSignPkiSubscriberCertDTO = { subscriberId: string; + csr: string; } & Omit; diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index ab90a71d4..1a2f4061b 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -188,6 +188,7 @@ export type TSignCertificateEvent = { properties: { caId?: string; certificateTemplateId?: string; + subscriberId?: string; commonName: string; userAgent?: string; }; @@ -198,6 +199,7 @@ export type TIssueCertificateEvent = { properties: { caId?: string; certificateTemplateId?: string; + subscriberId?: string; commonName: string; userAgent?: string; }; diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 5efe7ca69..7f79a0fb2 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -298,6 +298,10 @@ export const ROUTE_PATHS = Object.freeze({ PkiCollectionDetailsByIDPage: setRoute( "/cert-manager/$projectId/pki-collections/$collectionId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" + ), + PkiSubscriberDetailsByIDPage: setRoute( + "/cert-manager/$projectId/subscribers/$subscriberId", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId" ) }, Ssh: { diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx new file mode 100644 index 000000000..009934c5e --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/PkiSubscriberDetailsByIDPage.tsx @@ -0,0 +1,179 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + PageHeader, + Tooltip +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { + ProjectPermissionPkiSubscriberActions, + ProjectPermissionSub, + useWorkspace +} from "@app/context"; +import { useDeletePkiSubscriber, useGetPkiSubscriberById } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { PkiSubscriberModal } from "../PkiSubscribersPage/components/PkiSubscriberModal"; +// import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from "./components"; + +const Page = () => { + const { currentWorkspace } = useWorkspace(); + const navigate = useNavigate(); + const projectId = currentWorkspace?.id || ""; + const subscriberId = useParams({ + from: ROUTE_PATHS.CertManager.PkiSubscriberDetailsByIDPage.id, + select: (el) => el.subscriberId + }); + const { data } = useGetPkiSubscriberById(subscriberId); + + const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "pkiSubscriber", + "deletePkiSubscriber" + ] as const); + + const onRemoveSubscriberSubmit = async (subscriberIdToDelete: string) => { + try { + if (!projectId) return; + + await deletePkiSubscriber({ subscriberId: subscriberIdToDelete }); + + createNotification({ + text: "Successfully deleted PKI subscriber", + type: "success" + }); + + handlePopUpClose("deletePkiSubscriber"); + navigate({ + to: `/${ProjectType.CertificateManager}/$projectId/subscribers` as const, + params: { + projectId + } + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete PKI subscriber", + type: "error" + }); + } + }; + + return ( +
+ {data && ( +
+ + + +
+ + + +
+
+ + + {(isAllowed) => ( + + handlePopUpOpen("deletePkiSubscriber", { + subscriberId: data.id, + name: data.name + }) + } + disabled={!isAllowed} + > + Delete PKI Subscriber + + )} + + +
+
+
+
+ TODO: PkiSubscriberDetailsSection + {/* */} +
+
+ TODO: PkiSubscriberCertificatesSection + {/* */} +
+
+
+ )} + + handlePopUpToggle("deletePkiSubscriber", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveSubscriberSubmit( + (popUp?.deletePkiSubscriber?.data as { subscriberId: string })?.subscriberId + ) + } + /> +
+ ); +}; + +export const PkiSubscriberDetailsByIDPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "PKI Subscriber" })} + + {/* + + */} + TESTTT + + ); +}; + +/** + * import { createFileRoute } from "@tanstack/react-router"; + +import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId" +)({ + component: PkiSubscriberDetailsByIDPage +}); + + */ diff --git a/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx new file mode 100644 index 000000000..fb980ad5d --- /dev/null +++ b/frontend/src/pages/cert-manager/PkiSubscriberDetailsByIDPage/route.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId" +)({ + component: RouteComponent +}); + +function RouteComponent() { + return ( +
+ Hello + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId"! +
+ ); +} diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx index fae94b07b..a32b86b00 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscribersTable.tsx @@ -1,5 +1,6 @@ import { faEllipsis, faPencil, faServer, 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"; @@ -25,6 +26,7 @@ import { useWorkspace } from "@app/context"; import { useListWorkspacePkiSubscribers } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -35,6 +37,7 @@ type Props = { }; export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { + const navigate = useNavigate(); const { currentWorkspace } = useWorkspace(); const { data, isPending } = useListWorkspacePkiSubscribers(currentWorkspace?.id || ""); return ( @@ -55,7 +58,20 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => { data.length > 0 && data.map((subscriber) => { return ( - + { + e.preventDefault(); + navigate({ + to: `/${ProjectType.CertificateManager}/$projectId/subscribers/$subscriberId` as const, + params: { + projectId: currentWorkspace.id, + subscriberId: subscriber.id + } + }); + }} + > {subscriber.name} {subscriber.commonName} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 55eaff495..d78c30024 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -115,6 +115,7 @@ import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/se import { Route as secretManagerIntegrationsSelectIntegrationAuthPageRouteImport } from './pages/secret-manager/integrations/SelectIntegrationAuthPage/route' import { Route as secretManagerIntegrationsDetailsByIDPageRouteImport } from './pages/secret-manager/IntegrationsDetailsByIDPage/route' import { Route as organizationAppConnectionsOauthCallbackPageRouteImport } from './pages/organization/AppConnections/OauthCallbackPage/route' +import { Route as certManagerPkiSubscriberDetailsByIDPageRouteImport } from './pages/cert-manager/PkiSubscriberDetailsByIDPage/route' import { Route as certManagerCertAuthDetailsByIDPageRouteImport } from './pages/cert-manager/CertAuthDetailsByIDPage/route' import { Route as secretManagerIntegrationsListPageRouteImport } from './pages/secret-manager/IntegrationsListPage/route' import { Route as secretManagerIntegrationsWindmillConfigurePageRouteImport } from './pages/secret-manager/integrations/WindmillConfigurePage/route' @@ -1075,6 +1076,13 @@ const organizationAppConnectionsOauthCallbackPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute, } as any) +const certManagerPkiSubscriberDetailsByIDPageRouteRoute = + certManagerPkiSubscriberDetailsByIDPageRouteImport.update({ + id: '/$subscriberId', + path: '/$subscriberId', + getParentRoute: () => certManagerPkiSubscribersPageRouteRoute, + } as any) + const certManagerCertAuthDetailsByIDPageRouteRoute = certManagerCertAuthDetailsByIDPageRouteImport.update({ id: '/ca/$caId', @@ -2402,6 +2410,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof certManagerCertAuthDetailsByIDPageRouteImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId' + path: '/$subscriberId' + fullPath: '/cert-manager/$projectId/subscribers/$subscriberId' + preLoaderRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteImport + parentRoute: typeof certManagerPkiSubscribersPageRouteImport + } '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': { id: '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' path: '/$appConnection/oauth/callback' @@ -3231,12 +3246,27 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren, ) +interface certManagerPkiSubscribersPageRouteRouteChildren { + certManagerPkiSubscriberDetailsByIDPageRouteRoute: typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute +} + +const certManagerPkiSubscribersPageRouteRouteChildren: certManagerPkiSubscribersPageRouteRouteChildren = + { + certManagerPkiSubscriberDetailsByIDPageRouteRoute: + certManagerPkiSubscriberDetailsByIDPageRouteRoute, + } + +const certManagerPkiSubscribersPageRouteRouteWithChildren = + certManagerPkiSubscribersPageRouteRoute._addFileChildren( + certManagerPkiSubscribersPageRouteRouteChildren, + ) + interface certManagerLayoutRouteChildren { certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute - certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRoute + certManagerPkiSubscribersPageRouteRoute: typeof certManagerPkiSubscribersPageRouteRouteWithChildren projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute @@ -3252,7 +3282,7 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { certManagerCertificatesPageRouteRoute: certManagerCertificatesPageRouteRoute, certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute, certManagerPkiSubscribersPageRouteRoute: - certManagerPkiSubscribersPageRouteRoute, + certManagerPkiSubscribersPageRouteRouteWithChildren, projectAccessControlPageRouteCertManagerRoute: projectAccessControlPageRouteCertManagerRoute, certManagerCertAuthDetailsByIDPageRouteRoute: @@ -3923,7 +3953,7 @@ export interface FileRoutesByFullPath { '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute - '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute + '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRouteWithChildren '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute '/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute @@ -3953,6 +3983,7 @@ export interface FileRoutesByFullPath { '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/secret-manager/$projectId/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/cert-manager/$projectId/subscribers/$subscriberId': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4103,7 +4134,7 @@ export interface FileRoutesByTo { '/cert-manager/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute '/cert-manager/$projectId/overview': typeof certManagerCertificatesPageRouteRoute '/cert-manager/$projectId/settings': typeof certManagerSettingsPageRouteRoute - '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRoute + '/cert-manager/$projectId/subscribers': typeof certManagerPkiSubscribersPageRouteRouteWithChildren '/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute '/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute '/kms/$projectId/settings': typeof kmsSettingsPageRouteRoute @@ -4132,6 +4163,7 @@ export interface FileRoutesByTo { '/ssh/$projectId/access-management': typeof projectAccessControlPageRouteSshRoute '/secret-manager/$projectId/integrations': typeof secretManagerIntegrationsListPageRouteRoute '/cert-manager/$projectId/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/cert-manager/$projectId/subscribers/$subscriberId': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/secret-manager/$projectId/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/secret-manager/$projectId/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4300,7 +4332,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/overview': typeof certManagerCertificatesPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': typeof certManagerPkiSubscribersPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers': typeof certManagerPkiSubscribersPageRouteRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings': typeof kmsSettingsPageRouteRoute @@ -4330,6 +4362,7 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management': typeof projectAccessControlPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/': typeof secretManagerIntegrationsListPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId': typeof certManagerCertAuthDetailsByIDPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId': typeof certManagerPkiSubscriberDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback': typeof organizationAppConnectionsOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId': typeof secretManagerIntegrationsDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth': typeof secretManagerIntegrationsSelectIntegrationAuthPageRouteRoute @@ -4520,6 +4553,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/access-management' | '/secret-manager/$projectId/integrations/' | '/cert-manager/$projectId/ca/$caId' + | '/cert-manager/$projectId/subscribers/$subscriberId' | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' @@ -4698,6 +4732,7 @@ export interface FileRouteTypes { | '/ssh/$projectId/access-management' | '/secret-manager/$projectId/integrations' | '/cert-manager/$projectId/ca/$caId' + | '/cert-manager/$projectId/subscribers/$subscriberId' | '/organization/app-connections/$appConnection/oauth/callback' | '/secret-manager/$projectId/integrations/$integrationId' | '/secret-manager/$projectId/integrations/select-integration-auth' @@ -4894,6 +4929,7 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caId' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId' | '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/$integrationId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations/select-integration-auth' @@ -5465,7 +5501,10 @@ export const routeTree = rootRoute }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers": { "filePath": "cert-manager/PkiSubscribersPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout", + "children": [ + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId" + ] }, "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/kmip": { "filePath": "kms/KmipPage/route.tsx", @@ -5663,6 +5702,10 @@ export const routeTree = rootRoute "filePath": "cert-manager/CertAuthDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberId": { + "filePath": "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers" + }, "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback": { "filePath": "organization/AppConnections/OauthCallbackPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index d6a58e12c..b71931af1 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -284,6 +284,7 @@ const secretManagerIntegrationsRedirect = route("/integrations", [ const certManagerRoutes = route("/cert-manager/$projectId", [ layout("cert-manager-layout", "cert-manager/layout.tsx", [ + route("/subscribers/$subscriberId", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx"), route("/subscribers", "cert-manager/PkiSubscribersPage/route.tsx"), route("/overview", "cert-manager/CertificatesPage/route.tsx"), route("/certificate-authorities", "cert-manager/CertificateAuthoritiesPage/route.tsx"),