From a3acfa65a25cd74936e11b667344ffa8d94d47a9 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 7 Feb 2025 01:24:32 +0800 Subject: [PATCH] feat: finished up client cert generation --- backend/src/@types/knex.d.ts | 8 + .../db/migrations/20250203141127_add-kmip.ts | 18 ++ backend/src/db/schemas/index.ts | 1 + .../db/schemas/kmip-client-certificates.ts | 23 +++ backend/src/db/schemas/models.ts | 3 +- backend/src/ee/routes/v1/kmip-router.ts | 55 ++++++ .../ee/services/audit-log/audit-log-types.ts | 16 +- .../kmip/kmip-client-certificate-dal.ts | 13 ++ .../src/ee/services/kmip/kmip-constants.ts | 1 + backend/src/ee/services/kmip/kmip-service.ts | 185 +++++++++++++++++- backend/src/ee/services/kmip/kmip-types.ts | 7 + .../services/permission/project-permission.ts | 6 +- backend/src/server/routes/index.ts | 7 +- .../super-admin/super-admin-service.ts | 4 +- .../context/ProjectPermissionContext/types.ts | 3 +- frontend/src/hooks/api/kmip/mutation.ts | 21 +- frontend/src/hooks/api/kmip/types.ts | 14 ++ .../CreateKmipClientCertificateModal.tsx | 147 ++++++++++++++ .../components/KmipClientCertificateModal.tsx | 19 ++ .../KmipPage/components/KmipClientTable.tsx | 44 ++++- .../ProjectRoleModifySection.utils.tsx | 12 +- 21 files changed, 593 insertions(+), 14 deletions(-) create mode 100644 backend/src/db/schemas/kmip-client-certificates.ts create mode 100644 backend/src/ee/services/kmip/kmip-client-certificate-dal.ts create mode 100644 backend/src/ee/services/kmip/kmip-constants.ts create mode 100644 frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx create mode 100644 frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index f85e9393d..dabcfc060 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -143,6 +143,9 @@ import { TInternalKms, TInternalKmsInsert, TInternalKmsUpdate, + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate, TKmipClients, TKmipClientsInsert, TKmipClientsUpdate, @@ -922,5 +925,10 @@ declare module "knex/types/tables" { TKmipInstanceServerCertificatesInsert, TKmipInstanceServerCertificatesUpdate >; + [TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType< + TKmipClientCertificates, + TKmipClientCertificatesInsert, + TKmipClientCertificatesUpdate + >; } } diff --git a/backend/src/db/migrations/20250203141127_add-kmip.ts b/backend/src/db/migrations/20250203141127_add-kmip.ts index d4dba9011..081002dd7 100644 --- a/backend/src/db/migrations/20250203141127_add-kmip.ts +++ b/backend/src/db/migrations/20250203141127_add-kmip.ts @@ -63,6 +63,19 @@ export async function up(knex: Knex): Promise { t.binary("encryptedChain").notNullable(); }); } + + const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); + if (!hasKmipClientCertTable) { + await knex.schema.createTable(TableName.KmipClientCertificates, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("kmipClientId").notNullable(); + t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE"); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + }); + } } export async function down(knex: Knex): Promise { @@ -81,4 +94,9 @@ export async function down(knex: Knex): Promise { if (hasKmipInstanceServerCertTable) { await knex.schema.dropTable(TableName.KmipInstanceServerCertificates); } + + const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates); + if (hasKmipClientCertTable) { + await knex.schema.dropTable(TableName.KmipClientCertificates); + } } diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index e5dc3e53c..03f1f1136 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -45,6 +45,7 @@ export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-kms"; +export * from "./kmip-client-certificates"; export * from "./kmip-clients"; export * from "./kmip-instance-configs"; export * from "./kmip-instance-server-certificates"; diff --git a/backend/src/db/schemas/kmip-client-certificates.ts b/backend/src/db/schemas/kmip-client-certificates.ts new file mode 100644 index 000000000..a42d94a98 --- /dev/null +++ b/backend/src/db/schemas/kmip-client-certificates.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const KmipClientCertificatesSchema = z.object({ + id: z.string().uuid(), + kmipClientId: z.string().uuid(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date() +}); + +export type TKmipClientCertificates = z.infer; +export type TKmipClientCertificatesInsert = Omit, TImmutableDBKeys>; +export type TKmipClientCertificatesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index fd132b461..12f5768b1 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -135,7 +135,8 @@ export enum TableName { SecretSync = "secret_syncs", KmipClient = "kmip_clients", KmipInstanceConfig = "kmip_instance_configs", - KmipInstanceServerCertificates = "kmip_instance_server_certificates" + KmipInstanceServerCertificates = "kmip_instance_server_certificates", + KmipClientCertificates = "kmip_client_certificates" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts index 3bab6d7c0..dcbc3a501 100644 --- a/backend/src/ee/routes/v1/kmip-router.ts +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -1,3 +1,4 @@ +import ms from "ms"; import { z } from "zod"; import { KmipClientsSchema } from "@app/db/schemas"; @@ -8,6 +9,7 @@ import { OrderByDirection } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; const KmipClientResponseSchema = KmipClientsSchema.pick({ projectId: true, @@ -230,4 +232,57 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { return { kmipClients, totalCount }; } }); + + server.route({ + method: "POST", + url: "/clients/:id/certificates", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + body: z.object({ + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") + }), + response: { + 200: z.object({ + serialNumber: z.string(), + certificateChain: z.string(), + certificate: z.string(), + privateKey: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificate = await server.services.kmip.createKmipClientCertificate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + clientId: req.params.id, + ...req.body + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: certificate.projectId, + event: { + type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE, + metadata: { + clientId: req.params.id, + serialNumber: certificate.serialNumber, + ttl: req.body.ttl, + keyAlgorithm: req.body.keyAlgorithm + } + } + }); + + return certificate; + } + }); }; 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 422c9b1ed..3d8ca2ccd 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -258,7 +258,8 @@ export enum EventType { UPDATE_KMIP_CLIENT = "update-kmip-client", DELETE_KMIP_CLIENT = "delete-kmip-client", GET_KMIP_CLIENT = "get-kmip-client", - GET_KMIP_CLIENTS = "get-kmip-clients" + GET_KMIP_CLIENTS = "get-kmip-clients", + CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate" } interface UserActorMetadata { @@ -2112,6 +2113,16 @@ interface GetKmipClientsEvent { }; } +interface CreateKmipClientCertificateEvent { + type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE; + metadata: { + clientId: string; + ttl: string; + keyAlgorithm: string; + serialNumber: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2307,4 +2318,5 @@ export type Event = | UpdateKmipClientEvent | DeleteKmipClientEvent | GetKmipClientEvent - | GetKmipClientsEvent; + | GetKmipClientsEvent + | CreateKmipClientCertificateEvent; diff --git a/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts b/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts new file mode 100644 index 000000000..a1829c9f6 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-client-certificate-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TKmipClientCertificateDALFactory = ReturnType; + +export const kmipClientCertificateDALFactory = (db: TDbClient) => { + const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates); + + return { + ...kmipClientCertOrm + }; +}; diff --git a/backend/src/ee/services/kmip/kmip-constants.ts b/backend/src/ee/services/kmip/kmip-constants.ts new file mode 100644 index 000000000..9e7518875 --- /dev/null +++ b/backend/src/ee/services/kmip/kmip-constants.ts @@ -0,0 +1 @@ +export const INSTANCE_KMIP_CONFIG_ID = "00000000-0000-0000-0000-000000000000"; diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index e1dcc185f..c430d4d06 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -1,11 +1,25 @@ import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; +import crypto, { KeyObject } from "crypto"; +import ms from "ms"; import { ActionProjectType } from "@app/db/schemas"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal"; import { TKmipClientDALFactory } from "./kmip-client-dal"; +import { INSTANCE_KMIP_CONFIG_ID } from "./kmip-constants"; +import { TKmipInstanceConfigDALFactory } from "./kmip-instance-config-dal"; import { + TCreateKmipClientCertificateDTO, TCreateKmipClientDTO, TDeleteKmipClientDTO, TGetKmipClientDTO, @@ -15,12 +29,21 @@ import { type TKmipServiceFactoryDep = { kmipClientDAL: TKmipClientDALFactory; + kmipClientCertificateDAL: TKmipClientCertificateDALFactory; permissionService: Pick; + kmsService: Pick; + kmipInstanceConfigDAL: TKmipInstanceConfigDALFactory; }; export type TKmipServiceFactory = ReturnType; -export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipServiceFactoryDep) => { +export const kmipServiceFactory = ({ + kmipClientDAL, + permissionService, + kmipClientCertificateDAL, + kmipInstanceConfigDAL, + kmsService +}: TKmipServiceFactoryDep) => { const createKmipClient = async ({ actor, actorId, @@ -67,6 +90,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe }: TUpdateKmipClientDTO) => { const kmipClient = await kmipClientDAL.findById(id); + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -93,6 +122,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => { const kmipClient = await kmipClientDAL.findById(id); + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -115,6 +150,12 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => { const kmipClient = await kmipClientDAL.findById(id); + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${id} does not exist` + }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -151,5 +192,145 @@ export const kmipServiceFactory = ({ kmipClientDAL, permissionService }: TKmipSe return kmipClientDAL.findByProjectId({ projectId, ...rest }); }; - return { createKmipClient, updateKmipClient, deleteKmipClient, getKmipClient, listKmipClientsByProjectId }; + const createKmipClientCertificate = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + ttl, + keyAlgorithm, + clientId + }: TCreateKmipClientCertificateDTO) => { + const kmipClient = await kmipClientDAL.findById(clientId); + + if (!kmipClient) { + throw new NotFoundError({ + message: `KMIP client with ID ${clientId} does not exist` + }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId: kmipClient.projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.KMS + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionKmipActions.GenerateClientCertificates, + ProjectPermissionSub.Kmip + ); + + const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(INSTANCE_KMIP_CONFIG_ID); + if (!kmipInstanceConfig) { + throw new InternalServerError({ + message: "KMIP has not been configured for the instance." + }); + } + + const decryptWithRoot = kmsService.decryptWithRootKey(); + + const caCertObj = new x509.X509Certificate( + decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaCertificate) + ); + + const notBeforeDate = new Date(); + const notAfterDate = new Date(new Date().getTime() + ms(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" }); + } + + if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" }); + + // check not after constraint + if (notAfterDate > caCertNotAfterDate) { + throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); + } + + const alg = keyAlgorithmToAlgCfg(keyAlgorithm); + const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | + x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] | + x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) + ]; + + const caAlg = keyAlgorithmToAlgCfg(kmipInstanceConfig.caKeyAlgorithm as CertKeyAlgorithm); + + const decryptedCaCertChain = decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaChain).toString( + "utf-8" + ); + + const caSkObj = crypto.createPrivateKey({ + key: decryptWithRoot(kmipInstanceConfig.encryptedClientIntermediateCaPrivateKey), + format: "der", + type: "pkcs8" + }); + + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const serialNumber = createSerialNumber(); + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `OU=${kmipClient.projectId},CN=${clientId}`, + issuer: caCertObj.subject, + notBefore: notBeforeDate, + notAfter: notAfterDate, + signingKey: caPrivateKey, + publicKey: leafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const skLeafObj = KeyObject.from(leafKeys.privateKey); + const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim(); + + await kmipClientCertificateDAL.create({ + kmipClientId: clientId, + keyAlgorithm, + issuedAt: notBeforeDate, + expiration: notAfterDate, + serialNumber + }); + + return { + serialNumber, + privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string, + certificate: leafCert.toString("pem"), + certificateChain, + projectId: kmipClient.projectId + }; + }; + + return { + createKmipClient, + updateKmipClient, + deleteKmipClient, + getKmipClient, + listKmipClientsByProjectId, + createKmipClientCertificate + }; }; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index c0eee29b6..3447454e6 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -1,7 +1,14 @@ import { OrderByDirection, TProjectPermission } from "@app/lib/types"; +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; import { KmipPermission } from "./kmip-enum"; +export type TCreateKmipClientCertificateDTO = { + clientId: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; +} & Omit; + export type TCreateKmipClientDTO = { name: string; description?: string; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4a4e21530..b9a24889f 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -48,7 +48,8 @@ export enum ProjectPermissionKmipActions { CreateClients = "create-clients", UpdateClients = "update-clients", DeleteClients = "delete-clients", - ReadClients = "read-clients" + ReadClients = "read-clients", + GenerateClientCertificates = "generate-client-certificates" } export enum ProjectPermissionSub { @@ -596,7 +597,8 @@ const buildAdminPermissionRules = () => { ProjectPermissionKmipActions.CreateClients, ProjectPermissionKmipActions.UpdateClients, ProjectPermissionKmipActions.DeleteClients, - ProjectPermissionKmipActions.ReadClients + ProjectPermissionKmipActions.ReadClients, + ProjectPermissionKmipActions.GenerateClientCertificates ], ProjectPermissionSub.Kmip ); diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 109c9632c..1de9b7780 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -35,6 +35,7 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; +import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal"; import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { kmipInstanceConfigDALFactory } from "@app/ee/services/kmip/kmip-instance-config-dal"; import { kmipInstanceServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-instance-server-certificate-dal"; @@ -385,6 +386,7 @@ export const registerRoutes = async ( const projectTemplateDAL = projectTemplateDALFactory(db); const resourceMetadataDAL = resourceMetadataDALFactory(db); const kmipClientDAL = kmipClientDALFactory(db); + const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db); const kmipInstanceConfigDAL = kmipInstanceConfigDALFactory(db); const kmipInstanceServerCertificateDAL = kmipInstanceServerCertificateDALFactory(db); @@ -1429,7 +1431,10 @@ export const registerRoutes = async ( const kmipService = kmipServiceFactory({ kmipClientDAL, - permissionService + permissionService, + kmipClientCertificateDAL, + kmipInstanceConfigDAL, + kmsService }); await superAdminService.initServerCfg(); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 467ecb325..278a359c3 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -11,7 +11,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; -import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { isValidIp } from "@app/lib/ip"; import { TAuthLoginFactory } from "../auth/auth-login-service"; @@ -562,7 +562,7 @@ export const superAdminServiceFactory = ({ }: TGenerateInstanceKmipServerCertificateDTO) => { const kmipInstanceConfig = await kmipInstanceConfigDAL.findById(ADMIN_CONFIG_DB_UUID); if (!kmipInstanceConfig) { - throw new BadRequestError({ + throw new InternalServerError({ message: "KMIP has not been configured for the instance" }); } diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index e05506dde..d368a949f 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -28,7 +28,8 @@ export enum ProjectPermissionKmipActions { CreateClients = "create-clients", UpdateClients = "update-clients", DeleteClients = "delete-clients", - ReadClients = "read-clients" + ReadClients = "read-clients", + GenerateClientCertificates = "generate-client-certificates" } export enum ProjectPermissionSecretSyncActions { diff --git a/frontend/src/hooks/api/kmip/mutation.ts b/frontend/src/hooks/api/kmip/mutation.ts index 736c4c5cb..7e42dc050 100644 --- a/frontend/src/hooks/api/kmip/mutation.ts +++ b/frontend/src/hooks/api/kmip/mutation.ts @@ -3,7 +3,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { kmipKeys } from "./queries"; -import { TCreateKmipClient, TDeleteKmipClient, TUpdateKmipClient } from "./types"; +import { + KmipClientCertificate, + TCreateKmipClient, + TDeleteKmipClient, + TGenerateKmipClientCertificate, + TUpdateKmipClient +} from "./types"; export const useCreateKmipClient = () => { const queryClient = useQueryClient(); @@ -56,3 +62,16 @@ export const useDeleteKmipClients = () => { } }); }; + +export const useGenerateKmipClientCertificate = () => { + return useMutation({ + mutationFn: async (payload: TGenerateKmipClientCertificate) => { + const { data } = await apiRequest.post( + `/api/v1/kmip/clients/${payload.clientId}/certificates`, + payload + ); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/kmip/types.ts b/frontend/src/hooks/api/kmip/types.ts index fd2450174..f1b9fc88d 100644 --- a/frontend/src/hooks/api/kmip/types.ts +++ b/frontend/src/hooks/api/kmip/types.ts @@ -1,3 +1,4 @@ +import { CertKeyAlgorithm } from "../certificates/enums"; import { OrderByDirection } from "../generic/types"; export enum KmipPermission { @@ -30,6 +31,19 @@ export type TProjectKmipClientList = { totalCount: number; }; +export type TGenerateKmipClientCertificate = { + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + clientId: string; +}; + +export type KmipClientCertificate = { + serialNumber: string; + certificate: string; + certificateChain: string; + privateKey: string; +}; + export type TDeleteKmipClient = KeyRef & ProjectRef; export type TListProjectKmipClientsDTO = { diff --git a/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx b/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx new file mode 100644 index 000000000..57505e50a --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx @@ -0,0 +1,147 @@ +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, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; +import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; +import { useGenerateKmipClientCertificate } from "@app/hooks/api/kmip"; +import { KmipClientCertificate, TKmipClient } from "@app/hooks/api/kmip/types"; + +const formSchema = z.object({ + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + ttl: z.string() +}); + +export type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + kmipClient?: TKmipClient | null; + displayNewClientCertificate: (certificate: KmipClientCertificate) => void; +}; + +type FormProps = Pick & { + onComplete: () => void; +}; + +const KmipClientCertificateForm = ({ + displayNewClientCertificate, + kmipClient, + onComplete +}: FormProps) => { + const { mutateAsync: createKmipClientCertificate } = useGenerateKmipClientCertificate(); + + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema) + }); + + const handleKmipClientSubmit = async (payload: FormData) => { + if (!kmipClient) { + return; + } + + const certificate = await createKmipClientCertificate({ + ...payload, + clientId: kmipClient?.id + }); + + createNotification({ + text: "Successfully created KMIP client certificate", + type: "success" + }); + + displayNewClientCertificate(certificate); + onComplete(); + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + + + +
+ + ); +}; + +export const CreateKmipClientCertificateModal = ({ + isOpen, + onOpenChange, + kmipClient, + displayNewClientCertificate +}: Props) => { + return ( + + + onOpenChange(false)} + displayNewClientCertificate={displayNewClientCertificate} + kmipClient={kmipClient} + /> + + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx new file mode 100644 index 000000000..4f8744682 --- /dev/null +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientCertificateModal.tsx @@ -0,0 +1,19 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { KmipClientCertificate } from "@app/hooks/api/kmip/types"; +import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + certificate: KmipClientCertificate; +}; + +export const KmipClientCertificateModal = ({ isOpen, onOpenChange, certificate }: Props) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx index ac7cf2c55..b357a95b9 100644 --- a/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx @@ -2,6 +2,7 @@ import { faArrowDown, faArrowUp, faArrowUpRightFromSquare, + faCertificate, faEdit, faEllipsis, faMagnifyingGlass, @@ -45,7 +46,9 @@ import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip"; import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types"; +import { CreateKmipClientCertificateModal } from "./CreateKmipClientCertificateModal"; import { DeleteKmipClientModal } from "./DeleteKmipClientModal"; +import { KmipClientCertificateModal } from "./KmipClientCertificateModal"; import { KmipClientModal } from "./KmipClientModal"; export const KmipClientTable = () => { @@ -88,7 +91,9 @@ export const KmipClientTable = () => { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ "upsertKmipClient", - "deleteKmipClient" + "deleteKmipClient", + "generateKmipClientCert", + "displayKmipClientCert" ] as const); const handleSort = () => { @@ -107,6 +112,11 @@ export const KmipClientTable = () => { ProjectPermissionSub.Kmip ); + const cannotGenerateKmipClientCertificate = permission.cannot( + ProjectPermissionKmipActions.GenerateClientCertificates, + ProjectPermissionSub.Kmip + ); + return ( { + +
+ + handlePopUpOpen("generateKmipClientCert", kmipClient) + } + icon={} + iconPos="left" + isDisabled={cannotGenerateKmipClientCertificate} + > + Generate Certificate + +
+
{ onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)} kmipClient={popUp.upsertKmipClient.data as TKmipClient | null} /> + handlePopUpToggle("generateKmipClientCert", isOpen)} + kmipClient={popUp.generateKmipClientCert.data as TKmipClient | null} + displayNewClientCertificate={(certificate) => + handlePopUpOpen("displayKmipClientCert", certificate) + } + /> + handlePopUpToggle("displayKmipClientCert", isOpen)} + certificate={popUp.displayKmipClientCert.data} + />
); diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 15381b32f..7dcb118fd 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -53,7 +53,8 @@ const KmipPolicyActionSchema = z.object({ [ProjectPermissionKmipActions.ReadClients]: z.boolean().optional(), [ProjectPermissionKmipActions.CreateClients]: z.boolean().optional(), [ProjectPermissionKmipActions.UpdateClients]: z.boolean().optional(), - [ProjectPermissionKmipActions.DeleteClients]: z.boolean().optional() + [ProjectPermissionKmipActions.DeleteClients]: z.boolean().optional(), + [ProjectPermissionKmipActions.GenerateClientCertificates]: z.boolean().optional() }); const SecretRollbackPolicyActionSchema = z.object({ @@ -373,6 +374,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canEditClients = action.includes(ProjectPermissionKmipActions.UpdateClients); const canDeleteClients = action.includes(ProjectPermissionKmipActions.DeleteClients); const canCreateClients = action.includes(ProjectPermissionKmipActions.CreateClients); + const canGenerateClientCerts = action.includes( + ProjectPermissionKmipActions.GenerateClientCertificates + ); if (!formVal[subject]) formVal[subject] = [{}]; @@ -381,6 +385,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if (canEditClients) formVal[subject]![0][ProjectPermissionKmipActions.UpdateClients] = true; if (canCreateClients) formVal[subject]![0][ProjectPermissionKmipActions.CreateClients] = true; if (canDeleteClients) formVal[subject]![0][ProjectPermissionKmipActions.DeleteClients] = true; + if (canGenerateClientCerts) + formVal[subject]![0][ProjectPermissionKmipActions.GenerateClientCertificates] = true; return; } @@ -783,6 +789,10 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Delete clients", value: ProjectPermissionKmipActions.DeleteClients + }, + { + label: "Generate client certificates", + value: ProjectPermissionKmipActions.GenerateClientCertificates } ] }