From 943c2b0e69e5d779e0c8942f30d7dc3babb178da Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 18 Feb 2025 02:22:59 +0800 Subject: [PATCH] misc: finalize KMIP management --- backend/src/@types/fastify.d.ts | 2 +- .../src/ee/routes/v1/kmip-operation-router.ts | 107 +++++++--- backend/src/ee/routes/v1/kmip-router.ts | 61 +++++- .../ee/services/audit-log/audit-log-types.ts | 33 +++ .../src/ee/services/kmip/kmip-client-dal.ts | 23 ++- .../services/kmip/kmip-operation-service.ts | 159 +++++++++----- backend/src/ee/services/kmip/kmip-service.ts | 156 ++++++++++++-- backend/src/ee/services/kmip/kmip-types.ts | 10 +- .../src/ee/services/license/license-fns.ts | 3 +- .../src/ee/services/license/license-types.ts | 1 + .../ee/services/permission/org-permission.ts | 3 + backend/src/server/routes/index.ts | 6 +- .../src/hooks/api/auditLogs/constants.tsx | 3 + frontend/src/hooks/api/auditLogs/enums.tsx | 3 + frontend/src/hooks/api/kmip/mutation.ts | 10 - frontend/src/hooks/api/kmip/types.ts | 7 - frontend/src/hooks/api/subscriptions/types.ts | 1 + .../CreateKmipClientCertificateModal.tsx | 2 +- .../KmipPage/components/KmipClientTable.tsx | 22 +- .../components/KmipTab/OrgKmipTab.tsx | 195 ++---------------- 20 files changed, 490 insertions(+), 317 deletions(-) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index e552e5c91..b05d3184a 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -227,7 +227,7 @@ declare module "fastify" { // everywhere else access using service layer store: { user: Pick; - kmipClient: Pick; + kmipClient: Pick; }; } } diff --git a/backend/src/ee/routes/v1/kmip-operation-router.ts b/backend/src/ee/routes/v1/kmip-operation-router.ts index 32e3b76ec..f0a86b181 100644 --- a/backend/src/ee/routes/v1/kmip-operation-router.ts +++ b/backend/src/ee/routes/v1/kmip-operation-router.ts @@ -1,5 +1,3 @@ -import crypto from "crypto"; -import jwt, { JwtPayload } from "jsonwebtoken"; import z from "zod"; import { KmsKeysSchema } from "@app/db/schemas"; @@ -7,21 +5,17 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { SymmetricEncryption } from "@app/lib/crypto/cipher"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { writeLimit } from "@app/server/config/rateLimiter"; -import { ActorType } from "@app/services/auth/auth-type"; -import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; export const registerKmipOperationRouter = async (server: FastifyZodProvider) => { server.decorateRequest("kmipUser", null); - server.addHook("preHandler", async (req) => { - const token = req.headers["x-kmip-jwt"] as string; - const serverCertSerialNumber = req.headers["x-server-certificate-serial-number"] as string; - - if (!jwt) { - throw new ForbiddenRequestError({ - message: "Missing KMIP JWT" - }); - } + server.addHook("onRequest", async (req) => { + const clientId = req.headers["x-kmip-client-id"] as string; + const projectId = req.headers["x-kmip-project-id"] as string; + const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string; + const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string; if (!serverCertSerialNumber) { throw new ForbiddenRequestError({ @@ -29,25 +23,28 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }); } - const serverCert = await server.services.kmip.getServerCertificateBySerialNumber(serverCertSerialNumber); + if (!clientCertSerialNumber) { + throw new ForbiddenRequestError({ + message: "Missing client certificate serial number from request" + }); + } + + if (!clientId) { + throw new ForbiddenRequestError({ + message: "Missing client ID from request" + }); + } + + if (!projectId) { + throw new ForbiddenRequestError({ + message: "Missing project ID from request" + }); + } // TODO: assert that server certificate used is not revoked // TODO: assert that client certificate used is not revoked - const publicKey = crypto.createPublicKey({ - key: serverCert.publicKey, - format: "pem", - type: [CertKeyAlgorithm.ECDSA_P256, CertKeyAlgorithm.ECDSA_P384].includes(serverCert.keyAlgorithm) - ? "spki" - : "pkcs1" - }); - - const decodedToken = jwt.verify(token, publicKey) as JwtPayload & { projectId: string; clientId: string }; - - const kmipClient = await server.store.kmipClient.findOne({ - id: decodedToken.clientId, - projectId: decodedToken.projectId - }); + const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId); if (!kmipClient) { throw new NotFoundError({ @@ -55,9 +52,15 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }); } + if (kmipClient.orgId !== req.permission.orgId) { + throw new ForbiddenRequestError({ + message: "Client specified in the request does not belong in the organization" + }); + } + req.kmipUser = { - projectId: decodedToken.projectId, - clientId: decodedToken.clientId, + projectId, + clientId, name: kmipClient.name }; }); @@ -77,9 +80,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => 200: KmsKeysSchema } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.create({ ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, algorithm: req.body.algorithm }); @@ -124,9 +132,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.get({ ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, id: req.body.id }); @@ -172,9 +185,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.getAttributes({ ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, id: req.body.id }); @@ -216,9 +234,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.deleteOp({ ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, id: req.body.id }); @@ -261,9 +284,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.activate({ ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, id: req.body.id }); @@ -306,9 +334,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.revoke({ ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, id: req.body.id }); @@ -356,9 +389,14 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const objects = await server.services.kmipOperation.locate({ - ...req.kmipUser + ...req.kmipUser, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId }); await server.services.auditLog.createAuditLog({ @@ -403,10 +441,15 @@ export const registerKmipOperationRouter = async (server: FastifyZodProvider) => }) } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const object = await server.services.kmipOperation.register({ ...req.kmipUser, - ...req.body + ...req.body, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/ee/routes/v1/kmip-router.ts b/backend/src/ee/routes/v1/kmip-router.ts index 378592aba..bc73e0887 100644 --- a/backend/src/ee/routes/v1/kmip-router.ts +++ b/backend/src/ee/routes/v1/kmip-router.ts @@ -306,13 +306,26 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - return server.services.kmip.setupOrgKmip({ + const chains = await server.services.kmip.setupOrgKmip({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.SETUP_KMIP, + metadata: { + keyAlgorithm: req.body.caKeyAlgorithm + } + } + }); + + return chains; } }); @@ -332,46 +345,76 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - return server.services.kmip.getOrgKmip({ + const kmip = await server.services.kmip.getOrgKmip({ 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.GET_KMIP, + metadata: { + id: kmip.id + } + } + }); + + return kmip; } }); server.route({ method: "POST", - url: "/server-certificates", + url: "/server-registration", config: { rateLimit: writeLimit }, schema: { body: z.object({ - commonName: z.string().trim().min(1), - altNames: validateAltNamesField, - keyAlgorithm: z.nativeEnum(CertKeyAlgorithm), + hostnamesOrIps: validateAltNamesField, + commonName: z.string().trim().min(1).optional(), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048), ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number") }), response: { 200: z.object({ - serialNumber: z.string(), + clientCertificateChain: z.string(), certificateChain: z.string(), certificate: z.string(), privateKey: z.string() }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - return server.services.kmip.generateOrgKmipServerCertificate({ + const configs = await server.services.kmip.registerServer({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.REGISTER_KMIP_SERVER, + metadata: { + serverCertificateSerialNumber: configs.serverCertificateSerialNumber, + hostnamesOrIps: req.body.hostnamesOrIps, + commonName: req.body.commonName ?? "kmip-server", + keyAlgorithm: req.body.keyAlgorithm, + ttl: req.body.ttl + } + } + }); + + return configs; } }); }; 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 3787e7b03..54834fe95 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -267,6 +267,11 @@ export enum EventType { GET_KMIP_CLIENT = "get-kmip-client", GET_KMIP_CLIENTS = "get-kmip-clients", CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate", + + SETUP_KMIP = "setup-kmip", + GET_KMIP = "get-kmip", + REGISTER_KMIP_SERVER = "register-kmip-server", + KMIP_OPERATION_CREATE = "kmip-operation-create", KMIP_OPERATION_GET = "kmip-operation-get", KMIP_OPERATION_DELETE = "kmip-operation-delete", @@ -2207,6 +2212,31 @@ interface KmipOperationRegisterEvent { }; } +interface SetupKmipEvent { + type: EventType.SETUP_KMIP; + metadata: { + keyAlgorithm: CertKeyAlgorithm; + }; +} + +interface GetKmipEvent { + type: EventType.GET_KMIP; + metadata: { + id: string; + }; +} + +interface RegisterKmipServerEvent { + type: EventType.REGISTER_KMIP_SERVER; + metadata: { + serverCertificateSerialNumber: string; + hostnamesOrIps: string; + commonName: string; + keyAlgorithm: CertKeyAlgorithm; + ttl: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -2404,6 +2434,9 @@ export type Event = | GetKmipClientEvent | GetKmipClientsEvent | CreateKmipClientCertificateEvent + | SetupKmipEvent + | GetKmipEvent + | RegisterKmipServerEvent | KmipOperationGetEvent | KmipOperationDeleteEvent | KmipOperationCreateEvent diff --git a/backend/src/ee/services/kmip/kmip-client-dal.ts b/backend/src/ee/services/kmip/kmip-client-dal.ts index 25043d35c..2650ebad0 100644 --- a/backend/src/ee/services/kmip/kmip-client-dal.ts +++ b/backend/src/ee/services/kmip/kmip-client-dal.ts @@ -13,6 +13,26 @@ export type TKmipClientDALFactory = ReturnType; export const kmipClientDALFactory = (db: TDbClient) => { const kmipClientOrm = ormify(db, TableName.KmipClient); + const findByProjectAndClientId = async (projectId: string, clientId: string) => { + try { + const client = await db + .replicaNode()(TableName.KmipClient) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`) + .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`) + .where({ + [`${TableName.KmipClient}.projectId` as "projectId"]: projectId, + [`${TableName.KmipClient}.id` as "id"]: clientId + }) + .select(selectAllTableCols(TableName.KmipClient)) + .select(db.ref("id").withSchema(TableName.Organization).as("orgId")) + .first(); + + return client; + } catch (error) { + throw new DatabaseError({ error, name: "Find by project and client ID" }); + } + }; + const findByProjectId = async ( { projectId, @@ -60,6 +80,7 @@ export const kmipClientDALFactory = (db: TDbClient) => { return { ...kmipClientOrm, - findByProjectId + findByProjectId, + findByProjectAndClientId }; }; diff --git a/backend/src/ee/services/kmip/kmip-operation-service.ts b/backend/src/ee/services/kmip/kmip-operation-service.ts index 17e19290e..dddfe331e 100644 --- a/backend/src/ee/services/kmip/kmip-operation-service.ts +++ b/backend/src/ee/services/kmip/kmip-operation-service.ts @@ -1,9 +1,12 @@ -import { ProjectType } from "@app/db/schemas"; +import { ForbiddenError } from "@casl/ability"; + import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; import { TKmipClientDALFactory } from "./kmip-client-dal"; import { KmipPermission } from "./kmip-enum"; import { @@ -21,6 +24,7 @@ type TKmipOperationServiceFactoryDep = { kmsDAL: TKmsKeyDALFactory; kmipClientDAL: TKmipClientDALFactory; projectDAL: Pick; + permissionService: Pick; }; export type TKmipOperationServiceFactory = ReturnType; @@ -29,16 +33,28 @@ export const kmipOperationServiceFactory = ({ kmsService, kmsDAL, projectDAL, - kmipClientDAL + kmipClientDAL, + permissionService }: TKmipOperationServiceFactoryDep) => { - const create = async ({ projectId: preSplitProjectId, clientId, algorithm }: TKmipCreateDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + const create = async ({ + projectId, + clientId, + algorithm, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipCreateDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); - const project = await projectDAL.findById(projectId); const kmipClient = await kmipClientDAL.findOne({ id: clientId, projectId @@ -52,7 +68,7 @@ export const kmipOperationServiceFactory = ({ const kmsKey = await kmsService.generateKmsKey({ encryptionAlgorithm: algorithm, - orgId: project.orgId, + orgId: actorOrgId, projectId, isReserved: false }); @@ -60,13 +76,16 @@ export const kmipOperationServiceFactory = ({ return kmsKey; }; - const deleteOp = async ({ projectId: preSplitProjectId, id, clientId }: TKmipDeleteDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const deleteOp = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDeleteDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, @@ -104,13 +123,16 @@ export const kmipOperationServiceFactory = ({ return kms; }; - const get = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, @@ -158,13 +180,16 @@ export const kmipOperationServiceFactory = ({ }; }; - const activate = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, @@ -192,13 +217,16 @@ export const kmipOperationServiceFactory = ({ }; }; - const revoke = async ({ projectId: preSplitProjectId, id, clientId }: TKmipRevokeDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, @@ -242,13 +270,24 @@ export const kmipOperationServiceFactory = ({ }; }; - const getAttributes = async ({ projectId: preSplitProjectId, id, clientId }: TKmipGetAttributesDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const getAttributes = async ({ + projectId, + id, + clientId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipGetAttributesDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, @@ -291,13 +330,16 @@ export const kmipOperationServiceFactory = ({ }; }; - const locate = async ({ projectId: preSplitProjectId, clientId }: TKmipLocateDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, @@ -315,13 +357,26 @@ export const kmipOperationServiceFactory = ({ return keys; }; - const register = async ({ projectId: preSplitProjectId, clientId, key, algorithm, name }: TKmipRegisterDTO) => { - let projectId = preSplitProjectId; - const cmekProjectFromSplit = await projectDAL.getProjectFromSplitId(projectId, ProjectType.KMS); + const register = async ({ + projectId, + clientId, + key, + algorithm, + name, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TKmipRegisterDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); - if (cmekProjectFromSplit) { - projectId = cmekProjectFromSplit.id; - } + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); const kmipClient = await kmipClientDAL.findOne({ id: clientId, diff --git a/backend/src/ee/services/kmip/kmip-service.ts b/backend/src/ee/services/kmip/kmip-service.ts index 1f0d403ea..48876f2b2 100644 --- a/backend/src/ee/services/kmip/kmip-service.ts +++ b/backend/src/ee/services/kmip/kmip-service.ts @@ -15,6 +15,7 @@ import { hostnameRegex } from "@app/services/certificate-authority/certificate-a import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission"; @@ -30,6 +31,7 @@ import { TGetKmipClientDTO, TGetOrgKmipDTO, TListKmipClientsByProjectIdDTO, + TRegisterServerDTO, TSetupOrgKmipDTO, TUpdateKmipClientDTO } from "./kmip-types"; @@ -41,6 +43,7 @@ type TKmipServiceFactoryDep = { permissionService: Pick; kmsService: Pick; kmipOrgConfigDAL: TKmipOrgConfigDALFactory; + licenseService: Pick; }; export type TKmipServiceFactory = ReturnType; @@ -51,7 +54,8 @@ export const kmipServiceFactory = ({ kmipClientCertificateDAL, kmipOrgConfigDAL, kmsService, - kmipOrgServerCertificateDAL + kmipOrgServerCertificateDAL, + licenseService }: TKmipServiceFactoryDep) => { const createKmipClient = async ({ actor, @@ -77,6 +81,12 @@ export const kmipServiceFactory = ({ ProjectPermissionSub.Kmip ); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to create KMIP client. Upgrade your plan to enterprise." + }); + const kmipClient = await kmipClientDAL.create({ projectId, name, @@ -105,6 +115,12 @@ export const kmipServiceFactory = ({ }); } + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to update KMIP client. Upgrade your plan to enterprise." + }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -151,6 +167,12 @@ export const kmipServiceFactory = ({ ProjectPermissionSub.Kmip ); + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to delete KMIP client. Upgrade your plan to enterprise." + }); + const deletedKmipClient = await kmipClientDAL.deleteById(id); return deletedKmipClient; @@ -218,6 +240,12 @@ export const kmipServiceFactory = ({ }); } + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to create KMIP client. Upgrade your plan to enterprise." + }); + const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -340,6 +368,31 @@ export const kmipServiceFactory = ({ }; }; + const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => { + const serverCert = await kmipOrgServerCertificateDAL.findOne({ + serialNumber, + orgId + }); + + if (!serverCert) { + throw new NotFoundError({ + message: "Server certificate not found" + }); + } + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate })); + + return { + publicKey: parsedCertificate.publicKey.toString("pem"), + keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm + }; + }; + const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { const { permission } = await permissionService.getOrgPermission( actor, @@ -360,6 +413,12 @@ export const kmipServiceFactory = ({ }); } + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to setup KMIP. Upgrade your plan to enterprise." + }); + const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm); // generate root CA @@ -499,7 +558,9 @@ export const kmipServiceFactory = ({ }; }; - const getOrgKmip = async ({ actorOrgId }: TGetOrgKmipDTO) => { + const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { + await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId); + const kmipConfig = await kmipOrgConfigDAL.findOne({ orgId: actorOrgId }); @@ -525,32 +586,21 @@ export const kmipServiceFactory = ({ ); return { + id: kmipConfig.id, serverCertificateChain: `${serverIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(), clientCertificateChain: `${clientIntermediateCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim() }; }; const generateOrgKmipServerCertificate = async ({ - actorOrgId, - actor, - actorId, - actorAuthMethod, + orgId, ttl, commonName, altNames, keyAlgorithm }: TGenerateOrgKmipServerCertificateDTO) => { - const { permission } = await permissionService.getOrgPermission( - actor, - actorId, - actorOrgId, - actorAuthMethod, - actorOrgId - ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); - const kmipOrgConfig = await kmipOrgConfigDAL.findOne({ - orgId: actorOrgId + orgId }); if (!kmipOrgConfig) { @@ -559,9 +609,15 @@ export const kmipServiceFactory = ({ }); } + const plan = await licenseService.getPlan(orgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise." + }); + const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, - orgId: actorOrgId + orgId }); const caCertObj = new x509.X509Certificate( @@ -668,7 +724,7 @@ export const kmipServiceFactory = ({ const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim(); await kmipOrgServerCertificateDAL.create({ - orgId: actorOrgId, + orgId, keyAlgorithm, issuedAt: notBeforeDate, expiration: notAfterDate, @@ -687,6 +743,66 @@ export const kmipServiceFactory = ({ }; }; + const registerServer = async ({ + actorOrgId, + actor, + actorId, + actorAuthMethod, + ttl, + commonName, + keyAlgorithm, + hostnamesOrIps + }: TRegisterServerDTO) => { + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + + const kmipConfig = await kmipOrgConfigDAL.findOne({ + orgId: actorOrgId + }); + + if (!kmipConfig) { + throw new BadRequestError({ + message: "KMIP has not been configured for the organization" + }); + } + + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.kmip) + throw new BadRequestError({ + message: "Failed to register KMIP server. Upgrade your plan to enterprise." + }); + + const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({ + orgId: actorOrgId, + commonName: commonName ?? "kmip-server", + altNames: hostnamesOrIps, + keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm), + ttl + }); + + const { clientCertificateChain } = await getOrgKmip({ + actor, + actorAuthMethod, + actorId, + actorOrgId + }); + + return { + serverCertificateSerialNumber: serialNumber, + clientCertificateChain, + privateKey, + certificate, + certificateChain + }; + }; + return { createKmipClient, updateKmipClient, @@ -696,6 +812,8 @@ export const kmipServiceFactory = ({ createKmipClientCertificate, setupOrgKmip, generateOrgKmipServerCertificate, - getOrgKmip + getOrgKmip, + getServerCertificateBySerialNumber, + registerServer }; }; diff --git a/backend/src/ee/services/kmip/kmip-types.ts b/backend/src/ee/services/kmip/kmip-types.ts index 3e8ce7a3b..fd9808187 100644 --- a/backend/src/ee/services/kmip/kmip-types.ts +++ b/backend/src/ee/services/kmip/kmip-types.ts @@ -46,7 +46,7 @@ export type TListKmipClientsByProjectIdDTO = { type KmipOperationBaseDTO = { clientId: string; projectId: string; -}; +} & Omit; export type TKmipCreateDTO = { algorithm: SymmetricEncryption; @@ -91,4 +91,12 @@ export type TGenerateOrgKmipServerCertificateDTO = { altNames: string; keyAlgorithm: CertKeyAlgorithm; ttl: string; + orgId: string; +}; + +export type TRegisterServerDTO = { + hostnamesOrIps: string; + commonName?: string; + keyAlgorithm?: CertKeyAlgorithm; + ttl: string; } & Omit; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index c54daa3a6..cab19a530 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -50,7 +50,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ }, pkiEst: false, enforceMfa: false, - projectTemplates: false + projectTemplates: false, + kmip: false }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 0242377b0..2b4dbe49e 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -68,6 +68,7 @@ export type TFeatureSet = { pkiEst: boolean; enforceMfa: boolean; projectTemplates: false; + kmip: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index e71e251a1..03d1747d4 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -268,6 +268,9 @@ const buildAdminPermission = () => { can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); + // the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI + can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + return rules; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 01967f061..09936467c 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1434,14 +1434,16 @@ export const registerRoutes = async ( kmipClientCertificateDAL, kmipOrgConfigDAL, kmsService, - kmipOrgServerCertificateDAL + kmipOrgServerCertificateDAL, + licenseService }); const kmipOperationService = kmipOperationServiceFactory({ kmsService, kmsDAL, projectDAL, - kmipClientDAL + kmipClientDAL, + permissionService }); await superAdminService.initServerCfg(); diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index e750d0a5b..db6bcb63d 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -125,6 +125,9 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.GET_KMIP_CLIENT]: "Get KMIP client", [EventType.GET_KMIP_CLIENTS]: "Get KMIP clients", [EventType.CREATE_KMIP_CLIENT_CERTIFICATE]: "Create KMIP client certificate", + [EventType.SETUP_KMIP]: "Setup KMIP configuration", + [EventType.GET_KMIP]: "Get KMIP configuration", + [EventType.REGISTER_KMIP_SERVER]: "Register KMIP server", [EventType.KMIP_OPERATION_CREATE]: "KMIP operation create", [EventType.KMIP_OPERATION_GET]: "KMIP operation get", [EventType.KMIP_OPERATION_DELETE]: "KMIP operation delete", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 1294414d5..185719f96 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -137,6 +137,9 @@ export enum EventType { GET_KMIP_CLIENT = "get-kmip-client", GET_KMIP_CLIENTS = "get-kmip-clients", CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate", + SETUP_KMIP = "setup-kmip", + GET_KMIP = "get-kmip", + REGISTER_KMIP_SERVER = "register-kmip-server", KMIP_OPERATION_CREATE = "kmip-operation-create", KMIP_OPERATION_GET = "kmip-operation-get", KMIP_OPERATION_DELETE = "kmip-operation-delete", diff --git a/frontend/src/hooks/api/kmip/mutation.ts b/frontend/src/hooks/api/kmip/mutation.ts index fed5e01b0..af86c1fe0 100644 --- a/frontend/src/hooks/api/kmip/mutation.ts +++ b/frontend/src/hooks/api/kmip/mutation.ts @@ -5,11 +5,9 @@ import { apiRequest } from "@app/config/request"; import { kmipKeys } from "./queries"; import { KmipClientCertificate, - OrgKmipServerCert, TCreateKmipClient, TDeleteKmipClient, TGenerateKmipClientCertificate, - TGenerateOrgKmipServerCertDTO, TSetupOrgKmipDTO, TUpdateKmipClient } from "./types"; @@ -90,11 +88,3 @@ export const useSetupOrgKmip = (orgId: string) => { } }); }; - -export const useGenerateOrgKmipServerCert = () => { - return useMutation({ - mutationFn: async (payload: TGenerateOrgKmipServerCertDTO) => { - return apiRequest.post("/api/v1/kmip/server-certificates", payload); - } - }); -}; diff --git a/frontend/src/hooks/api/kmip/types.ts b/frontend/src/hooks/api/kmip/types.ts index d8353aa61..d065fd3fe 100644 --- a/frontend/src/hooks/api/kmip/types.ts +++ b/frontend/src/hooks/api/kmip/types.ts @@ -73,13 +73,6 @@ export type TSetupOrgKmipDTO = { caKeyAlgorithm: CertKeyAlgorithm; }; -export type TGenerateOrgKmipServerCertDTO = { - commonName: string; - keyAlgorithm: CertKeyAlgorithm; - altNames: string; - ttl: string; -}; - export type OrgKmipServerCert = { serialNumber: string; certificate: string; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index c67b3f8c3..3129dd508 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -46,4 +46,5 @@ export type SubscriptionPlan = { pkiEst: boolean; enforceMfa: boolean; projectTemplates: boolean; + kmip: boolean; }; diff --git a/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx b/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx index 57505e50a..2cb56e7f9 100644 --- a/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx +++ b/frontend/src/pages/kms/KmipPage/components/CreateKmipClientCertificateModal.tsx @@ -107,7 +107,7 @@ const KmipClientCertificateForm = ({ )} /> -
+
); diff --git a/frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx b/frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx index c2c5dd5e6..41d1478ee 100644 --- a/frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/KmipTab/OrgKmipTab.tsx @@ -4,12 +4,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { Button, FormControl, IconButton, - Input, Modal, ModalContent, Select, @@ -18,18 +18,13 @@ import { TextArea, Tooltip } from "@app/components/v2"; -import { useOrganization } from "@app/context"; +import { useOrganization, useSubscription } from "@app/context"; import { downloadTxtFile } from "@app/helpers/download"; import { usePopUp, useTimedReset } from "@app/hooks"; import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; -import { - useGenerateOrgKmipServerCert, - useGetOrgKmipConfig, - useSetupOrgKmip -} from "@app/hooks/api/kmip"; +import { useGetOrgKmipConfig, useSetupOrgKmip } from "@app/hooks/api/kmip"; import { OrgKmipConfig } from "@app/hooks/api/kmip/types"; -import { CertificateContent } from "@app/pages/cert-manager/CertificatesPage/components/CertificatesTab/components/CertificateContent"; const orgConfigFormSchema = z.object({ caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm) @@ -45,8 +40,11 @@ const OrgConfigSection = ({ isKmipConfigLoading: boolean; }) => { const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([ - "configureKmip" + "configureKmip", + "upgradePlan" ] as const); + const { subscription } = useSubscription(); + const { handleSubmit, control, @@ -181,6 +179,11 @@ const OrgConfigSection = ({ - handlePopUpToggle("configureKmipServerCert", state)} - > - -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> -
- - -
- -
-
- handlePopUpToggle("showCertificate", state)} - > - - - - -
- ); -}; - export const KmipTab = () => { const { currentOrg } = useOrganization(); const { data: kmipConfig, isPending } = useGetOrgKmipConfig(currentOrg.id); @@ -423,7 +263,6 @@ export const KmipTab = () => { return (
- {kmipConfig && }
); };